WPF címkéhez tartozó bejegyzések

MVVM (WPF & Silverlight) Beta 2

Néhány változás:

Az EditStateChanged esemény most közli a módosított tulajdonságokat is, esetleg ha szükség lenne loggoláshoz, vagy bármihez. Ez akkor váltódik ki amikor az IEditableObject műveletek használtak akár az EditableObject, akár az EditableViewModelBase származékok esetében.

Van egy új PagingHelper osztály a Helpers mappába, illetve ehhez egy extension az Extensions mappába. Így könnyebb lapozható query-ket írni.

Service

Az egyik legérdekesebb része a dolognak. Mivel egy-egy művelet lehet komplexebb, több input és több outputal, így ezt elvonatkoztatva a konkrét példáktól, generikusabb módszert választottam. Szolgáltatás esetében most tisztázzuk, hogy nem WCF szolgáltatásról, vagy hasonlóról beszélünk, hanem tulajdonképpen a model, mint szolgáltatás rétegről van szó. A történet egyszerű, szolgáltatás műveletek vannak, melyeket interfészek írnak le, pontosan két típus:

Ha nincs visszatérési érték:
interface IServiceAction<in TRequest> : IServiceOperation<TRequest>

Ha kapunk vissza értéket:
interface IServiceFunc<in TRequest, TResponse> : IServiceOperation<TRequest>

Mindegyikhez tartozik egy alap implementáció, melyek neve:

class ServiceActionBase<TService, TRequest> : ServiceOperationBase<TService, TRequest>, IServiceAction<TRequest>

class ServiceFuncBase<TService, TRequest, TResponse> : ServiceOperationBase<TService, TRequest>, IServiceFunc<TRequest, TResponse>

Az implementáció tartalmaz egy szinkronizációs kontextust (ami WPF és SL esetében a dispatcher kontextus lesz alapértelmezésben), illetve magát a tényleges szolgáltatást (TService), ami lehet egy WCF Data Service kontextus, sima WCF szolgáltatás, vagy akár egy generikus lista (List<IPerson>) is teszteléshez, mint adatforrás (szolgáltatás).

Minden szervíz művelet meghatároz egy abstract Execute metódust, aminek a Request lesz a paramétere. Amikor saját műveletet írunk, ezt kell mindig felülírnunk. Mutatok egy példa implementációt WCF Data Service-hez:

Ugye nem akarunk 667 query-t, meg anyám kínját írni minden lapozáshoz, stb. Ezért származtatunk egy service operation-t. Jelenleg én csináltam egy ItemsRequest és egy ItemsResponse osztályt lekérdezésekhez. A ItemsRequest osztály tartalmaz a lapozáshoz szükséges paraméterek megadását, az ItemsResponse pedig az eredményt, illetve az összes tétel számát. Ezalapján csináljunk egy közös őst lekérdezésekhez általában, ami ráadásul a megfelelő felüleletbe is már automatikusan kasztol (Person => IPerson). Ezt a példát megtaláljátok a Tests mappában:

public abstract class DataServiceQueryOperation<TItem, TInterfaceItem>    : ServiceFuncBase<ShopContext, ItemsRequest, ItemsResponse<TInterfaceItem>>{    public DataServiceQueryOperation(ShopContext service)        : base(service) { }

    protected abstract DataServiceQuery<TItem> OnExecute(ItemsRequest request);

    public override void Execute(ItemsRequest request)    {        try        {            var query = (DataServiceQuery<TItem>)(OnExecute(request)                .AddQueryOption("$inlinecount", "allpages")                .Paging(request.Paging.PageIndex, request.Paging.PageSize));

            query.BeginExecute((IAsyncResult ar) =>            {                var list = default(IEnumerable<TInterfaceItem>);                var error = default(Exception);                var response = default(QueryOperationResponse<TItem>);                try                {                    response = (QueryOperationResponse<TItem>)query.EndExecute(ar);                    list = response.Cast<TInterfaceItem>().ToArray();                }                catch (Exception ex)                {                    error = ex;                }

                Synchronization.Post(state =>                {                    OnCompleted(new ResponseEventArgs<ItemsResponse<TInterfaceItem>>(                        error, new ItemsResponse<TInterfaceItem>(list, response.TotalCount)));                }, null);

            }, null);        }        catch (Exception ex)        {            Synchronization.Post(state =>            {                OnCompleted(new ResponseEventArgs<ItemsResponse<TInterfaceItem>>(ex, null));            }, null);        }    }}

