Jak vypadá ViewModel v asp.net MVC
Při psaní úvodního článku, ve kterém jsem se věnoval postřehům při vývoji asp.net MVC aplikace jsem si ani nepomyslel, že vydám i druhý článek, který bude popisovat Model-View-ViewModel upravený vzor pro takovou aplikaci. A už vůbec to, že bych se rozhodl ke psaní článku třetího o tom, jak vlastně taková třída ViewModel v mém podání vypadá.
Okolnosti tomu však chtěli a tak jsem zde s dalším pokračováním na rozpracované téma. Tentokrát se pokusím osvětlit, jak vypadá třída ViewModel v mém podání zasazená do asp.net MVC aplikace.
ViewModel
Jak jsem již popsal, třída ViewModel zprostředkovává data a komunikuje se sevisními objekty aplikace a zároveň poskytuje data aplikace pro View. Zároveň pak upravuje tato data, aby s nimi ve View byla snazší práce a View mohlo být velice jednoduché a nevykonávalo žádnou logiku. Ještě neopomenu zmínit jednu věc, že k vytvoření konkrétní instance ViewModel používám IoC/DI container, tudíž servisní objekty jsou injektovány a o jejich existenci tak nemusí mít Controller ponětí.
Zkusím projít popisem tak, jak většinou prochází takový normální požadavek na získání informací a jejich editaci a to pro jeden datový objekt.
Akce Controlleru vypadá nějak takto
public ActionResult Update(int id) { var model = Container.Resolve<ProductDetailViewModel>(); model.Load(id); return View(model); }
Odpovídající třída ViewModelu pak nějak takto
public class ProductDetailViewModel { private readonly IProductService _productService; public ProductDetailViewModel(IProductService productService) { _productService = productService; } public void Load(int id) { Product = _productService.Get(id); } public ProductEntity Product { get; private set; } public bool IsProductLoaded { get { return Product != null; } } }
Pro jednoduchost jsem momentálně odstranil další potřebné servisní objekty o kterých jsem se zmiňoval již dříve. Například providera na TempDataDictionary. Zároveň je vidět, že ProductDetailViewModel obsahuje i další vlastnosti, které jsou použity ve View a odpadá nám tak nutnost testování a rozhodování se na úrovni View.
Nyní přejdu k akci, kdy uživatel žádá o aktualizaci takto poskytnutého záznamu, který zaktualizoval.
[AcceptVerbs(HttpVerbs.Post)] public ActionResult Update(int id, FormCollection formCollection) { var model = Container.Resolve<ProductDetailViewModel>(); model.Load(id); if (TryUpdateModel(model.Product, "Product", null, new string[] {"Id"})) { if (ModelState.IsValid) { model.Save(); return RedirectToAction("Get", new { id = id}); } } return View(model); }
Jak je vidět, téměř nic se nezměnilo, jen přibyla metoda Save() na našem ViewModelu, která zpropaguje požadavek na servisní vrstvu. Celé by to šlo samozřejmě ještě upravit tak, že by metoda Save vracela výjimku v případě, že by se nepodařilo záznam uložit a došlo by k znovupožadavku na editaci. Je však na samotné logice aplikace, jak se s takovou chybou vypořádá a zda nabídne uživateli opět možnost editace a odstranění problému, nebo jej přesune na jinou stránku.
Důležitou součástí celého procesu se tak stává ModelBinder, který může zároveň provést validaci vstupních dat oproti business pravidlům.
Spolupráce s ModelBinder
Výše uvedené je celkem pěkný postup, ale stále zde je ještě spousta kroků, které se mohou přesunout, abychom se mohli soustředit jen na samotné akce a pokud možno se co nejvíce přiblížili k pouhému vyvolávání metod na ViewModelem, tak jako se vyvolávají události ve WPF aplikaci při použití vzoru MVVM.
K tomu však potřebujeme maličko lepší spolupráci ModelBinderu, než která nám je nabízena prostřednictvím výchozího DefaultModelBinderu. Hlavní úlohou námi definovaného ModelBinderu je vytvoření instance ViewModelu a validace vstupních dat. Samozřejmě si zde můžeme připsat i další logiku bindování na data.
Takový ModelBinder pro náš ViewModel k detailu produktů může vypadat následovně:
public class ProductDetailViewModelBinder: DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { bindingContext.Model = Container.Resolve<ProductDetailViewModel>(); var id = int.Parse(bindingContext.ValueProvider["Product.Id"].AttemptedValue); bindingContext.Model.Load(id); return base.BindModel(controllerContext, bindingContext); } }Samozřejmě opět odhlížím od kontrol, které by bylo potřeba doplnit. Zároveň by bylo vhodné doplnit validaci nabindovaných dat oproti business pravidlům, což je pro zjednodušení vynecháno.
Akce v příslušném Controlleru se nám tedy rázem zjednoduší a její implementace bude následující:
[AcceptVerbs(HttpVerbs.Post)] public ActionResult Update([Bind]ProductDetailViewModel model) { if (ModelState.IsValid) { model.Save(); return RedirectToAction("Get", new { id = model.Product.Id}); } return View(model); }
Což už je myslím akceptovatelný stav. Samozřejmě bude záležet na okolnostech a celkovém chápání aplikace. Neboť tento případ skrývá před vývojářem samotné naplnění modelu daty třídy Product, což je na druhou stranu obdobný případ, jaký nastává ve WPF aplikaci, kdy nedochází ke ztrátě stavu a pracujeme již s existující instancí naplněnou daty.
Před cílovou rovinkou
Už je mi celkem jasné, že jsem se nedostal ani před cílovou rovinku a vyvstaly další otázky. Třeba jak řeším právě předávání dočasných dat v TempDataDictionary a jak dochází k jejímu injektování do ViewModelu. Takže u tří článků určitě nezůstane. V příštím článku se tak pokusím soustředit se na tuto oblast a případně zodpovědět vložené dotazy.
5 Comments
Michal Augustýn said
Myslím, že nějaké takové řešení je opravdu blízko nějakému finálnímu best-practice. Sám jsem k velice podobnému proiteroval :)
Moje řešení (z větší části neimplementované, pouze v hlavě) se liší v tom, že do ModelView předám celý ControllerContext (jeho součástí je TempData i ViewData, tedy i ModelState) a o bindování se postará samotný ViewModel - to pro případ, že ViewModel může obsahovat více Modelů.
Ale určitě bych se striktně držel PRG schématu - tedy redirectovat i po neúspěšném zvalidování modelu.
Jarda Jirava said
Osobně se mi nechtělo "zatahovat" do ViewModelu ControllerContext a to z toho důvodu, že bych jej příliš svázal s konkrétním prostředím - prezentační vrstvou. Obdobně pak bindování je zajištěno externě. V tom jsem vycházel z toho, co nabízí právě WPF, kde se také není třeba starat o binding dat, byť malou úlitbou je právě ono jednorázové předání dat přes TempDataDictionary, i když i v tomto ohledu jsem se snažil poskytnout jen jakýsi provider, a nikoliv samotnou Dictionary. ViewModel se snažím držet tak izolovaný, abych jej v případě potřeby mohl vzít, doplnit rozhraní INotifyPropertyChanged a nasadit právě ve WPF aplikaci.
Petr said
Jardo, nekdo tu chtel ukazovy priklad a myslim ze by kopnul i me. Nemohl by jsi sem najakou kompletni ukazku dat?
FilipK said
@Steve - celkem souhlasim, taky mi prijde lepsi pouzivat ViewModel jako tupou prepravku dat a service volat z controlleru. Takhle dokonce teoreticky muze nekdo z View volat Model.Load(123) apod nepeknosti..
Co mne ale teda vylozene bije do oci je to opakovane volani Container.Resolve<X>() - to neni zrovna IoC/DI best practice afaik :)
Steve said
Sám se poslední dobou taky trochu zabývám hledáním něčeho jako MVC best-practice a narazil jsem na dost nesrovnalostí ohledně Modelu nebo chcete-li ViewModelu nebo naprosto přesně řečeno těch tříd, co se uvádějí v deklaraci generických view. Z různých článků a příkladů mi přijde, že nemalá část programátorů tento "model" vnímá spíše jako přepravku, ve které pošle data do view. Viz článek od Scotta Guthrieho, kde v části o generických view uvádí ukázku třídy ProductListViewData, která nemá nic společného s něčím jako aplikační logika. S podobným způsobem použití se můžeme setkat i u aplikace MVC-Storefront od Roba Coneryho. Dále jsem měl možnost si poslechnout přednáku TTD by Google vedenou odborníkem od Googlu, který v jeho ukázkové desktopové aplikaci v javě používal model čistě jako "appliaction state".
Podle GoF by model měl být "the application object". Rudolf Pecinovský v knize Návrhové vzory uvádí příklad, kde model je opravdu tělo aplikace, třída která řeší celou aplikační logiku, ale třeba na wikipedii se můžeme dočíst že "the model represents the information (the data) of the application".
Mně osobně se více líbí pojetí "application state" tzn tupé přepravky na data, které nic víc neumí, je jedno jestli se takovým objektů říká model nebo viewdata a modelem je pak rozuměna celá bussines vrstva, která se v controlleru volá tak, aby správně naplnila viewdata, ale je dosti nešikovné, že když někdo řekne model, tak není úplně jasné který tím myslí :).
ScottGu - http://weblogs.asp.net/scottgu/archive/2007/12/06/asp-net-mvc-framework-part-3-passing-viewdata-from-controllers-to-views.aspx
MVC-Storefront - http://blog.wekeroad.com/