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; }
}
{
public IEnumerable<Person> Members { get; set; }
}
a tu využíváme v ActionMetodě pro vstupní parametr:
public ActionResult Family(Family family)
{
return View(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"
}
}
{
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);
}
{
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);
}
}
{
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();
.....
{
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.
Zdrojový kód je dostupný na Codeplex - včetně kódu pro předchozí díly.
Žádné komentáře:
Okomentovat