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
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@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