Mint látható ez a kis osztály segít egy query-t aszinkron futtatni, majd az eredményt a dispatcher kontextusába (vagy az éppen aktuális szinkronizációs kontextusba) vissza postolni. Az $inlinecount segít, hogy egyetlen kéréssel kapjuk vissza az összes tétel számát is egyúttal. Ez alapján egy Person listát lekérdező művelet:

public class PersonQueryOperation : DataServiceQueryOperation<Person, IPerson>{    public PersonQueryOperation(ShopContext service)        : base(service)    {    }            protected override DataServiceQuery<Person> OnExecute(ItemsRequest request)    {        return Service.Person;    }}

Jó nem? Természetesen csinálhatunk saját request és response-t is, a lényeg ugyanaz. Egységes módon, műveleteket definiálunk a szolgáltatás modellünkbe, kicsit SOA mintára, amiket különböző adatforrásokhoz implementálhatunk. Maga a szolgáltatás csak a műveletet írja le:

public interface IPersonService{    IServiceFunc<ItemsRequest, ItemsResponse<IPerson>> GerPersonList { get; }}

Azt már különböző implementációk döntik el, honnan jönnek ehhez az adatok. További részletekért nézzétek át a teszt alkalmazást. Megjegyzem az a teszt nem teljes, csak azon kísérletezgetek.

ViewModels

Összevontam néhány dolgot, csak egy RepositoryViewModelBase létezik, viszont van egy új property: Pager. A Pager egy RepositoryPagerViewModel, amely az összes szükséges lapozási szolgáltatás commandokba foglalva a felületen elérhetővé teszi. Fontos látni, ez az implementáció nem függ a IPagedCollectionView-tól, amúgy is az WPF-ben nincs is, hanem bármihez jó. A tesztbe szintén láthattok rá példát, hogyan kell XAML-be a felületbe bekötni. DataPager nem támogatott és nem is kell! A lényeg, hogy kezeli a szerkesztést, stb. Így pl. nem lehet addig lapozni, amíg valami szerkesztési állapotba van, illetve auto frissíti a lapozó parancsokat, gombokat. Lehet állítani az oldal méretet is a felületbe kötve, stb. Alapértelmezésbe felülírhatók az OnPageChanging és OnPageChanged tagok, így akár egyénileg megkadályozható a lapozás. A commandok auto kezelik ezt és a UI szintén frissül eszerint. De lehetőség van az egész Pager cseréjére is, felülírva a virtuális property-t. Egy dologra kell figyelni ha saját pager-t csinálunk, hogy az OnPageChanging és OnPageChanged eseményekre iraktkoztassuk fel (nem kötelező, de pl. az OnPageChanged auto meghívja a Refresh metódust, ami újra lekéri az adatokat a PageIndex, PageSize, ItemCount változáskor).

Jó szórakozást hozzá:

VVMF Design Patterns & Practices – Beta 2.

MVVM (WPF & Silverlight) Beta 1

Felraktam az első Beta verziót mind WPF 4.0, mind Silverlighth 4.0-hoz. Mit tud?

Ha belenéz valaki, láthatja hogy van benne minden a világon. Vannak olyan dolgok, amik a Prism implementációból, vannak amik az MS MVVM Toolkit-ből, illetve vannak amik az MVVM Light Toolkit-ből származnak, javarészt kibővítve azok működését. Az alap objektum mindenhez az ObservableObject. Ez implementálja az INotifyPropertyChanging, illetve INotifyPropertyChanged felületeket. SL-be is csináltam INotifyPropertyChanging-et. Aztán van egy EditableObject, ami implementálja az IEditableObject,, INotifyDataErrorInfo, IDataErrorInfo interfészeket, így mind WPF, mind SL-be működik a validáció is. A validáció akárcsak a RIA Service entitás osztályai, a DataAnnotation attribútumokat használja (pl. [Key], [Required], [StringLength]). Jegyzem meg, hogy van egy EditSession osztály is, ami bármilyen osztályhoz újrahasználható az IEditableObject implementálásához. Az EditableObject is ezt használja belsőleg. Az EditSession a chikk által készített Accessor API-t használja és cachel, elkerülve a reflection-t, lásd: Reflection/PropertyOrFieldAccessorRepository.cs. Van egy DispatcherSync statikus osztály, amivel könnyedén tudunk szinkronizálni a Dispatcherrel. Pl. ViewModel-be tanácsos a kollekció műveleteket mindig szinkronizálni a Dispatcher-el, ahol a SafeBeginInvoke() segít annak eldöntésében, hogy szükség van-e rá, vagy sem. Na ennyit az alap osztályokról, térjünk az MVVM részre:

Commands

DelegateCommand
ICommand implementáció paraméter nélkül (pontosabban null paraméterrel).

