INotifyPropertyChanged mit Hilfe von AOP lösen
Heute möchte ich einmal eine Technik vorstellen, die ich schon öfters bei meinen WPF Projekten eingesetzt habte. Verwendet man in WPF die MVVM-Architektur (ModelView – View – Model), so kommt man um das Konzept des mächtigen WPF Databindings nicht herum. Wer mit Databinding noch nicht so vertraut ist, soll sich zuerst mal hier http://msdn.microsoft.com/en-us/library/ms750612.aspx etwas schlau machen.
Das interessante am WPF Databinding ist die Möglichkeit, Werte bidirektional zu binden: Das heißt, dass man einerseits die Daten von der Datenquelle zur Datensenke binden kann und andererseits auch Änderungen der Datensenke automatisch in die Datenquelle übernehmen kann. In MVVM würde das bedeuten, dass man auf der View (XAML) UI-Controls aus Daten des ViewModels befüllen kann und Änderungen, die auf der View auftreten automatisch zurück in das ViewModel speichern kann, ohne dabei eine Benutzerinteraktion zu benötigen. Wichtig ist auch, dass bereits gebundene Werte auf der View automatisch aktualisiert werden sollen, sobald im ViewModel – also bei der Datenquelle – Änderungen auftreten.
INotifyPropertyChanged oder DependencyProperties
Um ein automatisches Binding-Update zu erreichen, gibt es zwei unterschiedliche Wege, das zu erreichen:
- Man leitet eine Klasse von DependencyObject ab und implementiert DependencyProperties. Da ein ViewModel normalerweise nicht von DependencyObject ableitet, ist diese Möglichkeit im Kontext von MVVM nicht wirklich brauchbar. Mehr über DependencyObject und DependencyProperty gibts unter http://msdn.microsoft.com/en-us/library/system.windows.dependencyobject.aspx
- Man implementiert das Interface INotifyPropertyChanged (http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged.aspx). Dieses Interface implementiert einen Event names PropertyChanged, der gefeuert wird, sobald sich eine Property bzw. der Wert einer Property verändert hat. Bindet man nun in der View (XAML) auf eine Property eines ViewModels, muss diese Property jedesmal, wenn sich der Wert der Property verändert, diesen Event feuern, um automatisch das Databinding zu aktualisieren. Wirft es den Event nicht, gibts auch keine automatische Aktualisierung der View.
Das war mal das Grundgerüst, dass wir für unsere weiteres Thema heute benötigen.
Wo ist das Problem?
Sehen wir uns einmal folgende Klasse an, die INotifyPropertyChanged implementiert und ein paar Properties für das Databinding zur Verfügung stellt:
public class ApplicationViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _ApplicationTitleField = string.Empty;
public string ApplicationTitle
{
get { return _ApplicationTitleField; }
set
{
_ApplicationTitleField = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("ApplicationTitle"));
}
}
private string _ApplicationVersionField = string.Empty;
public string ApplicationVersion
{
get { return _ApplicationVersionField; }
set
{
_ApplicationVersionField = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("ApplicationVersion"));
}
}
}
So, wo ist hier jetzt das Problem? Genau, die Properties selbst. Im Grunde genommen ist es kein Problem bzw. ist syntaktisch alles in Ordnung. Ich persönlich habe aber ein Problem damit, aus den Automatic Properties “alte .NET 2.0″ Properties zu erzeugen, nur um im Setter der Property den PropertyChanged Event feuern zu können. Wie lösen wir das Ganze jetzt: Wir verwenden ein tolle Bibliothek names PostSharp, die die Prinzipen von AOP (Aspect Oriented Programming) implementiert.
Lösung: AOP mittel PostSharp
Mit AOP können wir Funktionalität in sogenannte Aspekte auslagern und diese nach Bedarf wiederverwenden. Genau so einen Aspekt können wir jetzt für unser Problem mit dem PropertyChanged Event einsetzen. Wir können hergehen und uns selbst ein die notwendige Funktionalität programmieren oder wir verwenden bereits vorhandene Bibliotheken, die uns bei unserer Problemlösung unterstützen.
In der Community gibt es mittlerweile eine handvoll verfügbaren Bibliotheken, die AOP Funktionalität für .NET zur Verfügung stellen. Mein persönlicher Favorit ist auf alle Fälle PostSharp entwickelt von Gael Fraiteur. Einer der großen Vorteile von PostSharp ist , dass sich die Bibliothek in den MSBuild Process hängt, direkt den CIL Code manipuliert und somit keine Perfomanceeinbussen entstehen. Wie gehen wir jetzt also vor, um unser Problem der alten .NET Properties mit Hilfe eines Aspekts zu lösen?
- Zuerst laden wir uns mal PostSharp herunten und installieren es: http://www.sharpcrafters.com/postsharp/download
- Um PostHsarp verwenden zu können, müssen wir 2 Referenzen zu unserem Projekt hinzufügen: PostSharp.Public und PostSharp.Laos
- Dann erstellen wir uns den neuen Aspekt. Im Grunde ist das eine einfach Klasse, die von einer bestimmten Basisklasse ableitet und als Serializeable gekenntzeichnet werden muss:
[Serializable] public class NotifyAspect : OnMethodBoundaryAspect { } - Die Basisklasse OnMethodBoundaryAspect stellt PostSharp zur Verfügung. Diese Klasse bietet eine Methode names OnExit an, die wir in unserem Apsekt einfach überschreiben und implementieren. Diese Methode wird aufgerufen, wenn eine Methode verlassen wird — also wenn das Ende einer Methode erreicht wurde. Da wir unseren Aspekt für Properties verwenden, ist unsere Methode eigentlich der Getter bzw. Setter – Zugriff der Property:
public override void OnExit(MethodExecutionEventArgs eventArgs) { if (!(eventArgs.Instance is ViewModelBase)) throw new InvalidCastException("Cannot raise PropertyChanged on viewmodel that doesn't derived from ViewModelBase"); if (!eventArgs.Method.IsSetter()) return; ViewModelBase viewModel = eventArgs.Instance as ViewModelBase; viewModel.OnPropertyChanged(eventArgs.Method.SetterName()); }Wir prüfen,ob die Instanz, zu der die Methode gehört von einer bestimmten Basisklasse ist. Ist das nicht so, dann werfen wir einen Fehler, da der Aspekt nur mit bestimmen Typen umgehen kann. Warum? Weil wir später eine Methode der Instanz aufrufen müssen. Jetzt überprüfen wir, ob die Methode, für die der Aspekt aufgerufen wird eigentlich eine Setter-Methode einer Property ist. Das haben wir in eine Extension-Methode ausgelagert (der detaillierte Code ist im Beispielprojekte nachzulesen). Jetzt können wir casten und eine bestimmte Methode der Basisklasse aufrufen: OnPropertyChanged wirft im Endeffekt den PropertyChanged Event des Interfaces INotifyPropertyChanged. Diesen Event können wir nur in der Klasse werfen, in der der Event auch definiert wurde. Daher müssen wir hier in unserem Aspekt diesen Umweg gehen.
public void OnPropertyChanged(string propertyName) { if (string.IsNullOrEmpty(propertyName)) throw new ArgumentException("Propertyname is null or too short."); PropertyInfo info = this.GetType().GetProperty(propertyName); if (info == null) throw new InvalidProgramException("The requested property doesn't exist."); if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } - Jetzt fehlt uns nur noch, dass wir den Aspekt in unserem ViewModel verwenden:
public class ApplicationViewModel : ViewModelBase { [Notify] public string ApplicationTitle { get; set; } [Notify] public string ApplicationVersion { get; set; } }
Wir können uns jetzt eine View bzw. XAML Seite bauen, die dieses ViewModel als DataContext gesetzt hat und auf die beiden Properties databinden. Das Demoprojekt ist wie immer unter http://downloads.juergenoberngruber.at/blog/INotifyPropertyChangedWithAOP.zip zu finden.
In diesem Sinne.
