Főoldal » MVVM

MVVM

MEGOSZTÁS

Ha tetszett a cikk, akkor nyugodtan oszd meg ismerőseiddel, valószínű ők is örülni fognak neki.

MVVM-ről szóló cikkek.

Forrás: Janka János blog bejegyzése
(http://jankajanos.wordpress.com/2009/05/27/wpf-model-view-viewmodel-bevezeto/)

A Microsoft azért adta ki az MVVM Toolkit-et, hogy egy egyszerűbb és jobban áttekinthető megoldást nyújtson minden WPF fejlesztő számára. Ez annyira igaz, hogy elvileg része lesz az eljövendő RTM verziónak, illetve a WPF 4.0 parancsrendszerét is megreformálják pont emiatt. Voltak páran akik megkértek mostanában, hogy meséljek erről az MVVM-ről néhány szót. Mivel ez a téma elég szerteágazó és kisezer megközelítés létezik, hozzávetőlegesen megpróbálom bemutatni a Microsoft-os WPF MVVM elképzelést, mely pont az egyszerűséget és könnyen érthetőséget hivatott megcélozni.
Először is a lényege az MVVM-nek, akárcsak az ősének az MVC-nek, hogy szeparálja a kód logikát a felhasználói felülettől. Egy jól tervezett alkalmazás ismérve a könnyű fejleszthetőség, tesztelhetőség, illetve fenntarthatóság.

Model

A model definiálja az adatot, ami használt az alkalmazásban. Ezek általában implementálják az INotifyPropertyChanged interfészt. Fontos, hogy ez a réteg ha lehet ne tartalmazzon WPF specifikus kódot (mint pl. ObservableCollection), hogy újrahasználható legyen más típusú projektekben is. A model szerepe többek között az alatta lévő adatforrás logikailag egybefüggő részeinek ábrázolása/egyszerűsítése. Miért fontos ez az utóbbi mondat? Azért, mert nem keverendő össze az adathozzáférési réteggel. Illetve ez is olyan, hogy mikor igen, mikor nem, teljesen megvalósítás függő, de általában a tiszta megoldás az, amikor a model becsomagolja ezeket az adatforrás osztályokat, tehát pl. egy Entity Framework model esetében lehetőség van a Country, StateProvince, Settlement, Address entitásokat egyként, például LocalityData ábrázolni a modelben és a szükséges tulajdonságokat kitenni. Ugyanígy nemcsak a tényleges adat kerül itt ábrázolásra, hanem az alapvető funkciók is, mint pl. egy MessengerData osztály Connect(), Disconnect() metódusa is. A model osztályok sose kommunikálnak a ViewModellel, illetve a View-al, mindent események formájában tesznek ki, amikre a ViewModel objektumok feliratkozhatnak.

View

A View definiálja a felhasználói felületet. Amikor lehet ez legyen írva mindig tisztán XAML markupban. A WPF ugyan támogatja az eseményeket a code-behind fájlokban (akárcsak a WinForms és VCL), de az állapotadatok (mint pl. a kiválasztott tételek, aktuális tétel, vagy akár direkt szabályozható WindowState, stb.) és üzleti logika semmikép ne legyen ide írva. Ezeket a ViewModel-ben kell tartani.

ViewModel

A ViewModel absztraktálja amit a View reprezentál. A ViewModel tárolja a vizuális állapotokat (mint pl. a kiválasztott felhasználókat: ObservableCollection SelectedUsers) és kitesz action-öket, amik végrejtottak a UI-ban WPF parancsok által. A View rögzít egy referenciát a ViewModel-re és használja a meghatározott ViewModel-t a WPF adatkötési lehetőségei által. Ez utóbbi általában a WPF FrameworkElement származékok DataContext tulajdonságán keresztül valósul meg. A FrameworkElement pedig közös őse minden WPF vezérlőnek.
A sorrend nagyon fontos !
A View tudatában van a ViewModel-nek, de a ViewModel nem tud semmit a View-ról. Ugyanígy a ViewModel tudatában van a Model-nek, de a model semmit sem tud a ViewModel-ről.
 
Megjegyzendő:
Vannak példák, amikor csak három réteget használnak, pl. az Entity Frameworkot mint Model-t, s nem mint DAL-t (Data Access Layer) használják. Az én véleményem erről az, hogy szükséges a plusz model réteg, mert ott logikai ábrázolás, illetve validációs logika implementálás is történik. Ez utóbbira is vannak ellenpéldák, amikor a ViewModel-be implementálják a validációt, de látni fogjuk, hogy sokminden javarészt feladattól függ.

Haladjunk sorjában:

Először is készítünk egy adatbázist. Ez egy egyszerű kis adatbázis, mindössze 1 táblát tartalmaz (Products) néhány mezővel, mint Name, Description, Price. Néhány tesztadatot érdemes felvenni bele.

DAL

Most jön az adatelérési réteg (Data Access Layer). Ide kerül az Entity Framework kontextus és minden alapvető adatelérési funkció, illetve a compiled query-k is. Jelen esetben a ProductProvider osztály lesz az, ami szolgáltatja nekünk az adatokat. Ennek az osztálynak van egy LoadProducts(), AddProduct(), DeleteProduct(), SaveProducts() metódusa. Tulajdonképpen a lényeg az, hogy egy kontextus van a termékek manageléséhez. Ezt lehet természetesen generikusabbá tenni, de így sokkal könnyebb megérteni elsőre azok számára is, akik most először találkoznak a témával.

Model

Jön a model réteg. Itt készítünk egy ProductData osztályt, ami a meglévő EF Product entitás osztályunkat fogja becsomagolni, illetve kibővíti azt más tulajdonságokkal. Ebbe a rétegbe visszük le a validációs logikát is. Minden adatot reprezentáló model osztály implementálja az INotifyPropertyChanged, illetve IDataErrorInfo interfészeket. Jelen esetben én ezt leegyszerűsítettem egy ObservableObject és ValidableObject segítségével.

Ugyanitt kerül implementálásra a ProductBook osztály. Ez reprezentál egy terméklista manager osztályt. Ő már maga tárolja a becsomagolt termék objektumokat, de semmilyen állapotadatot nem tárol, mint pl. ki van kijelölve. Direkt NotificationCollection-be vannak téve a termék adatok, hogy ne függjön a kód a WPF-től. Ebbe az osztályba kerülnek megvalósításra az alapvető műveletek is, mint pl. új termékadat felvétele, egy termékadat törlése, illetve a módosítások mentése.

ViewModel

Itt alapvetően két darab ViewModel osztály létezik, az egyik a MainViewModel, a másik pedig a ProductBookViewModel. Készíthetnénk külön ViewModel osztályt magának a ProductData-nak is, de jelen esetben ez szükségtelen. Az annyit tenne mindössze, hogy magát a ProductData-t is becsomagolnánk egy ProductDataViewModel osztályba és ezt használnánk a ProductBookViewModel-ben, de ez nem szükséges, mivel a validáció a modelben van és nincs szükség bővíteni vizuális állapotinformációkkal ezt. Mint láthatjuk a kódban, a ProductBookViewModel konstruktora példányosítja a model (ProductBook) manager osztályt és feliratkozik annak a kollekciójának CollectionChanged eseményére, hogy diszpécselje a változásokat a model listájából a ViewModel ObservableCollection-be. Tehát magyarul szinkronban tartjuk a modellünk kollekcióját a view modellünk kollekciójával, ami már egy WPF specifikus kollekció (ObservableCollection), ugyanis ezt fogjuk bekötni a view objektumaink lista vezérlőibe, pontosabban ezeknek egy nézetét (CollectionViewSource).

Tehát most ott tartunk, hogy a ViewModel-ünknek van egy példánya a hozzá tartozó modelből: röviden a ProductBookViewModel objektumunk ismeri a ProductBook model objektumot.

Következő lépésben minden commandot kipakolunk ide. Azt, hogy épp ki van kijelölve a SelectedProductData tulajdonság határozza meg a ViewModel-be. Tehát a törlés parancsunk például így néz ki:

private void DeleteProductData()
{           
    ProductBook.DeleteProductData((ProductData)SelectedProductData);           

View

Ha megnézzük a ProductBookView.xaml fájlt máris megértünk mindent. Látható, hogy a lista control be van kötve a view model osztályunk megfelelő tulajdonságaiba. A kötésben az adatforrás ilyenkor a legközelebbi ráeső DataContext, mely jelen esetben a UserControl-unk DataContext-je lesz, ami ugye nem más lesz, mint egy ViewModel objektum.

              SelectedItem=”{Binding SelectedProductData, Mode=TwoWay}”
              ItemTemplate=”{StaticResource ProductDataTemplate}”
              SelectionMode=”Single” IsSynchronizedWithCurrentItem=”True”/>

Mivel a SelectedItem = ViewModel.SelectedProductData és TwoWay módú a kötés, automatikusan szinkronba fog maradni a két tulajdonság a view és viewmodel között. Igen ám, de honnan a frászból tudja a view, jelen esetben a user controlunk, hogy melyik ViewModel objektumba kell kötni? Azaz honnan tudja, hogy melyik ViewModel az adatforrás, a DataContext, stb.? Na ezt szabályozza a konverterünk (LogicTypeInstanceConverter). Fontos, a View-okat nem használhatjuk a ViewModel osztályokba! Emlékezzünk, a sorrend fontos! Csináltam pontosan ennek illusztrálására mégegy View-ot (WelcomePageView.xaml) és a konverterünk automatikusan egy enum értékből (public enum LogicType { WelcomePage, Products }) fogja eldönteni, hogy milyen View/ViewModel logikai párost kell visszaadni, amit a MainView.xaml egy ContentPresenter kontrolja fog megjeleníteni:

     Margin=”5″ Content=”{Binding LogicType,
           Converter={converters:LogicTypeInstanceConverter}}”/>

Tehát én egy LogicType enum értékhez kötök, a konverterem pedig a cache-ből előkotorja nekem a megfelelő View + ViewModel párost, illetve ha nem léteznek, akkor automatikusan példányosítani is fogja őket. Ez itt a lényeg, ez párosítja össze őket, de bárhogy kombinálhatnánk ezeket további View és ViewModellekkel, ha lennének:

_logics = new Dictionary();

// View = WelcomePage, ViewModel = NINCS                              
_logics.Add(LogicType.WelcomePage, new LogicTypePair(typeof(WelcomePage), null));

// View = ProductBookView, ViewModel = ProductBookViewModel
_logics.Add(LogicType.Products, new LogicTypePair(typeof(ProductBookView), typeof(ProductBookViewModel)));

Sajnos elég nehéz ezt az egész metodikát szavakba önteni, vázlatosan lehet, de szájbarágósan elég nehéz ezt elmagyarázni, úgyhogy ha bárkinek aki még nem nagyon foglalkozott a témával bármi kérdése van, tegye fel nyugodtan. Mindenesetre próbáltam a kódot elég részletesen kommentezni, hogy világos legyen mi miből jön. Talán úgy a legegyszerűbb megérteni, ha az aljából indulunk ki: DAL <= Model <= ViewModel <= View és egyszerre csak egy rétegre összpontosítunk. Így viszonylag egyszerű lesz végigkövetni az egész folyamatot. Sok sikert!

MEGOSZTÁS

Ha tetszett a cikk, akkor nyugodtan oszd meg ismerőseiddel, valószínű ők is örülni fognak neki.