DelegateCommand<T>
ICommand implementáció egy T típusú paraméterrel.

EventCommand
UI események (event handlers) ICommand-ba diszpécselése.

CommandReference
Segít abban, hogy egy command-ot összeköthessünk valamilyen KeyBinding-al.

WeakCommandManager
Silverlighthoz készítettem, ami megoldja a parancsfrissítési limitációt. Az SL-be a CanExecute kiértékelése nem történik meg automatikusan javarészt, így szükséges meghívni az InvalidateRequerySuggested()-t metódust, ami az összes nyilvántartott CanExecuteChanged handler-t megívja. Most ezt a ViewModelBase minden view model property változáskor automatikusan meghívja, így frissül a UI korrekten, akárcsak WPF-ben. Természetesen a WPF-es változatban ez a CommandManager ugyanezen metódusát hívja meg.

Service

IQueryableService
Leír egy alap lekérdezhető szolgáltatást amiből kitudunk nyerni adatokat. Ennek mindössze egy Load metódusa van és egy esemény, ami akkor váltódik ki, ha a betöltés megtörtént vagy elbukott valamilyen okból kifolyólag. Az OperationEventArgs Error tulajdonsága hordozza ilyenkor a kapott kivételt és a Result az eredmény kollekciót. A lényege ennek a résznek az MVVM-ben, hogy az adatkezelést egy jól szeparált módon valósítja meg, beleértve azt is, hogy teljesen függetleníthető az adatkezelés az adatforrás típusától (lehet az EF, WCF Data Service, WCF RIA Service, WCF, stb.). Ha pl. közös felületeket használunk (IPerson, IOrganization, stb.) – amiket agyrém kézzel megírni mindenhez – univerzálisan rugalmas implementációt kapunk.

IUpdateableService
Ugyanaz mint az előző, csak kiteszi CUD műveleteket, illetve van egy Save, Cancel tagja is. Nyilván nem mindenhol használatos minden, ezért ha nincs felülírva valami NotSupportedException-t kapunk. Ha pl. WCF Data Service-t, vagy RIA Service-t használunk, akkor a kontextus change tracking dolgozik, így a Save használható, sőt RIA-nál a Cancel is.

View Models

ViewModelBase
Hasonló az MVVM Light Toolkit-hez, lényegében a különbség mindössze, hogy van INotifyPropertyChanging (SL-be is), van egy Execute metódus parancsok biztonságos futtatására (tehát a CanExecute-ot is kiértékeli), illetve minden ViewModel és ObservableObject szerializálható is egyben. Az üzenetes megoldás ugyanaz mint a Light Toolkit-ben, van egy singleton Messenger instance, stb.

EditableViewModelBase
Mivel a view model általában kiteszi az alatta lévő model egyes tulajdonságait, s mivel a UI közvetlen ezekkel fog érintkezni, szükséges ezen a szinten is biztosítani a validációt és szerkeszthetőséget. Azért mondom, hogy ezen a szinten is, mert kinek hogy tetszik, van aki szereti a model osztályokban tartani (lásd WCF RIA entitások), valaki a View Model-be felhozza. Ez a scenariotól függ, hogy model osztályokkal dolgozunk, vagy view model wrapperekkel a modellek körül (ez utóbbi a rugalmasabb, de több meló kivezetni a property-ket). De visszatérve, összességében véve ez osztály ugyanaz, mint az EditableObject, viszont ez szerkesztési parancsokat is kitesz már (BeginEditCommand, CancelEditCommand, EndEditCommand), melyek már konyhakészen köthetőek gombokhoz, vagy bármihez ami támogatja a command kezelést.

Fontos megjegyezni, mindent code contract-ok írnak le, így mindenki aki megvalósítja az adott interfészeket, örökli ezeket a kényszerített megszorításokat. Így pl. minden művelet (Open, Close, stb.) csak akkor tud lefutni, ha a hozzájuk tartozó (CanOpen, CanClose, stb.) igaz értéket ad vissza. A compiler auto beilleszti az osztályok metódusaiba az interfész contract megszorításokat, így nincs szükséges egyéni exception mutatványokra az osztályok felületek által leírt tagjaiban.

