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.

analysis-sw-architecture
Posted by: Jarda Jirava
Last revised: 10 Apr, 2009 01:00 PM History

Comments

10 Apr, 2009 04:45 PM @ version 0

Myslím, že nejaké takové rešení je opravdu blízko nejakému finálnímu best-practice. Sám jsem k velice podobnému proiteroval :) Moje rešení (z vetší cásti neimplementované, pouze v hlave) se liší v tom, že do ModelView predám celý ControllerContext (jeho soucástí je TempData i ViewData, tedy i ModelState) a o bindování se postará samotný ViewModel - to pro prípad, že ViewModel muže obsahovat více Modelu. Ale urcite bych se striktne držel PRG schématu - tedy redirectovat i po neúspešném zvalidování modelu.

11 Apr, 2009 01:17 AM @ version 0

Osobne se mi nechtelo "zatahovat" do ViewModelu ControllerContext a to z toho duvodu, že bych jej príliš svázal s konkrétním prostredím - prezentacní vrstvou. Obdobne pak bindování je zajišteno externe. V tom jsem vycházel z toho, co nabízí práve WPF, kde se také není treba starat o binding dat, byt malou úlitbou je práve ono jednorázové predání dat pres 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 prípade potreby mohl vzít, doplnit rozhraní INotifyPropertyChanged a nasadit práve ve WPF aplikaci.

Petr
Petr
13 Apr, 2009 12:53 PM @ version 0

Jardo, nekdo tu chtel ukazovy priklad a myslim ze by kopnul i me. Nemohl by jsi sem najakou kompletni ukazku dat?

FilipK
FilipK
16 Jun, 2009 01:28 AM @ version 0

@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
Steve
31 May, 2009 02:59 PM @ version 0

Sám se poslední dobou taky trochu zabývám hledáním neceho jako MVC best-practice a narazil jsem na dost nesrovnalostí ohledne Modelu nebo chcete-li ViewModelu nebo naprosto presne receno tech tríd, co se uvádejí v deklaraci generických view. Z ruzných clánku a príkladu mi prijde, že nemalá cást programátoru tento "model" vnímá spíše jako prepravku, ve které pošle data do view. Viz clánek od Scotta Guthrieho, kde v cásti o generických view uvádí ukázku trídy ProductListViewData, která nemá nic spolecného s necím jako aplikacní logika. S podobným zpusobem použití se mužeme setkat i u aplikace MVC-Storefront od Roba Coneryho. Dále jsem mel možnost si poslechnout prednáku TTD by Google vedenou odborníkem od Googlu, který v jeho ukázkové desktopové aplikaci v jave používal model ciste jako "appliaction state".

Podle GoF by model mel být "the application object". Rudolf Pecinovský v knize Návrhové vzory uvádí príklad, kde model je opravdu telo aplikace, trída která reší celou aplikacní logiku, ale treba na wikipedii se mužeme docíst že "the model represents the information (the data) of the application".

Mne osobne se více líbí pojetí "application state" tzn tupé prepravky na data, které nic víc neumí, je jedno jestli se takovým objektu ríká model nebo viewdata a modelem je pak rozumena celá bussines vrstva, která se v controlleru volá tak, aby správne naplnila viewdata, ale je dosti nešikovné, že když nekdo rekne model, tak není úplne 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/

Your Comments

Used for your gravatar. Not required. Will not be public.
Posting code? Indent it by four spaces to make it look nice. Learn more about Markdown.

Preview