2010. július 28., szerda

Univerzális View Model készítése 1. rész

Az MVVM (Model-View-View Model) tervezési minta használata nagyon célszerű üzleti alkalmazások fejlesztése során. Egyrészt igen átláthatóvá teszi a felület működését, másrészt remekül lehet modularizálni a rendszert. Továbbá megsokszorozódik a jelentősége, ha több platformra kívánunk GUI-t fejleszteni.

Ebben a cikkben megosztom gyakorlati tapasztalataimat a legújabb technológiák használatával összeállított ViewModel alaposztály elkészüléséről. Fontos, hogy olyan alaposztályokat akartam létrehozni, amelyet cross-platform tudok alkalmazni (pl. .NET 4 és Silverlight 4 környezetben is).

A ViewModel funkciója, hogy biztosítsa a View (pl. egy Silverlight UserControl) számára az adatokat, amelyek az adatkötéssel (binding) ér el. Továbbá, hogy reagáljon a modellben történt adat- és állapotváltozásokra.

Ahhoz, hogy a binding működjön, szükséges, hogy a modell implementálja a INotifyPropertyChanged interface-t, amely biztosítja a modellre “csatlakozóknak” az értesítést a modell változásairól. Az alaposztályunk:

public abstract class ViewModelBase : INotifyPropertyChanged
{
    // code...
}

Hagyományosan úgy valósítjuk meg az értesítést, hogy a tulajdonság set metódusába beleírunk egy hívást, amely a meghívja a PropertyChanged eseményt:

IEnumerable<Partner> partners;

public IEnumerable<Partner> Partners
{
    get { return partners; }
    set
    {
        partners = value;
        RaisePropertyChanged("Partners");
    }
}

Ezzel a megoldással nincs semmi gond – szintaktikailag. Látható, hogy elég körülményes lenne minden egyes tulajdonság definiálása a fenti módszerrel, ráadásul magában hordozza a tévesztés lehetőségét, hiszen a tulajdonságunk nevét string konstansként kell megadnunk. Ha refaktorálunk, akkor elveszhet a szinkron. Diplomatikusan még elmondható, hogy az áttekinthetőséget sem javítja.

A probléma orvoslására a PostSharp programot használtam, amellyel úgynevezett AOP valósítható meg, azaz aspektus orientált programozás. A PostSharp bemutatkozó oldalán olvasható, hogy a komplikált teória elmagyarázása helyett mutatnak néhány példát. Én is inkább ezt teszem.

A PostSharp használata általában egy attribútum definiálásával kezdődik. Ezt valamelyik a PostSharp library-ben található aspektus attribútum osztályból származtatjuk.

A fent meghatározott problémára remek válasz lenne egy olyan aspektus implementálása, amely a tulajdonság változásakor fut le. Szerencsénk van, mert létezik ilyen! Úgy hívják: LocationInterceptionAspect. Nem nagyon értem, hogy miért ez a neve, de legyen. (Itt szeretném megjegyezni, hogy az INotifyPropertyChanged problémára van a PostSharphoz remek példakód, amelynek csak egy apró hátulütője van, hogy az ingyenes verzióval nem lehet lefordítani... Természetesen, aki megvette a programot, annak az a megoldás sokkal elegánsabb, hiszen csak az osztályt kell attributálni, és mindent megy “magától”.)

Két virtuális metódust tartalmaz a LocationInterceptionAspect aspektusosztály. OnGetValue, OnSetValue. Nos, nekünk az utóbbira van szükségünk. Azt szeretnénk, hogy ez a metódus meghívja a ViewModel osztály RaisePropertyChanged metódusát. Mivel tudnia kell, hogy implementál a ViewModel egy ilyen metódust, de azért nem kellene, hogy az publikus legyen, ezért egy interfészt vezetünk be, amelyet explicit valósítunk meg:

public interface ISupportRaisePropertyChanged
{
    void RaisePropertyChanged(string propertyName);
}
public abstract class ViewModelBase : INotifyPropertyChanged, 
ISupportRaisePropertyChanged
{
    protected void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged.Invoke(this,
new PropertyChangedEventArgs(propertyName));
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion

    #region ISupportRaisePropertyChanged Members

    void ISupportRaisePropertyChanged.RaisePropertyChanged(string 
propertyName)
    {
        ((ISupportRaisePropertyChanged)this)
.RaisePropertyChanged(propertyName);
    }

    #endregion
}

Remek. Most már meg tudjuk hívni a RaisePropertyChanged metódust az aspektus OnSetValue metódusából:

public sealed class NotifyPropertyChangedAspectAttribute 
: LocationInterceptionAspect
{
    public override void OnSetValue(LocationInterceptionArgs args)
    {
        base.OnSetValue(args);

        if (typeof(ISupportRaiseProperyChanged)
.IsAssignableFrom(args.Instance.GetType()))
        {
            var target =
((ISupportRaiseProperyChanged)args.Instance);
            target.RaisePropertyChanged(args.LocationName);
        }
    }
}

Ezzel a megoldással elértem, hogy az alábbi egyszerű formában lehessen definiálni a View Modellünk tulajdonságait:

[NotifyPropertyChangedAspect]
public IEnumerable<Partner> Partners { get; set; }

Ez azért így jobb. Tévedések is kizárva. Ez a kód fordul .NET 4 frameworkkel és Silverlight 4-el egyaránt.

Folytatom…

Referenciák:

MVVM – Model-View-View Model “startlap” Josh Smith MVVM Guru-tól
PostSharp – Aspektus orientált programozási keretrendszer .net-hez

2 megjegyzés:

  1. Az aspektuson belül az interfész implementálásának ellenőrzésére ajánlom az aspektus CompileTimeValidate metódusát, így ha "rossz" elemre teszed az attribútumot, az fordítási időben kiderül.

    VálaszTörlés
  2. @borzos: Kösz a tippet! Az átok csak az, hogy a Silverlightos Postsharp kicsit "más", így ott az Aspect osztály nem tartalmaz CompileTimeValidate virtuális metódust. Sajnos, ilyen kellemetlenségek vannak, amikor több platformra használatos kódot akarunk írni. Na, de mire jó a "#if !SILVERLIGHT"? Erre!

    VálaszTörlés