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.