QueryableRepositoryViewModelBase
Ez tulajdonképp felfogható egy lekérdezés eredmény-kollekciójának. Tartalmazhat view model (mint model wrapper), de akár sima model osztályokat is. A szokásos adatforrás műveleteket implementálja, mint Open, Close, Refresh, illetve nyilvántartja a saját állapotát (State). A UI-ba szinte mindig a CollectionView propertyhez kell kötnünk. A CollectionView előnye, hogy cserélhető (lehet lapozható, szűrhető, csoportosítható, stb.), illetve állapotinformációkkal lát el egy Collection-t. Bár a neve egy nézetre utal, hiszen az is, a kollekció egy nézete, de semmilyen köze nincs olyan vonatkozásban a view-okhoz. Tulajdonképp egy adatforrásnak fogható fel, a WCF RIA is implementál egy saját lapozható Collection View-ot és annak változása során állítja elő a megfelelő lekérdezéseket. A CollectionView CurrentItem tulajdonsága jelöli az aktuális tételt, amit a WPF és Silverlight kötési motorja automatikusan frissít. Ez ki lett vezetve SelectedItem néven. Van IsSynchronizedWithCurrentItem, illetve SynchronizeSelectedItems behavior (SL: ListBox-hoz, WPF: MultiSelector származékokhoz) is a csomagban. A view model SelectedItems tulajdonsága van fenntartva a több tételes kijelölés szinkronizálására.

UpdateableRepositoryViewModelBase
Ugyanaz mint az előző, csak kiegészíti néhány művelettel, mint Insert, Delete, Save, Cancel. Ami nincs implementálva NotSupportedException-t fog dobni, ha használjuk.

Na ennyit így hajnali 4-kor erről. A tesztalkalmazást ne vegyétek alapul, azt csak úgy összeraktam kutyafuttában, de idáig megy, az a lényeg. Itt a kód:

VVMF MVVM Design Patterns & Practices

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!

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

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"

WPF DataGrid sorszámok

Mutatok egy újabb kis trükköt, amit pont most találtam ki. Mivel a DataGrid nem képes megjeleníteni a sorszámot, ráadásul a DataGridRow csak egy GetIndex() metódust tartalmaz ennek lekérdezésére, szükség van egy property-re. Ugye a DataGridRowHeader, és a DataGridCell is, mint gyermekei jelennek meg a DataGridRow-nak. Ez jó, ezt ki lehet használni. A DataGrid-nek pedig van egy LoadingRow eseménye. Semmi más dolgunk nincs, mint készíteni egy attached property-t, ami kiegészíti a DataGridRow–ot egy index tulajdonsággal (ami a DataGridRow.GetIndex()-ből veszi az értékét):

public sealed class DataGridRowExtender
{
    public static int GetIndex(DataGridRow obj)
    {
        return (int)obj.GetValue(IndexProperty);
    }

    public static void SetIndex(DataGridRow obj, int value)
    {
        obj.SetValue(IndexProperty, value);
    }

    public static readonly DependencyProperty IndexProperty =
        DependencyProperty.RegisterAttached("Index", typeof(int),
typeof(DataGridRowExtender), new UIPropertyMetadata(
new PropertyChangedCallback(OnIndexChanged))); private static void OnIndexChanged( DependencyObject dpo, DependencyPropertyChangedEventArgs e) { var row = dpo as DataGridRow; if (row == null) return; var grid = UIHelper.GetParentByType<DataGrid>(row); if (grid == null) return; grid.LoadingRow -= OnLoaded; grid.LoadingRow += OnLoaded; } private static void OnLoaded(object sender, DataGridRowEventArgs e) { SetIndex(e.Row, e.Row.GetIndex() + 1); } }

Csiszoltam valamennyit ehhez az UIHelper osztályon is:

public sealed class UIHelper
{
    public static T GetChildByType<T>(DependencyObject dpob)
        where T : DependencyObject
    {
        if (dpob == null)
            return null;

        var count = VisualTreeHelper.GetChildrenCount(dpob);
        for (var i = 0; i < count; i++)
        {
            var child = VisualTreeHelper.GetChild(dpob, i);
            T childAsT = child as T;
            if (childAsT != null)
                return childAsT;

            childAsT = GetChildByType<T>(child);
            if (childAsT != null)
                return childAsT;
        }

        return null;
    }

    public static T GetParentByType<T>(DependencyObject dpob)
        where T : DependencyObject
    {
        if (dpob == null)
            return null;

        var parent = dpob;
        while ((parent != null) && (parent is T == false))
            parent = VisualTreeHelper.GetParent(parent);

        return parent as T;
    }
}

Ezt követően pedig megadjuk az attached property-t a DataGridRow-on, illetve bekötjük azt a DataGridRowHeader ControlTemplate-jében található vezérlőkbe:

<!-- DataGridRow -->
<Style TargetType="{x:Type basic:DataGridRow}">
    <Setter Property="utils:DataGridRowExtender.Index" Value="-1"/>
</Style>

