WPF – Model-View-ViewModel bevezető

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<User> 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 !

MVVM 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.

 

Szemléltetésképp készítettem egy kis termékmenedszer példát (még VS 2008-ban), amin keresztül lépésről lépésre megtudjuk nézni ezt a gondolatmenetet. A kód innen letölthető: MVVM.zip

Mint ahogy a ábrán is látszik az egyes részek elhatárolódása, ugyanúgy szépen látszik ez a kódon is.

Vew > ViewModel > Model > DAL

View < ViewModel < Model < DAL

image

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.

<ListBox ItemsSource="{Binding ProductDataCollectionView, Mode=OneTime}"                
              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:

<ContentPresenter
     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<LogicType, LogicTypePair>();

// 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!

13 thoughts on “WPF – Model-View-ViewModel bevezető

  1. Péter

    Feltettem a WPF Toolkit -et, de ezután is a "Termékek" menüre kattintva a "ProductBook.cs" "foreach (var product in Provider.LoadProducts())" során "TargetInvocationException was unhandled" hibával leáll.

  2. János

    Szerintem valami a connection string körül nem stimmel. Ellenőrizzétek az app.config-ban! Esetleg megtehetitek, hogy attacholjátok az adatbázist SQL Management Studio segítségével, majd eltávolítva az app.config-ból a connection részt megnyitjátok az EF designer-t (MVVM.edmx) és jobb klikk: "update model from database". Ilyenkor felajánlja, hogy beállít újra egy connection-t neki.

  3. Péter

    Megpróbáltam SQL2005 szerveren az attacholást, de mem ment, mert magasabb a verziószáma az adatbázisnak. SQLEXPESS nincs a gépemen.

  4. János

    Azzal is megy, csak akkor hozd létre újra az adatbázist a 2005-el. Az EF dolgozik SQL Server 2005-el is emlékeim szerint. A tábla sémát pedig megtudod nézni, ha megnyitod az .edmx fájlt, sőt, ha VS2010-et használsz, akkor az új EF 4.0 legenerálja az adatbázis létrehozásához szükséges SQL DDL parancsokat is neked a modelből.

  5. János

    Biztos, hogy van, uniqueidentifier a neve. EDMX fájl: <Property Name="UID" Type="uniqueidentifier" Nullable="false" /> <Property Name="Name" Type="nvarchar" Nullable="false" MaxLength="256" /> <Property Name="Description" Type="ntext" /> <Property Name="Price" Type="money" Nullable="false" />

  6. Péter

    Hali!Nem kötözködni akarok, csak megjegyezni egy eléggé fontos dolgot:Mint ahogyan Te is említetted a separated presentation pattern-eknél eléggé nagy hangsúlyt kap a flexibilitás és a tesztelhetőség.Nos ez sajnos ebben az egyszerű kis példában ott sérül, hogy a ProductBookViewModel osztály konstruktorába be van drótozva, hogy milyek modell osztályhoz tartozik a ViewModel. Vagyis így pl.: nem lehet teszteni a ViewModel-t egy mock objekttel. Erre a megoldás a Depedency Injection…Tényleg nem okoskodásnak szántam, csak megjegyeztem, hogy ha azt is beleteszed, akkor megmarad a tesztelhetőség és a flexibilitás is!Üdv.:Petiu.i.: Ja és a validációt általában a Service Layer-be szokás tenni 😀

  7. János

    Igaz. Ebben a példában a modell osztály be van drótozva a viewmodell-be, de ezen a DI segít, mint ahogy említetted is. Tesztet nem írtam hozzá, ezért kerülte el ez a figyelmemet, de nyilván ott és akkor egyből előjött volna ez is. De köszi! Azért vezetem ezt a blogot, hogy közösen megbeszéljük mi a legjobb út, csak általában én szeretem rögtön papírra vetni a gondolataim, anélkül, hogy 20x átgondolnám a nagy tűz és lelkesedés hevében. 🙂

  8. Bencsik Norbert

    Kedves János!

    Van lehetőség arra, hogy az említett mvvm.zip fájl letölthetővé válna? Most kezdek ismerkedni a témával és nagyon hasznosnak találom a bejegyzést.

    Segítséget előre is köszönöm: Bencsik Norbert

    1. jankajanos

      Üdv! Sajnos ezt már nem igen tudom odaadni. Amikor volt egy nagy MS live reform és átköltöztettek mindent a wordpress-re, illetve a skydrive-ot is megreformálták elveszett néhány dolog, illetve én is átrendeztem a skydrive-ot akkoriban. De van erről a NET-en rengeteg írás ma már, átfutva ez körülbelül az, amit én anno csináltam, annyi kivétellel, hogy én nem UserControl, hanem tisztán DataTemplate alapon valósítottam meg minden View-ot a mi saját projektünkbe is. De a többi nagyjából egyezik: http://msdn.microsoft.com/en-us/library/gg405484(v=pandp.40).aspx

Hozzászólás