sobota 27. prosince 2014

ASP.NET Binding kolekcí po třetí

View modely, které obsahují kolekce objektů, s sebou nesou obvykle nutnost testovat kolekci na na null hodnotu. Což není příjemné, komplikuje to kód a ten je i méně čitelný. V tomto posledním příspěvku věnovaného kolekcím v ASP.NET MVC popíši jedno z možných řešení.



Null hodnota kolekce

Pokud tedy máme takovouto třídu:

public class Family
{
    public IEnumerable<Person> Members { get; set; }
}

a tu využíváme v ActionMetodě pro vstupní parametr:

public ActionResult Family(Family family)
{
    return View(family);
}

a poté jako model následně ve view, abychom zjistili, zda obsahuje nějakou položku, například pomocí tohoto helperu:

@helper Checked(Person person)
{
    if (this.Model.Members.Any(m => m.FirstName.Equals(person.FirstName) && m.LastName.Equals(person.LastName)))
    {
        @:checked="checked"
    }
}

tak se nám obvykle stane, že volání this.Model.Members vyhodí vyjímku, protože Members jsou null - v příchozím požadavku (například první Get na stránku) žádné informace o Members nebyly, takže výchozí binder neměl z čeho kolekci naplnit a tak ji nechal prázdnou.

Tomu se lze vyhnout například tím, že v konstruktoru objektu nastavíme příslušnou vlastnost na prázdnou kolekci. Což ovšem neřeší případ, kdy ActionMetoda vypadá takto:

public ActionResult Index(IEnumerable<Person> persons)
{
    if (persons == null)
        persons = Enumerable.Empty<Person>();

    return View(persons);
}

Zde tedy musíme nutně testovat persons na null  a provést úpravu - popřípadě toto řešit v následném kódu (ve view apod) a zde provádět před každým použítím persons  (modelu) test na null.

Náhrada výchozího binderu

Druhým způsobem je upravit defaultni binder, který v případě, že zjistí null hodnotu, tak tuto hodnotu nastaví na prázdnou kolekci.  Jak na to bylo popsáno v předchozím díle, takže tento nový binder bude vypadat nějak takto:


public class ArrayBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var model = base.BindModel(controllerContext, bindingContext);

        if (model == null)
        {
            Type modelType = bindingContext.ModelType;
            model = CreateEnumerableIfAppropriate(modelType);
        }

        return model;
    }

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

        var value = propertyDescriptor.GetValue(bindingContext.Model);

        if (value != null) return;

        var type = propertyDescriptor.PropertyType;
        value = this.CreateEnumerableIfAppropriate(type);

        if (value != null)
        {
            propertyDescriptor.SetValue(bindingContext.Model, value);
        }
    }


    private readonly IEnumerable<Type> genericEnumerableTypes = new Type[] { typeof(IEnumerable<>), typeof(ICollection<>), typeof(IList<>) };
    private object CreateEnumerableIfAppropriate(Type type)
    {
        Type typeToCreate = null;


        if (type.IsArray)
        {
            return Array.CreateInstance(type.GetElementType(), 0);
        }

        Type genericTypeDefinition = type.GetGenericTypeDefinition();
        
        if (genericTypeDefinition == typeof(IDictionary<,>))
        {
            typeToCreate = typeof(Dictionary<,>).MakeGenericType(type.GetGenericArguments());
        }
        else if (this.genericEnumerableTypes.Contains(genericTypeDefinition))
        {
            typeToCreate = typeof(List<>).MakeGenericType(type.GetGenericArguments());
        }
        else
        {
            return null;
        }

        return Activator.CreateInstance(typeToCreate);
    }
}

Tímto upraveným binderem nahradíme původní MVC binder a to následujícím voláním v metodě Application_Start v Global.asax:

protected void Application_Start()
{
    ModelBinders.Binders.DefaultBinder = new ArrayBinder();
.....
    

Výhodou podobného přístupu je, že se nemusíme spoléhat na programátory view modelu, kteří by měli v konstruktoru nastavovat prázdné kolekce - navíc to není ani nutné  dostaneme prázdnou kolekci i v případě, že parametr ActionMetody je přímo kolekce.

Pomocí tohoto nového binderu tedy vlastně zavedeme pravidlo, že kolekce ve view modelu není nikdy null a je tedy buď prázdná a nebo obsahuje nějaké prvky.

Zdrojový kód je dostupný na Codeplex - včetně kódu pro předchozí díly.



Žádné komentáře:

Okomentovat