MVVM: Model-View-ViewModel
21. dubna 2009
Autor: Václav Dajbych
Model-View-ViewModel je návrhový vzor pro WPF aplikace. Nabízí řešení, jak oddělit logiku aplikace od uživatelského rozhraní. Kódu je pak méně, vše je přehlednější a případné změny nejsou implementační noční můrou. MVVM odděluje data, stav aplikace a uživatelské rozhraní. Samotné WPF bylo vytvořeno tak, aby se v něm MVVM používal pohodlně. Proto vše pěkně doplňuje binding a command – náhrada za uživatelské rozhraní řízené událostmi.
Podle mých zkušeností roste složitost třídy s její chytrostí exponenciálně. Proto je výhodnější mít více hloupých tříd, než jednu chytrou. Pokud se přikloníme k dělení kódu do více tříd, nabízí se otázka, jak to provést správně. Jedním řešením je právě Model-View-ViewModel, který představuje vyzkoušené a ověřené řešení. Podle tohoto vzoru je naprogramován například Expression Blend.

Popišme si tedy případ, ve kterém používáme MVVM v Silverlight aplikaci. Máme data v databázi s daty netriviální struktury a můžeme je uživateli zobrazovat několika způsoby, nějak je třídit, nebo přepínat mezi kategoriemi. A ještě k tomu jsou data celkem veliká a chceme šetřit síť, takže se rozhodneme mít na straně klienta proxi. Rozeberme si tedy, jak putují data z databáze až do grafického rozhraní. MVVM sice nic neříká o řešení na straně serveru, přijde mi ale vhodné pro úplnost uvést nějaké řešení i na serverové straně.
Databáze poskytuje svá data pomocí uložených procedur. Uložené procedury krásně obalí strukturu databáze rozhraním. Aplikace, která přistupuje k databázi jen přes uložené procedury, tedy nemusí nic vědět o struktuře tabulek. Změny v databázové struktuře se tím pádem obejdou bez úprav aplikace, která databázi využívá (nevyžadují-li úpravy i změny v rozhraní, i tak ale budou změny menší).
Pomocí LINQ se tyto procedury volají z jazyka C# pohodlně a jednoduše. Získaná data se jen přebalí do transportních tříd a nabídnou jako webová služba (v našem případě Silverlight-enabled WCF Service). Proč přebalovat do transportních tříd? Inu, webovými službami neprotlačíme jen tak nějakou třídu. Ty protlačitelné (čti serializovatelné) se označují jako transportní a jsou celkem specifické. Tak jako je funkce webové služby označena atributem OperationContract, musí být transportní třída označena atributem DataContract. Jednotlivé vlastnosti třídy musí mít atribut DataMember a musí mít veřejný jak getter tak i setter. U typu Enum musí mít jednotlivé výčty atribut EnumMember. A konečně transportní třída musí mít veřejný konstruktor bez parametrů. Tyto transportní třídy je dobré mít v samostatném projektu Silverlight Class Library, na kterou má referenci jak Silverlight projekt, tak i projekt webové služby poskytující.
Není nutné dělit Silverlight aplikaci mezi více projektů, je ale dobrý nápad si oddělit vlastní aplikaci Client od datových struktur Client.Data. Client obsahuje vrstvu View, Client.Data pak vrstvy ViewModel a Model. Pojďme si jednotlivé vrstvy popsat.

Model obsahuje referenci na zdroj dat, v našem případě na webovou službu. Pokud obsahuje reference na více služeb z jednoho logického kontextu, nabízí celý kontext z více služeb v rámci jednoho celku, čili sebe. Naopak může být více modelů vedle sebe pro různé logické kontexty čerpající z jedné webové služby. Pokud se na straně klienta používá datová proxi, implementuje se do této vrstvy.
View zastupuje grafické rozhraní v jazyce XAML s troškou nezbytného kódu C# na pozadí, který především provádí vytvoření nové ViewModel třídy či tříd, za kterých jednotlivé formulářové prvky čerpají svůj obsah. Binding skrz ObservableCollection umožní automatickou změnu obsahu ovládacího prvku při změně obsahu této datové struktury. A naopak, změní-li uživatel obsah ovládacího prvku, projeví se to i v datové struktuře. Nově přidané či odebrané prvky jsou rychle k nalezení v obsluze události, kterou tato změna vyvolá. Je samozřejmě možné využívat i jiné datové struktury s vlastními ovládacími prvky. Při používání tříd pro binding se v XAML kódu jen jednoduše deklaruje, která vlastnost třídy se má do obsahu ovládacího prvku vkládat. Je tedy možné velice pohodlně čerpat z více ovládacích prvků různé vlastnosti jedné třídy. Pokud třída implementuje rozhraní INotifyCollectionChanged a INotifyPropertyChanged, projeví se změny na obou stranách (uživatelského rozhraní a datové struktury) automaticky.
ViewModel spojuje Model a View. K Modelu se neváže přímo, ale přes rozhraní, které Model implementuje. Konkrétní Model se předá v konstruktoru. Je dovoleno mít i bezparametrický konstruktor, který vytvoří výchozí instanci Modelu. Ovládací prvky provádějí binding z této třídy. V této třídě se provádí filtrování dat v závislosti na stavu ovládacích prvků. Model nesmí o stavu ovládacích prvků nic vědět.
Rozeberme si teď, kde jdou dělat řezy aplikací a jak snadno. Rozhodně se nám bude lépe vyměňovat databází za jinou, přistupujeme-li k ní jen přes uložené procedury. Přirozený řez je mezi webovými službami na ISS a Modelem v Silverlightu. Důležitý řez je mezi Modelem a ViewModelem. Ke grafickému rozhraní můžeme připojit libovolný (testovací) Model (vytvoříme třídu ViewModel s jinou instancí třídy Model), který může obsahovat například testovací data uložená v XML podobně na místním úložišti. Stejně tak můžeme vytvořit testovací grafické rozhraní využívající stejnou třídu Model. Mezi XAML a logickým kódem je přirozené oddělení, aby bylo možné vyvíjet grafické rozhraní nezávisle na logickém kódu aplikace.
Silverlight nevyžaduje programování jednotného masivního špatně udržovatelného kódu. Pro složitější úlohu je rozumné vytvoření vrstev, kde je každá vrstva zodpovědná za nějakou práci. S tímto přístupem je snadnější aplikaci udržovat, rozšiřovat, testovat i nasazovat.