Lepší bindování kolekcí v ASP.NET MVC

Výchozí model-binder v ASP.NET MVC se nechová v případě kolekcí vždy tak intuitivně, jak bychom předpokládali. Naštěstí máme ale k dispozici dost extension-pointů, kterými můžeme výchozí chování jednoduše upravit tak, jak nám vyhovuje. Rád bych nejprve ukázal přirozenější způsob bindování polí, a dále pak bindování Dictionaries.

Protože od .NET verze 2.0 máme k dispozici generické kolekce, nevystačíme si v našem případě se základní registrací vlastního model-binderu:

System.Web.Mvc.ModelBinders.Binders[typeof(MyType)] = new MyTypeModelBinder(); // implements IModelBinder

Důvodem je to, že takto nemůžeme podchytit otevřené generické typy. K flexibilnější registraci model-binderů slouží rozhraní IModelBinderProvider. Pokud ho implementujeme, tak z metody GetBinder vracíme model-binder pro daný Type. Pokud model-binder pro daný typ vytvořit neumíme, vracíme null. Vlastní model-binder-provider pak zaregistrujeme takto:

System.Web.Mvc.ModelBinderProviders.BinderProviders.Add(new MyModelBinderProvider());

Bindování polí

ASP.NET MVC je samo o sobě schopno ošetřit tři způsoby bindování polí.

První způsob vyžaduje tento typ vstupních dat (bindujeme na Person[] people):

<input type="text" name="people[0].FirstName" value="Michal" />
<input type="text" name="people[0].LastName" value="Novak" />
<input type="text" name="people[1].FirstName" value="Honza" />
<input type="text" name="people[1].LastName" value="Novy" />

Je třeba dodržet vzestupnou nepřerušenou řadu indexů – kdybychom měli ještě people[3].FirstName, tak nabindované pole bude mít stále jen dva prvky.

Druhý způsob bindování je jednodušší (bindujeme na string[] people):

<input type="text" name="people" value="Novak" />
<input type="text" name="people" value="Novy" />

Tento přístup má jednu zásadní výhodu – pokud prvky generujeme dynamicky JavaScriptem, můžeme si odpustit problémy způsobené s udržováním správných indexů.
Je tu ale jedna zřejmá nevýhoda – lze takto bindovat jen typy, které se bindují z jediné hodnoty. Kdybychom totiž měli takováhle data, nešlo by jednoznačně určit, které property patří k jakému prvku (leda spoléhat na pořadí):

<input type="text" name="people.FirstName" value="Michal" />
<input type="text" name="people.LastName" value="Novak" />
<input type="text" name="people.FirstName" value="Honza" />
<input type="text" name="people.LastName" value="Novy" />

Výchozí chování je tedy poměrně rozumné, ale narazil jsem na jednu nevýhodu, s kterou jsem se musel vypořádat. Druhý způsob bindování totiž nepoužívá pro konstrukci instancí prvků pole IModelBinder pro daný typ (tj. např. binder pro string), ale ke konverzi ze stringu na daný typ používá TypeConverter získaný z TypeDescriptoru.
Pokud tak nepracujeme se základními typy, ale třeba vlastní jednoduchou třídou (která je model-bindovatelná z jednoho stringu), nedojde k použití registrovaného IModelBinderu. Máme několik možností, jak toto omezení obejít:

První možností je, že si napíšeme decorator pro IValueProvider, který bude vracet upravený ValueProviderResult s přepsanou metodou ConvertTo, která bude používat model-bindery místo TypeConverteru.

Druhou (a jistě ne poslední) možností je implementovat generický ArrayModelBinder<TElement> (typicky poděděný z DefaultModelBinder), který si vytáhne model-binder pro typ prvku pole (Binders.GetBinder(typeof(TElement))) a prožene přes něj vstupní hodnoty získané nějak takto (chybí checky):

bindingContext.ValueProvider.GetValue(bindingContext.ModelName).RawValue as Array

Pro registraci tohoto generického model-binderu pak využijeme vlastní ArrayModelBinderProvider.

No a ještě tu máme třetí způsob, jak bindovat na pole. Je to způsob, který se hodí v případě, že potřebujeme bindovat na komplexní typ a zároveň nechceme zajišťovat, že budou indexy sekvenční.

<input type="hidden" name="people.Index" value="bla" />
<input type="text" name="people[bla].FirstName" value="Michal" />
<input type="text" name="people[bla].LastName" value="Novak" />
<input type="hidden" name="people.Index" value="ble" />
<input type="text" name="people[ble].FirstName" value="Honza" />
<input type="text" name="people[ble].LastName" value="Novy" />

Fígl je v tom, že Index může být cokoliv. V praxi se tedy hodí použít něco, co je jednoduše získatelné a přitom unikátní – nabízí se tak timestamp, guid apod.

Bindování Dictionaries

Když bindujeme na Dictionary, předpokládají se takováto data (bindujeme na Dictionary<string, PersonData> data):

<input type="hidden" name="data[0].Key" value="novak" />
<input type="text" name="data[0].Value.Age" value="25" />
<input type="text" name="data[0].Value.Location" value="Prague" />
<input type="hidden" name="data[0].Key" value="novy" />
<input type="text" name="data[0].Value.Age" value="26" />
<input type="text" name="data[0].Value.Location" value="Krno" />

Tento způsob bindování je vynikající, protože nám umožňuje použít pro klíče i hodnoty komplexní typy. Pokud bychom ale chtěli intuitivnější bindování pro jednoduché klíče, máme smůlu – toto fungovat nebude:

<input type="text" name="data[novak].Age" value="25" />
<input type="text" name="data[novak].Location" value="Prague" />
<input type="text" name="data[novy].Age" value="26" />
<input type="text" name="data[novy].Location" value="Krno" />

Řešení je ale poměrně jednoduché – stačí napsat si generický DictionaryModelBinder<TKey, TValue>, který budeme vytvářet v DictionaryModelBinderProvideru.
Implementace DictionaryModelBinderu nebude triviální, ale nebude to ani žádná věda. Stačí načíst si ze všech value-providerů klíče, které by nás mohli zajímat (tj. začínající na bindingContext.ModelName + “[“) a z nich si vykousneme klíč uzavřený do hranatých závorek. Pak jen stačí obstarat si IModelBindery pro TKey a TValue, správně je zavolat, a máme hotovo.
Samozřejmě bychom měli zajistit encodování hranatých závorek, aby nám nenadělaly paseku.

Závěr

ASP.NET MVC se chová při model-bindingu velmi rozumně, ale všude je prostor pro vylepšení a ASP.NET MVC se vylepšením vůbec nebrání, jelikož nabízí dostatek míst, kde se můžeme zapojit do hry.
V článku jsem chtěl ukázat cestu jak na to, když chceme bindovat kolekce trošku jiným způsobem, který se může hodit např. u aplikací, ve kterých v JavaScriptu přidáváme/odebíráme položky ve formuláři – pak nemusíme řešit správné indexování, což vyžaduje výchozí model-binder.



Příspěvek byl publikován v rubrice Programování a jeho autorem je Augi. Můžete si jeho odkaz uložit mezi své oblíbené záložky nebo ho sdílet s přáteli.

2 komentáře u „Lepší bindování kolekcí v ASP.NET MVC

Napsat komentář

Vaše emailová adresa nebude zveřejněna.