János's profileJános JankaPhotosBlogListsMore Tools Help

Blog


    May 27

    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!

    May 19

    WPF konverterek

    Készítettem egy gyors módszert a konverterek használatához. Először is, ugye ahhoz, hogy konvertereket tudjunk használni mindig fel kell vennünk őket az erőforrások közé, majd StaticResource segítségével bekötni őket. Ezzel csak az a probléma, hogy egy idő után fárasztóvá válik a dolog. Tehát, az első akadályozó tényezőre a megoldás XAML markup extension-ök használata. Csinálunk először is egy generikus bázis osztályt, ami az összes konverterünk őse lesz markup támogatással:

    using System;
    using System.Globalization;
    using System.Windows.Markup;
    using System.Windows.Data;
    
    namespace VVMF.Core.WPF.Converters
    {
        [MarkupExtensionReturnType(typeof(IValueConverter))]
        public abstract class ConverterMarkupExtension<T> : MarkupExtension, IValueConverter
            where T: class, IValueConverter, new()
        {
            private static T _converter = null;
    
            public override object ProvideValue(IServiceProvider serviceProvider)
            {
                if (_converter == null)
                    _converter = new T();
    
                return _converter;
            }
    
            #region IValueConverter Members
    
            public abstract object Convert(object value, Type targetType,
                object parameter, CultureInfo culture);
    
            public abstract object ConvertBack(object value, Type targetType,
                object parameter, CultureInfo culture);
    
            #endregion
        }
    }

    Ezt követően azért, hogy az értéktípusokat/nullázható típusokat is könnyedén tudjuk kezelni, készítettem egy egyszerű kis segédosztályt:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace VVMF.Core.WPF.Converters
    {
        public static class ConverterHelper        
        {
            public static TValueType GetValue<TValueType>(
                object obj, TValueType defaultValue) where TValueType : struct
            {
                TValueType value = defaultValue; 
    
                if (obj is TValueType)
                {
                    value = (TValueType)obj;
                }
                else if (obj is TValueType?)
                {
                    var nullable = (TValueType?)obj;
                    value = nullable ?? defaultValue;
                }
    
                return value;
            }
    
            public static TValueType GetValue<TValueType>(
                object obj) where TValueType : struct
            {
                return GetValue<TValueType>(obj, default(TValueType));
            }
        }
    }
    

    Majd tesztnek készítünk egy egyszerű kis szám inkrementáló konvertert:

    using System;
    using System.Windows.Data;
    using System.Globalization;
    
    namespace VVMF.Core.WPF.Converters
    {
        [ValueConversion(typeof(int), typeof(int))]
        public class IntegerIncrementerConverter :
            ConverterMarkupExtension<IntegerIncrementerConverter>
        {
            public override object Convert(object value, Type targetType,
                object parameter, CultureInfo culture)
            {            
                return ConverterHelper.GetValue<int>(value) +
                  ConverterHelper.GetValue<int>(parameter, 1);
            }
    
            public override object ConvertBack(object value, Type targetType,
                object parameter, CultureInfo culture)
            {
                return ConverterHelper.GetValue<int>(value) -
                  ConverterHelper.GetValue<int>(parameter, 1);
            }
        }
    }
    

    Ezután már XAML-be mindössze ennyit kell írnunk:

    <TextBlock Grid.Column="0" Text="{Binding SelectedIndex,
      Converter={coreconverters:IntegerIncrementerConverter}, 
    ElementName=dgOrganizations}" Margin="5"/>

    Ehelyett:

    <UserControl.Resources>
        <coreconverters:IntegerIncrementerConverter
    x:Key="IntegerIncrementerConverter"/> </UserControl.Resources>
    <TextBlock Grid.Column="0" Text="{Binding SelectedIndex,
                    Converter={StaticResource IntegerIncrementerConverter},
                    ElementName=dgOrganizations}" Margin="5"/>
    May 12

    Delphi Prism májusi release

    A májusi kiadás már megpróbál bevezetni egy-két újdonságot még a .NET 4.0 és C# 4.0 előtt. Az új feature-k részletes leírása itt megtalálható. Ezen a listán szerepel a volatile fieldek, a generic type variance–ok, illetve újabb LINQ operátorok támogatása is, mint pl. skip, while, take és take while.

    May 08

    WPF IntelliSense: hol volt, hol nem volt

    Egyes CTP-k telepítése után érhet bennünket meglepetés, mint pl. eltűnik a XAML IntelliSense a VS-ből. Mondanom sem kell, hogy nem éppen kellemes meglepetés volt a dolog. Mondom a megoldást máris:

    1. El kell indítani a cmd.exe-t admin joggal
    2. Újra be kell regisztrálni a következő COM könyvtárat:
      regsvr32
      "C:\Program Files (x86)\Common Files\microsoft shared\MSEnv\TextMgrP.dll"
    May 07

    Windows® API Code Pack for Microsoft® .NET Framework (v0.85)

    Érkeznek az utángyártott managelt könyvtárak a Windows 7 újdonságainak kihasználáshoz is többek között. Na meg persze a Vista olyan szolgáltatásaihoz is, melyekhez nem voltak managelt könyvtárak (lsd. DWM). Úgy látom, hogy mostanában ez a divat, akárcsak a Sync Framework esetében (minden megy natív kódba és arra húznak még egy managelt réteget). Letöltés itt.

    May 04

    WPF Futures

    WPF-hez megjelent az első Model-View-ViewModel Toolkit 0.1. Letöltés itt.