MVVM patternt használva WPF fejlesztés során elkerülhetetlen, hogy implementáljuk az INotifyPropertyChanged interfészt. Amikor egy olyan tulajdonság változik a ViewModelben, amire a felületen valami rá van kötve, akkor egy PropertyChanged eseményt kell dobni, hogy értesüljön a UI is. Ehhez tartozóan egy property tipikusan így szokott kinézni:
public class MyViewModel : ViewModelBase
{
private string demoProp;
public string DemoProp
{
get { return demoProp; }
set
{
if (demoProp == value) return;
demoProp = value;
OnPropertyChanged("DemoProp");
}
}
}
Ebben a példában a ViewModelBase ősosztály implementálja az interfészt:
public class ViewModelBase : INotifyPropertyChanged
{
private event PropertyChangedEventHandler propertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (!string.IsNullOrWhiteSpace(propertyName)
&&
propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
A
PostSharp nagy mértékben meg tudja könnyíteni az életet úgy, hogy átláthatóbb, karbantarthatóbb kódot eredményez az aspektus orientáltság révén. A fenti DemoProp kódját többféle módon is képes egyszerűsíteni, ebből két lehetséges:
1. Osztályszintű attribútum és nincs manuális kódolás
Ekkor elegendő egy attribútumot felhelyezni az osztályra és fordításkor minden publikus autoproperty megváltoztatásáról ugyanúgy értesülni fog a rendszer, mintha saját magunk implementáltuk volna az INotifyPropertyChanged interfészt, illetve kötöttük volna be a property-k set ágába az esemény kiváltását. Ennek részletes működéséről
itt lehet olvasni.
Ennek a módszernek egyszerre előnye és hátránya is, hogy csak osztályra lehet feltenni ezt az attribútumot. Ebből adódóan nincs lehetőség arra, hogy ki lehessen választani, hogy melyik property-t figyelje és melyiket ne.
2. Saját készítésű attribútum property-re
Előfordulnak olyan esetek, amikor nem jó az 1. módszer, mivel nem akarjuk az összes property-re ráakasztani. Például ha saját magunk már implementáltuk az INotifyPropertyChanged interfészt, viszont vannak olyan property-k, amikhez nem tartozik extra logika az érték megváltozásához a felület értesítését leszámítva. Emiatt felesleges is lenne kézzel megcsinálni hozzá a backfieldet és a get/set metódusokat. Egyszerűbb lenne, ha csak egy attribútumot kéne feltenni egy autoproperty-re.
A fenti MyViewModel átírva így nézne ki. A különbség szerintem önmagáért beszél:
public class MyViewModel : ViewModelBase
{
[RaisePropertyChanged]
public string DemoProp { get; set; }
}
A példában használt RaisePropertyChanged PostSharp attribútum pedig alább látható:
using System;
using System.Reflection;
using PostSharp.Aspects;
[AttributeUsage(AttributeTargets.Property)]
[Serializable]
public class RaisePropertyChangedAttribute : OnMethodBoundaryAspect
{
/// <summary>
/// Property neve
/// </summary>
private string propertyName;
/// <summary>
/// történt-e változás, kell-e értesítés
/// </summary>
private bool hasChanged;
/// <summary>
/// fordítási időben lekérdezett mezőinformáció
/// </summary>
private PropertyInfo propertyInfo;
public override void OnEntry(MethodExecutionArgs args)
{
//A
CompileTimeValidate biztosítja, hogy ez a kódrészlet
//csak
ViewModelBase-ben lévő property-khez tartozó Sethez legyen hozzáfűzve
//mivel
ez egy Set metódusba való belépés elején fut,
//így biztosan van egy paramétere
(value)
object value = propertyInfo.GetValue(args.Instance);
object param = args.Arguments[0];
hasChanged = (value != null && param == null) ||
(value == null && param != null) ||
(value != null &&
!value.Equals(param));
if (!hasChanged)
{
args.FlowBehavior = FlowBehavior.Return;
return;
}
base.OnEntry(args);
}
public override void OnSuccess(MethodExecutionArgs args)
{
//siker
esetén kell csak dobni az értesítést,
//feltéve, hogy történt egyáltalán változás
if (!hasChanged) return;
((ViewModelBase) args.Instance).ReportPropertyChanged(propertyName);
}
public override bool CompileTimeValidate(MethodBase method)
{
var type = method.ReflectedType;
if (type == null) return false;
//ha nem
ő maga egy ViewModelBase és ráadásul nem is leszármazottja
if (type != typeof(ViewModelBase) &&
!type.IsSubclassOf(typeof(ViewModelBase))) return false;
//ha nem Set metódus
if (!IsPropertySetter(method)) return false;
//fordítási időben eltároljuk a property nevét és a leíróját
propertyName = GetPropertyName(method);
propertyInfo =
type.GetProperty(propertyName);
return true;
}
private static string GetPropertyName(MethodBase method)
{
return method.Name.Replace("set_", "");
}
private static bool IsPropertySetter(MethodBase method)
{
return method.Name.StartsWith("set_");
}
}