<!-- DataGridRowHeader -->    
<Style TargetType="{x:Type primitives:DataGridRowHeader}">
  <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="primitives:DataGridRowHeader">
               <CheckBox VerticalAlignment="Center"
                         IsChecked="{Binding Path=IsSelected, Mode=TwoWay,
                            RelativeSource={RelativeSource FindAncestor,
                            AncestorType={x:Type basic:DataGridRow}}}"                                    
                            Content="{Binding Path=(utils:DataGridRowExtender.Index),
Mode=TwoWay, RelativeSource={RelativeSource FindAncestor
, AncestorType={x:Type basic:DataGridRow}}}"/> </ControlTemplate> </Setter.Value> </Setter> </Style>

Ha mindent jól csináltunk, az eredmény:

RowNumber

ExpressionDark.xaml

Lassan elkészül a saját Blend skinem. Azt kell tudni róla, hogy ez a létező téma könyvtáros változat lényeges feljavítása (illetve kijavítása), arról nem is beszélve, hogy ez a DataGrid-et is ráncba szedi. Tartalmaz alap validációs templatet is minden szükséges controlhoz, illetve néhány plusz vezérlő stílust is, mint pl. GroupBox, Grid, etc. Használat előtt szükség van a WPFToolkit-re is, amit innen lehet letölteni. A ExpressionDark téma Beta változata pedig innen letölthető. Használni pofonegyszerű:

<Application x:Class="ThemeApp.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"    
    DispatcherUnhandledException="OnDispatcherUnhandledException"
    StartupUri="WinMain.xaml">
<
Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Resources\ExpressionDark.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
ExpressionDark 1

 

ExpressionDark 2

Remélem tetszik! Használjátok egészséggel!
Ha kellenek új control stylelok, stb., akkor várom az ötleteket!

WPF PasswordBox adatkötés

Ha egy PasswordBox-hoz szeretnénk kötni, szintén szükség van egy kis trükközésre, mivel a Microsoft biztonsági okokból nem dependency property-ként definiálta a PasswordBox password tulajdonságát:

public static class PasswordBoxAssistant
{
    public static readonly DependencyProperty BoundPassword =
        DependencyProperty.RegisterAttached("BoundPassword",
typeof(string), typeof(PasswordBoxAssistant),
new FrameworkPropertyMetadata(string.Empty, OnBoundPasswordChanged)); public static readonly DependencyProperty BindPassword =
DependencyProperty.RegisterAttached( "BindPassword", typeof(bool), typeof(PasswordBoxAssistant),
new PropertyMetadata(false, OnBindPasswordChanged)); private static readonly DependencyProperty UpdatingPassword = DependencyProperty.RegisterAttached("UpdatingPassword",
typeof(bool), typeof(PasswordBoxAssistant)); private static void OnBoundPasswordChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) { PasswordBox box = d as PasswordBox; // only handle this event when the property is attached to a PasswordBox // and when the BindPassword attached property has been set to true if (d == null || !GetBindPassword(d)) return; // avoid recursive updating by ignoring the box's changed event box.PasswordChanged -= HandlePasswordChanged; string newPassword = (string)e.NewValue; if (!GetUpdatingPassword(box)) box.Password = newPassword; box.PasswordChanged += HandlePasswordChanged; } private static void OnBindPasswordChanged(DependencyObject dp,
DependencyPropertyChangedEventArgs e) { // when the BindPassword attached property is set on a PasswordBox, // start listening to its PasswordChanged event PasswordBox box = dp as PasswordBox; if (box == null) return; bool wasBound = (bool)(e.OldValue); bool needToBind = (bool)(e.NewValue); if (wasBound) box.PasswordChanged -= HandlePasswordChanged; if (needToBind) box.PasswordChanged += HandlePasswordChanged; } private static void HandlePasswordChanged(object sender, RoutedEventArgs e) { PasswordBox box = sender as PasswordBox; // set a flag to indicate that we're updating the password SetUpdatingPassword(box, true); // push the new password into the BoundPassword property SetBoundPassword(box, box.Password); SetUpdatingPassword(box, false); } public static void SetBindPassword(DependencyObject dp, bool value) { dp.SetValue(BindPassword, value); } public static bool GetBindPassword(DependencyObject dp) { return (bool)dp.GetValue(BindPassword); } public static string GetBoundPassword(DependencyObject dp) { return (string)dp.GetValue(BoundPassword); } public static void SetBoundPassword(DependencyObject dp, string value) { dp.SetValue(BoundPassword, value); } private static bool GetUpdatingPassword(DependencyObject dp) { return (bool)dp.GetValue(UpdatingPassword); } private static void SetUpdatingPassword(DependencyObject dp, bool value) { dp.SetValue(UpdatingPassword, value); } }