Links
- Teil 1 mit den Grundlagen
- Teil 3 zu IDataErrorInfo
- Teil 4 zu Converter und Messenger
- Teil 5 zu Binding von Listen
- Beispiel-Projekt-Repo auf GitHub
- Fody-Repo auf GitHub
- .NET Decompiler dotPeek zum freien Download
Screencast
Vorbemerkungen
Wir könnten uns nach dem ersten Teil nun in die vollen stürzen und das tun, was die meisten Artikel zum Thema nun tun würden - Daten binden usw. Ich denke, es ist jedoch besser, sich erst einmal mit den unterschiedlichen Basisthemen zu beschäftigen, die die WPF-Entwickler und auch die von MvvmLight im Hinterkopf hatten, um bestimmte Dinge besser zu verstehen und vor allem einige typische Fehler nicht zuzulassen.
Bestimmte wichtige Ansatzpunkte zum vorhergehenden Teil können wir durch diese Vorgehensweise allerdings nicht einfach aufgreifen. Daher wird dieser Teil ein wenig außen vor sein und code-seitig nicht nahtlos anknüpfen.
Für diejenigen unter den Lesern, die das im Teaser beschriebene Thema schon aus dem ff kennen bleibt nur der Hinweis auf die noch kommenden Teile dieser Serie.
“Programmier immer gegen ein Interface”
Dieser wichtige Spruch ist im .NET Framework allgemein und speziell bei WPF extrem beherzigt worden. Die beiden Interfaces INotifyPropertyChanged
und IDataErrorInfo
sind für das Verständnis von WPF und MVVM essentiell. Fangen wir in diesem Beitrag mit dem einfachereren an.
INotifyPropertyChanged
Es gibt Interfaces, die sind so simpel, dass viele einfach nie verstanden haben, was Ihr Einsatz bringen soll. Das Interface erfordert von einer Klasse die es implementiert genau eine Sache: Sie muss ein Ereignis namens “PropertyChanged” vom Delegat-Typ PropertyChangedEventHandler
anbieten. Eine kleine Beispiel-Implementierung soll dies verdeutlichen:
public class TestClass : INotifyPropertyChanged
{
#region events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
Genau genommen war es das schon. Das allein brächte natürlich noch nichts, wäre da nicht die in WPF integrierte Komponente der Bindings. Diese Bindings basieren letztlich auf sog. DependencyProperty
-Objekten. Vereinfach ausgedrückt, ist eine Eigenschaft in WPF nicht einfach vom Typ string
oder int
, sondern es ist eine DependencyProperty. Dieser Typ kapselt u.a. auch das Erkennen und Überwachen des Interfaces INotifyPropertyChanged in sich. Man kann also sagen, dass das Interface einfach deswegen so essentiell ist, weils es von WPF beachtet wird.
Unter Windows Forms war das schlicht und ergreifend nicht der Fall, sodass die Integration der Interfaces ein wesentlicher Unterschied (neben vielen anderen) ist. Machen wir aber weiter mit unserem INotifyPropertyChanged. Um das Muster der Nutzung besser zu verstehen, sehen wir uns eine typische Implementierung an:
/// <summary>
/// A class showing the usage of <see cref="INotifyPropertyChanged"/>.
/// </summary>
public class TestClass : INotifyPropertyChanged
{
#region member vars
private string _someProperty;
#endregion
#region events
/// <summary>
/// Occurs when the value of property has changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region methods
/// <summary>
/// Fires the <see cref="PropertyChanged"/> event.
/// </summary>
/// <param name="propertyName"></param>
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region properties
/// <summary>
/// Includes a string value.
/// </summary>
public string SomeProperty
{
get
{
return _someProperty;
}
set
{
if (value == _someProperty)
{
return;
}
_someProperty = value;
OnPropertyChanged();
}
}
#endregion
}
In Listing 2 habe ich den Code aus Listing 1 um eine Eigenschaft SomeProperty
ergänzt. Zunächst einmal ist es eine ganz normale Eigenschaft vom Typ string
. Der Setter der Eigenschaft ist nun interessant. Nachdem der Wert übernommen wurde (und zwar nur dann, wenn auch wirklich ein neuer Wert vergeben wurde) wird die interne Methode OnPropertyChanged
aufgerufen. Diese habe ich ergänzt, um das “Feuern” des Ereignisses zu vereinfachen.
Für alle, die sich über den Aufruf und das CallerMemberName
-Attribut wundern, hier ein kleiner Exkurs. CallerMemberName
ist ein Attribut, das auf Parameter von Methoden angewendet kann. Wird es einem Parameter übergeben, wird .NET beim Aufruf der Methode auotmatisch den Namen des Aufrufers in diese Variable schreiben. Das ist der Grund, warum ich in Zeile 52 in Listing 2 nichts an die Methode übergebe. Es wird späer automatisch befüllt. In unserem Szenario hat es den Vorteil, dass wir der OnPropertyChanged
-Methode nicht Informationen (z.B. nameof(SomeProperty)
mitgeben müssen, die die Runtime viel besser automatisch generieren kann - less code!
Erst bei der Benutzung der Klasse zeigt sich der Erfolg so richtig:
var test = new TestClass();
test.PropertyChanged += (s, e) =>
{
Console.WriteLine($"Property {e.PropertyName} has changed. New value is [{test.SomeProperty}].");
};
test.SomeProperty = "Hello World!";
test.SomeProperty = "Hello World!";
test.SomeProperty = "Hello World again!";
Die durch Listing 3 erzeugte Ausgabe bei Einsatz als Konsolenwendung ist:
Property SomeProperty has changed. New value is [Hello World!].
Property SomeProperty has changed. New value is [Hello World again!].
Wie man erkennen kann, meldet also unsere TestCkass
an jeden interessierten Aufrufer automatisch, wenn es Änderungen an der SomeProperty
gibt. Das ist genau das, was das WPF-Binding braucht, um Dialoge (also Views) synchron mit den Daten der Logik (den ViewModels) zu halten.
Lästiges los werden
Die Segnungen von C# haben uns nun vor einiger Zeit so etwas, wie die Auto-Properties gebracht. Setzen wir aber Listing 3 konsequent in unserer Logik um, enden wir wieder in elenden Copy-&-Paste-Orgien von Properties. Damit genau das nicht passiert, nutzen wir Technologie - in diesem Fall hilft uns Fody aus der Klemme. Genauer gesagt nicht nur Fody sondern eins seiner Module genannt PropertyChanged.
Fody kommt immer dann ins Spiel, wenn der Compiler dabei ist, Quellcode in Intermediate Language zu übersetzen. Fody PropertyChanged generiert dabei Quellcode anhand des Interfaces INotifyPropertyChanged.
Zunächst einmal holen wir uns Fody PropertyChanged ins Projekt:
install-package PropertyChanged.Fody
Ich habe den gesamten Inhalt dieses Teils in das MvvmSample-Projekt auf unserem GitHub integriert. Das entsprechende Projekt ist “Ui.TestConsole”. Hier Nachdem ich das Paket per NuGet eingebunden habe, ibt es eine neue Referenz “PropertyChanged” und eine “FodyWeavers.xml”. Fody selbst ist modular aufgebaut und über diese Datei bestimmt man, dass das PropertyChanged aktiviert sein soll:
<?xml version="1.0" encoding="utf-8"?>
<Weavers>
<PropertyChanged />
</Weavers>
Sobald das getan ist, entfernt dünnt man die TestClass wie folgt aus:
/// <summary>
/// A class showing the usage of <see cref="INotifyPropertyChanged"/>.
/// </summary>
public class TestClass : INotifyPropertyChanged
{
#region events
/// <summary>
/// Occurs when the value of property has changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region properties
/// <summary>
/// Includes a string value.
/// </summary>
public string SomeProperty { get; set; }
#endregion
}
Führt man den Quellcode aus Listing 3 nun erneut aus, funktioniert das Programm immer noch!
Der Schlüssel dazu liegt darin, dass PropertyChanged den kompletten Quellcode für das Werfen des PropertyChanged-Ereignisses nun selbst programmiert. Ein Screenshot mit einem Disassembler, wie z.B. dotPeek zeigt das Ergebnis unserer Klasse in der EXE:
Wie man hier sehen kann, ergänzt Fody PropertyChanged automatisch Getter und Setter im Quellcode (was der C# Compiler ohnehin tut, da AutoProperties nur ein Sprachfeature sind) und ruft bei Klassen, die INotifyPropertyChanged
implementieren das Event auf.
Soweit, so gut. Was passiert aber, wenn man Eigenschaften hat, die in Abhängigkeit von anderen Eigenschaften Ihre Werte ändern. Ein Beispiel in unserer TestClass wäre:
/// <summary>
/// The lenght of the string in <see cref="SomeProperty"/>.
/// </summary>
public int LengthOfSomeProperty => SomeProperty.Length;
Wenn man diesen Code ergänzt und dann F5 drückt, passiert in der Console folgendes:
Property LengthOfSomeProperty has changed. New value is [Hello World!].
Property SomeProperty has changed. New value is [Hello World!].
Property LengthOfSomeProperty has changed. New value is [Hello World again!].
Property SomeProperty has changed. New value is [Hello World again!].
Ein Blick in das Disassembly zeigt, woher die Magie kommt:
Manchmal reicht aber diese Logik nicht aus, weil sie Abhängigkeiten einfach nicht mehr auflösen kann. Daher kann man mit Attributen aus dem PropertyChanged-Reportoire nachhelfen. Eine expliziter Hinweis an PropertyChanged, das eine Eigenschaft von einer anderen abhängt könnte in unserem Fall so aussehen:
/// <summary>
/// The lenght of the string in <see cref="SomeProperty"/>.
/// </summary>
[DependsOn(nameof(SomeProperty))]
public int LengthOfSomeProperty => SomeProperty.Length;
Will man PropertyChanged anweisen, bei Änderung einer Eigenschaft nichts zu tun, geht das selbstverständlich auch:
/// <summary>
/// Contains a boolean value and will not raise <see cref="PropertyChanged"/> when it is changed.
/// </summary>
[DoNotNotify]
public bool SomeSecretProperty { get; set; }
Integration in WPF
Ich möchte nun doch noch eine kleine Brücke zu unserer Beispiel-Anwendung schlagen. Ich erweitere unser dort im ersten Teil erstelltes MainViewModel
um eine Eigenschaft Progress
vom Typ int
und hole mir per NuGet PropertyChanged.Fody
mit ans Boot. Das ganze findet im Projekt Logic.Ui
statt.
Danach binde ich eine ProgressBar im View an diese Eigenschaft und lasse sie in einer Timer-Schleife hochlaufen. Ohne weiteren Sync-Code zeigt nun das UI die Änderung des Wertes an und das Ganze -wie wir nun wissen - nur, weil WPF auf INotifyPropertyChanged
hört.
Das Ganze Verhalten lässt sich in einem Video besser beobachten, weshalb ich interessierte auf den Screencast verweise.
Ausblick
Das war ganz schön viel für ein mickriges Interface wie INotifyPropertyChanged
, aber der Teufel steckt wie immer im Detail. Ich hoffe, ich konnte ein paar Erkenntnisse bringen und möchte im nächsten Beitrag wieder alle MVVM-Pros vergraulen und mich mit dem Interface IDataErrorInfo
befassen.