čtvrtek 25. prosince 2014

ASP.NET MVC Binding podruhé

V minulém příspěvku jsem ukázal, jak navázat objekt s více jak jednou vlastností na ovládací prvek. Nyní popíši obrácený přístup, kdy je jedna vlastnost navázaná na dva ovládací prvky. Lze to řešit několika možnými způsoby, takže můj přístup určitě není jediný možný.



Mějme tedy takovýto ViewModel:

public class Schedule
{
    public int Duration { get; set; }
}

Vlastnost Duration udává dobu trvání v minutách. Na stránce ovšem chceme uživateli umožnit výběr pomocí dvou dropdownů - jeden pro nastavení hodin, druhý pro minut:


Formulář získáme poměrně snadno tímto kódem ve View - maximální délka Duration je omezena, minuty lze nastavovat pouze po násobcích pěti:

@model Schedule

<!DOCTYPE html>

<html>
<head>
    <title>Schedule</title>
</head>
<body>
    <div>
        @using (Html.BeginForm())
        {
            @:Hours: @Html.DropDownList("hours", this.GetHours()) Minutes: @Html.DropDownList("minutes", this.GetMinutes())<br />
            <input type="submit" />
        }
    </div>
</body>
</html>

@functions {

    IEnumerable<SelectListItem> GetHours()
    {
        int hours = Model.Duration / 60;
        int maxHours = (5 * 60) / 60;

        for (int i = 0; i < maxHours; i++)
        {
            yield return new SelectListItem()
            {
                Text = i.ToString(),
                Value = i.ToString(),
                Selected = (hours == i)
            };
        }
    }

    IEnumerable<SelectListItem> GetMinutes()
    {
        int minutes = ((Model.Duration % 60) / 5) * 5;

        for (int i = 0; i < 60; i = i + 5)
        {
            yield return new SelectListItem()
            {
                Text = i.ToString(),
                Value = i.ToString(),
                Selected = (minutes == i)
            };
        }

    }
}

Přizpůsobené bindování

Po odeslání formuláře je nutné nějak získat z hodnot těchto dvou prvků hodnotu Duration. Jedním z řešení je napsání vlastního binderu, tedy vlastně předpisu, jak se mají hodnoty pro vlastnost Duration získat z hodnot požadavku.
Není to navíc nic složitého ´- založíme si novu třídu ScheduleBinder, která bude dědit z třídy DefaultModelBinder. U ní přepíšeme metodu BindProperty - nejprve necháme proběhnout původní metodu a poté otestujeme, zda je nastavována vlastnost Duration a pokud ano, tak získat hodnoty pro Hours a Minutes, převést na číslo a nastavit vlastnost Duration:

public class ScheduleBinder : DefaultModelBinder
{
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
    {
        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);

        if (!string.Equals(propertyDescriptor.Name, "Duration"))
            return;
     
        var hoursValue= bindingContext.ValueProvider.GetValue("hours");
        var minutesValue = bindingContext.ValueProvider.GetValue("minutes");

        if (hoursValue == null || minutesValue == null)
            return;

        int hours = 0;
        int minutes = 0;
        
        if(!int.TryParse(hoursValue.AttemptedValue, out hours) || !int.TryParse(minutesValue.AttemptedValue, out minutes))
            return;

        propertyDescriptor.SetValue(bindingContext.Model, (hours * 60 + minutes));
    }
}

Za povšimnutí stojí, že pro získání hodnot hours a minutes se použivá ValueProvider  - neříká se tedy, odkud mají hodnoty pocházet. Poskytovatelů hodnot je hned několik a MVC je postupně volá, dokud některý z nich nevrátí hodnotu. Na následujícím obrázku je toto zachyceno, všimněte si prosím, že pořadí je důležité, např. hodnoty předané POST (Form) formulářem  mají přednost před parametry volání (GET - Query String):



Nyní jenom zbývá sdělit MVC, že chceme tento binder používat.  To můžeme udělat na třech místech:
  • pomocí atributu ModelBinderAttribute před parametrem u ActionMetody v controlleru:

public ActionResult Schedule([ModelBinder(typeof(ScheduleBinder))] Schedule model)
    
  •  pomocí atributu ModelBinderAttribute před definicí třídy:
[ModelBinder(typeof(ScheduleBinder)]
public class Schedule
{
    public int Duration { get; set; }
}
  • globálním nastavením v Global.asax, v metodě Application_Start:
protected void Application_Start()
{
    ModelBinders.Binders.Add(typeof(Schedule), new ScheduleBinder());

    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}

Upřednosťuji zápis přímo u parametru u ActionMetody - hned vidím, podle čeho je daný parametr plněn a mohu to změnit.

Další úpravy

Bylo by dobré před  nastavováním vlastnosti Duration napřed zjistit, zda už nebyla nastavena, defaultním binderem, například proto, že v požadavku, co na server dorazil, byla již hodnota duration (například požadavak vypadal ~/Home/Schedule?Duration=200)  - toho docílíme testováním objektu ModelState na přítomnost klíče Duration - pokud existuje, byla tato vlastnost již nastavena a není třeba ji nastavovat jinak.

Pokud nebudou v příchozím požadavku žádné hodnoty, které bychom mohli použít, tedy ani Duration a ani Hours a Minutes, můžeme v binderu rovnou nastavit výchozí hodnotu, například 180 minut.

Po těchto dvou úpravách vypadá binder takto:

    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
    {
        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);

        if (!string.Equals(propertyDescriptor.Name, "Duration") || bindingContext.ModelState.ContainsKey("Duration"))
            return;
        
        var hoursValue= bindingContext.ValueProvider.GetValue("hours");
        var minutesValue = bindingContext.ValueProvider.GetValue("minutes");

        int hours = 0;
        int minutes = 0;

        if (hoursValue == null || minutesValue == null ||
            !int.TryParse(hoursValue.AttemptedValue, out hours) || !int.TryParse(minutesValue.AttemptedValue, out minutes))
        {
            hours = 3;
        }
                    
        propertyDescriptor.SetValue(bindingContext.Model, (hours * 60 + minutes));
    }
}

Žádné komentáře:

Okomentovat