MVVM v asp.net MVC
V minulém příspěvku, kdy jsem se věnoval mým postřehů z použití asp.net MVC jsem též zmínil možné použití MVVM přístupu a analogii použití k desktopovým aplikacím.
Použití MVVM – Model View ViewModel vzoru
Hlavní motivací pro použití tohoto vzoru pro mě bylo, jak jsem již minule zmínil v odlehčení Controlleru a zjednodušení View, kde jsem nechtěl řešit implementační detaily, především pak samotné vyhodnocování podmínek.
Zároveň s tím jsem se však dostal v prvních krocích do menších komplikací, neboť jsem byl postaven před otázku rozumného vyřešení předávání některých potřebných objektů do ViewModelu (VM) a zároveň jsem si chtěl ponechat volnost a možnost testování VM. Pro lepší přehled zmíním použití jednorázového uložiště dat v podobě TempDataDictionary.
IoC/DI container
Jelikož jsem si velice oblíbil použití IoC/DI containeru konstruoval jsem aplikaci tak, abych tento přístup mohl využít i při tvorbě asp.net MVC aplikace. Jako vhodnou volbou se tak jevilo použití MVCContrib projektu, který nabízel nativní podporu pro všechny rozšířenější implementace. Moji volbou se stal Unity vyvíjený skupinou pattern & practices.
V konečném důsledku mi právě použití DI containeru pomohlo v tom, jak vyřešit právě ono předávání (injectování) objektů do VM. Samozřejmě by to šlo zařídit i jinak, ale přeci jen mi tento způsob přijde elegantnější a pro vývojáře jednodušší – nemusí myslet na to, co musí udělat a co případně musí zavolat.
Když už jsem zmínil právě ono jednorázové uložiště dat (TempDataDictionary), chvíli jsem přemýšlel, jak jej vhodně předat, nakonec mi jako vhodný způsob přišlo použít existující instanci tohoto objektu získaného v okamžiku vytvoření Controlleru a v přepsané třídě poděděné od DefaultControllerFactory jsem tuto instanci vložil do containeru – samozřejmě s platností pouze pro daný web request. Zde tedy bylo nutné ještě napsat si vlastní WebRequestLifetimeManager, neboť Unity ve své výchozí instalaci zná pouze dva LifetimeManagery a samozřejmě ani jeden z nich nepodporuje web requesty :-).
Výhod použití IoC/DI jsem pak také využil i v případě předávání servisních objektů do VM, většinou pak přes constructor injection.
Role Controlleru
Jak jsem již zmínil, snažil jsem se odlehčit Controller, jeho úloha v mém pojetí se tedy redukovala na tyto základní operace:
- vytvoření ViewModelu
- nabindování dat – většinou dat z requestu
- zavolání vhodné metody na ViewModelu – analogicky k desktop aplikaci, vyvolání vhodné události
- propojení ViewModelu na příslušné View
Z předchozí věty by se mohlo zdát, že ViewModel je svázán s konkrétním View a nemůže bez něj koexistovat, což ovšem není pravda a VM je nezávislou jednotkou a může k prezentaci dat využívat několik různých View.
Když jsem se zmínil o vytvoření ViewModelu, je třeba říct, že ani toto nemusí být tak úplně úlohou Controlleru, stejně tak jako nabindování dat, neboť toto může zajistit vhodně napsaný a upravený ModelBinder.
Tím se tedy dostávám k odpovědi na Augiho otázku pod minulým příspěvkem, jakým způsobem řeším bindování dat. Určitě nepoužívám generický ViewModel, ten by bylo možné použít možná u jednoduchých CRUD stránek. Zároveň určitě nepředávám do ViewModelu kolekci FormCollection, neboť bych ViewModel příliš svázal s prostředím a já se snažil jej mít co nejvíce nezávislý – i ono vložení TempDataDictionary je řešeno přes interface poskytující přístup k dictionary.
Tudíž odpověď je následující. U stavových stránek je za vytvoření správného ViewModelu v akci zodpovědná daná akce, která též vybírá View. Vytvoření se pak děje přes resolving instance IoC/DI containeru. V případě, že se jedná o request mající za úkol vykonat příkaz, potom jsou dvě možnosti. Nechat ViewModel vytvořit upraveným ModelBinderem a bindování dat ponechat v jeho režii, případně postupovat obdobně jako ve stavové akci a binding ponechat na metodě UpdateModel/TryUpdateModel.
Osobně jsem volil spíše druhý přístup, ale to mělo spíše jiný důvod, ke kterému se přiznám a rovnou se i vymluvím :-) Funkce ModelBinderu mi byla delší dobu malou neznámou a převod dat jsem řešil spíše přes ValueProvidery, toto řešení však ve verzi 1.0 bylo zavrženo a já se nakonec stejně musel použití ModelBinderu naučit. A samozřejmě i ověřit, že tento přístup s použitím upraveného MVVM je stále funkční a vytvoření VM je možné i v ModelBinderu.
Těsně před koncem?
Tím se pomaličku dostávám ke konci tohoto příspěvku. Vím, že jsem neodpověděl na všechny otázky, které vyvstaly, ale toto určitě není poslední příspěvek na toto téma. Jistý podnět mám i ke komentářům v předchozím článku, které nabízeli možné řešení plugin modelu v asp.net MVC aplikaci. Toto řešení se mi nezdá úplně optimální a mám k němu jisté výhrady a určitě by se dala nalézt lepší implementace při použítí mnou navrženého modelu.
Diskuze
Jak jsem též v komentáři uvedl, budu rád, pokud se ozvete, ať už pomocí komentáře pod článkem nebo přes kontaktní stránku a projevíte zájem o možnou osobní diskuzi nad tvorbou MVC aplikací (nemusí se jednat jen o asp.net MVC implementaci). Každý podnět uvítám a určitě se najde prostor k zorganizování takové diskuze, kde může dojít ke sdělení a výměně poznatků.
4 Comments
Michal Augustýn said
Pěkné pěkné. Dokážu si představit, jak to pěkně funguje...jen jedna věc mi není jasná (že by námět na další článek? ;)) a to ta, jak vypadá ViewModel. Pro účely zobrazení mi přijde velmi vhodné, aby dával k dispozici properties, které půjdou jednoduše přečíst ve view. Při vykonání příkazu je pak potřeba totéž, abysme mohli použít DefaultModelBinder (nebo používáš nějaký vlastní?). Obsahuje tedy Tvůj ViewModel properties, které v getterech a setterech pouze delegují volání na Modely?
Možná by to šlo udělat trošku jinak - gettery nechat, ale místo setterů udělat metodu pro update, které by se jako parametr předal nástupce IValueProvidera - IDictionary<string, ValueProviderResult>. No, možná by se mu mohl předat dokonce celý ControllerContext...
Michal Augustýn said
Jinak o diskuzi bych určitě zájem měl. Teď jsem právě v takové fázi, že ještě neportuji z Preview5 ani Bety na Release verzi, protože bych chtěl během portu udělat takový refactor, aby výsledný kód používal "best-practices". Takže teď jen víceméně sbírám informace...
Jarda Jirava said
Hned včera když jsem dopisoval tento článek mi bylo jasné, že bude muset mít pokračování a to právě ve vysvětlení toho, jak vlastně vypadá samotný ViewModel a jak se s ním pracuje ve View. Takže místo fotbálku budu sepisovat a možná dojde i na ty zdrojáky (to neslibuju).
Daniel Kolman said
Osobně bych doporučil zůstat u názvu MVC. MVVM je návrhový vzor, který se v poslední době objevil pro WPF a je možné ho použít pouze na platformě s dobře udělaným databindingem, což web asi nikdy nebude. Mám pro to dva důvody:
1. V tvém řešení se stále používá controller, takže korektní by bylo používat "MVCVM". Na platformě s dobrým databindingem controller odpadá, je nahrazen reakcemi ViewModelu na změnu dat (databindingem) a dokonce i příkazy jsou vkládané do UI přes databinding (např. navázání tlačítka na ICommand poskytnutý jako property ViewModelu).
2. To písmenko "M" v MVC nemusí nutně znamenat doménový model. V tvém přístupu je právě ViewModel modelem pro view, tudíž zastupuje ono "M". Ať už ho nazveme jakkoliv, stále se jedná o MVC, nikoliv o MVVM.
Rozlišit doménový model (DataModel, DomainModel...) a prezentační model (PresentationModel, DTO, Screen Bound Objects, ViewModel) je samozřejmě dobrá věc, která zpřehledňuje architekturu a separuje View od možnosti cokoliv páchat s doménovým modelem. Možná by v tomto případě bylo lepší místo termínu ViewModel použít jiný název, aby nás to nesvádělo k analogiím s MVVM. Osobně bych se přimlouval k PresentationModel, to prosazoval už ten fousatej chlap: http://martinfowler.com/eaaDev/PresentationModel.html