Der ultimative INotifyPropertyChanged Guide in 2024 – für C# & VB.NET
Inhaltsverzeichnis
- 1 Datenbindungs-Änderungen einfach ans UI weiterleiten
- 2 Zu viel Text? Schaue das Video!
- 3 Was ist INotifyPropertyChanged?
- 4 Wofür ist INotifyPropertyChanged?
- 5 Wie verwendet man INotifyPropertyChanged dann?
- 6 Ein konkretes MVVM Beispiel
- 7 Was, wenn sich etwas ändert?
- 8 Eine INotifyPropertyChanged Implementierung
- 9 Verbesserung 1 – Zentralisierung
- 10 Verbesserung 2 – Vererbung der Funktionalität
- 11 Weiterführende Links
Datenbindungs-Änderungen einfach ans UI weiterleiten
Kennst Du auch das Problem, die „INotifyPropertyChanged“-Schnittstelle immer wieder implementieren zu müssen? Egal ob in VB.NET oder C#, jedes „ViewModel“ benötigt in guter alter „WPF MVVM“-Manier (leider) dieses implementierte Interface, um Änderungen der Daten an die Oberfläche bekanntzugeben. Nervig wird es dann nur, wenn man das Gefühl hat, endlose Monkey-Work zu schreiben, aber zum Glück gibt es die Vererbung, gell!?
Mithilfe von Vererbung werden wir uns heute ein kleines Helferlein, Welches einem die tägliche Arbeit im Bereich Datenbindungen vereinfachen kann, erstellen. In einem folgenden Beitrag werde ich hier auch noch einen Schritt weiter optimieren, das darfst Du auf keinen Fall verpassen – dort reduzieren wir alles auf eine einzige Zeile 😉. Ich meine, wer hat schon Bock alles 50x zu schreiben!?
Bevor wir damit jedoch starten, machen wir erst einmal eine Reise zurück, also zur Basis von INotifyPropertyChanged.
Zu viel Text? Schaue das Video!
Wenn Du keine Lust hast, diesen Beitrag in schriftlicher Form durchzuarbeiten, kannst Du Dir natürlich auch das Video zu Gemüte führen.
Was ist INotifyPropertyChanged?
Wie oben bereits erwähnt, gehen wir erst einmal einen Schritt zurück, bevor wir weiter nach vorn gehen. Man muss ja schließlich erstmal die Basics dahinter verstehen, bevor man dann auf Diesen aufbauen kann. Wie man zumindest schonmal an dem für .NET Schnittstellen typischen „I“-Prefix erkennen kann, handelt es sich bei „INotifyPropertyChanged“ um eine Schnittstelle (engl. Interface).
Exkurs Schnittstellen
Ohne zu tief in die Thematik Schnittstellen einzutauchen, könnte man sagen: „Schnittstellen definieren grobes Verhalten, Eigenschaften und auch Ereignisse, ohne Diese konkret zu implementieren“. Wenn man einen Menschen mit Interfaces beschreiben wollen würde, könnte man vereinfacht z. B. wie folgt beginnen:
- „ICanWalk“
- „IHaveBladder“
- „ICanSpeak“
Damit würden wir genau das tun, was Schnittstellen ausmacht: Beschreiben, was das jeweilige Objekt können wird (sogar muss). Wir wissen nun, dass ein Objekt, Welches diese Schnittstellen implementieren würde, laufen, sprechen und auf Toilette gehen können muss (eine Blase hat..). Im nächsten Schritt, könnte ich mir also bei der Verwendung eines dieser Objekte sicher sein, dass es sprechen kann. Ich weiß zwar nicht wie dieses „Ding“ spricht, aber ich kann mir sicher sein, dass es dies anbietet.
Reine Abstraktion – Nichts Konkretes
Wo wir beim nächsten wichtigen Punkt wären: Wir wissen nicht, wie der oder diejenige spricht! Die eine Person könnte eventuell nuscheln oder ggf. stottern. Der nächste könnte zum Beispiel in jedem zweiten Satz „Hm..“ sagen – wobei mich das an meine Videos erinnert (Spaß beiseite).
Okay, ich meine, wir könnten dies natürlich auch anders designen- wir sind ja schließlich die Programmierer. Wir könnten z. B. das „ICanSpeak“ nochmal genauer konkretisieren – aber dies ist eine Design-, Erfahrungs- und Anforderungssache! Unser „ICanSpeak“ könnte dann z. B. „ISpeakStutterly“ o. ä. heißen. Somit würde vielleicht eine andere Person, die auf stotternde Menschen spezialisiert ist, schauen: „Okay, wer von allen Menschen die ich hier vorliegen habe, stottern? Wem kann ich mit meiner speziellen Expertise helfen?“.
Quasi unendlich Möglichkeiten
Wenn man dies nun weiter spinnt, könnte man auch über eine dritte Instanz nachdenken, die „ISpeakStutterly – Stotterer“ und „IHelpStutterer“ gezielt zusammenbringt. Aber ich denke das reicht an dieser Stelle, sonst tauchen wir viel zu tief in die Interface-Thematik ab.
Wofür ist INotifyPropertyChanged?
Nachdem wir oben nun eine kleine Erinnerung an die Thematik „Schnittstellen“ ins Gedächtnis gerufen haben, nun zum „INotifyPropertyChanged“-Interface selbst. Grundsätzlich ist die „INotifyPropertyChanged“-Schnittstelle dazu da, der Oberfläche signalisieren zu können: „Hey, hier hat sich etwas geändert, bitte reagiere entsprechend darauf!“. Dies kann man grob und einfach definieren, WPF guckt bei einer Datenbindung: „Okay, immer wenn xy sich ändert, muss ich die Oberfläche neu zeichnen, bzw. die Darstellung aktualisieren“.
Das Problem – keine Kristallkugel!
Da weder Du noch ich z. B. voraussagen können, was der jeweils Andere in Zukunft realisieren wird, wird Dir auffallen, dass auch das .NET-Team vor diesem Problem stand. Sie konnten weder wissen, dass ich eine Transportsoftware baue (und vor allem nicht, wie ich Diese objekt-logisch-technisch gestalten würde), noch, dass Du z. B. eine rein hypothetische Lern-Software baust.
Doch kein Problem?
Das Gute ist allerdings, dass das .NET-Team das auch gar nicht wissen, oder gar hellsehen musste. Alles was wir zusammenfassen können, könnten wir mit einem groben „Verhalten“ festhalten: „Wenn sich was ändert, muss gesagt werden, was sich geändert hat“ – mehr nicht. Genau dort greift letztendlich das „INotifyPropertyChanged“-Interface und füllt die Lücke!
Wie verwendet man INotifyPropertyChanged dann?
Um dies zu erklären, packe ich mal wieder meine überaus heftigen (#ironieaus) Paint-Skills aus. Schaue Dir einmal die Parteien/Teilnehmer im Bild hier drüber an, dort habe ich es vereinfacht dargestellt.
Aus dem realen Leben
Stell Dir vor, Du bietest über einen bekannten Online-Marktplatz einen Artikel zum Verkauf an. Nun findet ein Nutzer Deinen Artikel und er kauft Diesen auch. Aber was nun? Du musst den Artikel natürlich verschicken und hierzu gibst Du die Sendungsverfolgung an – Du beobachtest also anschließend (mehr oder weniger), was der Versanddienstleister Dir mitteilt.
Auf unser Beispiel bezogen, bist Du jetzt praktisch die agierende und die zuhörende Partei in Einem – in der Praxis wäre dies getrennt, aber aktuell egal. Du bekommst von den modernen Marktplätzen das verdiente Geld heutzutage nur ausgezahlt, wenn der Versanddienstleister die Zustellung des Paketes bestätigt. Ohne den signalisierenden Dienstleister würde der Marktplatz also nie Wind davon bekommen, dass das Paket angekommen ist.
Nochmal auf MVVM bezogen
Wie oben erwähnt, vermischten sich zumindest in diesem Beispiel verschiedene Rollen. Daher werde ich dies nun auf das Model-View-ViewModel-Entwurfsmuster übertragen. Also das, was man gängigerweise in der Entwicklung mit WPF und XAML verwendet.
Ein konkretes MVVM Beispiel
Hierzu steigen wir nun in den Code ein und erstellen ein reelles Beispiel. Im allerersten Schritt kannst Du dafür einfach ein WPF-Projekt (egal ob C# oder VB.NET) aufsetzen. Wenn Du dies getan hast, kannst Du nun einen Ordner namens „ViewModels“ im Projekt anlegen.
Das MainViewModel erstellen
Erstelle im angelegten Ordner eine Datei namens „MainViewModel“ und befülle Sie mit dem gleichen folgenden Code. Beachte hier, dass wir nicht ganz MVVM-konform sind, da ich hier keine Art Container namens „ShellView“ erzeuge und auch das „MainWindow“ nicht wirklich umbenenne (z. B. in MainView). Ich möchte den Fokus hier wirklich nur auf das Notwendigste halten.
Namespace ViewModels Public Class MainViewModel Public Property FirstName As String Sub New() FirstName = "Robert" End Sub End Class End Namespace
namespace ViewModels { public class MainViewModel { public string FirstName { get; set; } public MainViewModel() { FirstName = "Robert"; } } }
Den DataContext des Views setzen
Wir arbeiten an dieser Stelle wirklich nur mit den Bordmitteln und verzichten daher auf Zusatz-Frameworks wie „Caliburn.Micro“. Daher müssen wir uns auch selbst darum kümmern, dass das View ein passendenes ViewModel gesetzt bekommt. Dies erledigen wir in diesem Beispiel (sonst nach Präferenz) in der „Code behind“-Datei. Öffne dazu einfach das „MainWindow“, sodass Du den XAML-Code siehst und drücke F7. Dies sollte dann der (einzige) Code im View-Code sein:
' HIER DEINEN PROJEKTNAMEN/BASE-NAMESPACE EINTRAGEN! Imports <YOURPROJECTNAME>.ViewModels Class MainWindow Sub New() InitializeComponent() DataContext = New MainViewModel() End Sub End Class
using <YOURPROJECTNAME>.ViewModels; class MainWindow { public MainWindow() { InitializeComponent(); DataContext = new MainViewModel(); } }
Eine erste Darstellung
Um nun die Eigenschaft namens „FirstName“ aus dem „MainViewModel“ tatsächlich im „MainWindow“ darstellen zu können, müssen wir einfach eine für WPF übliche Datenbindung verwenden. Erstelle im MainWindow also gerne einen Textblock und wende die Datenbindung wie folgt an. Beachte, dass Du in der Zeile mit dem „:local“ Deinen Projektnamen verwendest und das „YOURPROJECTNAME“ ersetzt!
<Window x:Class="MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:YOURPROJECTNAME" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800" WindowStartupLocation="CenterScreen"> <Grid> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"> <TextBlock Text="{Binding FirstName}" /> </StackPanel> </Grid> </Window>
Das daraus resultierende Ergebnis kannst Du hier sehen (ich weiß, noch nicht wirklich interessant, aber hier geht es ja nicht um’s Design):
Was, wenn sich etwas ändert?
Nachdem Du im letzten Schritt das Basic-MVVM-Beispiel erstellt hast, können wir uns nun weiter an das „INotifyPropertyChanged“-Interface selbst herantasten. Dazu zeige ich Dir eine kleine Demonstration, Welche alles noch weiter verdeutlichen wird. Außerdem ändern wir im nächsten Schritt den Inhalt der „FirstName“-Eigenschaft im ViewModel.
Lass‘ uns nun einmal folgende Methode im MainViewModel definieren, beachte jedoch, dass ich dies hier nur als Demonstration des eigentlichen Anliegens mache. Man sollte keine Async Subs/Voids verwenden, da man damit Probleme mit der sogenannten „Statemachine“, bzw. dem „Exception-Stack“ erzeugt – aber.. anderes Thema.
Natürlich müssen wir die Methode auch aufrufen, sonst passiert schließlich nicht viel – dies tue ich im Konstruktor. Ca. 2 Sekunden nach der Ausführung des Programmes sollte sich nun die „FirstName“-Eigenschaft im ViewModel ändern, tut Sie aber scheinbar nicht – doch, tut Sie wohl 😉! Setze gerne einen Haltepunkt, wenn Du mir nicht glaubst, die Zeile wird ausgeführt!
' .... MainViewModel Sub New() FirstName = "Robert" ChangeAfterSeconds() End Sub Private Async Sub ChangeAfterSeconds() Await Task.Delay(2000) FirstName = "Hans" End Sub ' ....
// .... MainViewModel public MainViewModel() { FirstName = "Robert"; ChangeAfterSeconds(); } private async void ChangeAfterSeconds() { await Task.Delay(2000); FirstName = "Hans"; } // ....
Warum wird die Änderung nicht angezeigt?
Nun – woah – nachdem wir nun einige Basics wie die groben Zwecke von Interfaces und vielen anderen Dingen wiederholt haben, kommen wir (endlich, sorry) zum Punkt! Das View kennt zwar nun das zu sich passende ViewModel – genauer gesagt, den gesetzten DataContext, Welcher eine Instanz des MainViewModels ist. Es bekommt allerdings ohne weitere Hilfe nichts von den Änderungen an einer jeweiligen Eigenschaft mit.
Dies lösen wir nun mit dem INotifyPropertyChanged Interface!
Eine INotifyPropertyChanged Implementierung
Wie Du im letzten Abschnitt gesehen hast, hat sich die „FirstName“-Eigenschaft trotz Ausführung der Zeile augenscheinlich nicht geändert. Wir haben aber durch z. B. Einzelschritt-Debugging feststellen können, dass Diese sich sehr wohl ändert, jedoch eine Benachrichtigung darüber an die UI fehlt. Diese werden wir nun mit einer „INotifyPropertyChanged“-Implementierung realisieren.
Das PropertyChanged-Ereignis
Schreibe dazu einfach unter den Namen Deiner Klasse „Implements INotifyPropertyChanged“ und bestätige die Importierung des „System.ComponentModel“-Namespaces. Dann sollte auch direkt das „PropertyChanged“-Ereignis eingebunden werden. Und ja, was soll ich sagen, dies ist es, worum es im Kern geht!
Nach Ausführung der Auto-Vervollständigung sieht unser MainViewModel nun ungefähr so aus:
Imports System.ComponentModel Namespace ViewModels Public Class MainViewModel Implements INotifyPropertyChanged Public Property FirstName As String Sub New() FirstName = "Robert" ChangeAfterSeconds() End Sub Private Async Sub ChangeAfterSeconds() Await Task.Delay(2000) FirstName = "Hans" End Sub Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged End Class End Namespace
using System.Threading.Tasks; using System.ComponentModel; namespace ViewModels { public class MainViewModel : INotifyPropertyChanged { public string FirstName { get; set; } public DummyViewModel() { FirstName = "Robert"; ChangeAfterSeconds(); } private async void ChangeAfterSeconds() { await Task.Delay(2000); FirstName = "Hans"; } public event PropertyChangedEventHandler PropertyChanged; } }
Aber dennoch passiert irgendwie nichts? Ich kann mir schon Deine Gedanken vorstellen: „Aber Robert, Du hast doch gesagt, dass das dann geht.. :/“. Geduld junger Padawan, es bringt natürlich nichts, das Ereignis nur zu deklarieren, wir müssen es ja schließlich auch zur passenden Zeit auslösen!
Das Ereignis auslösen
Um nun eine Änderung von „FirstName“ an die Datenbindung der grafischen Oberfläche zu kommunizieren, müssen wir das „PropertyChanged“-Ereignis mit entsprechenden Daten auslösen. Wir haben mit dem Ereignis die Möglichkeit mitzuteilen, welche Eigenschaft sich geändert hat. Um nun zu sagen, dass sich „FirstName“ geändert hat, können wir es an passender Stelle so machen:
' ... MainViewModel FirstName = "Hans" RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(FirstName))) ' ...
// ... MainViewModel FirstName = "Hans"; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FirstName))); // ...
Nun können wir die Änderung tatsächlich (endlich) im View sehen. Bedenke hierbei auch, dass Du diese Vorgehensweise für sämtliche Eigenschaften verwenden kann! Booleans, Strings, oder komplexe Objekte, Du sagst hier schließlich nur „xy hat sich geändert“ – wer das dann wie rendert (z. B. mit einem DataTemplate), ist eine andere Sache!
Verbesserung 1 – Zentralisierung
Sicherlich kannst Du Dir jetzt schon denken, dass es ziemlich anstrengend sein kann, wenn Du nun überall, wo Du die „FirstName“-Eigenschaft änderst, das Ereignis auslösen musst. Keine Sorge, musst Du nicht, Du kannst es schließlich durch eine manuell implementierte Eigenschaft (statt der Automatischen) zentralisieren. Wir können also folgendes formulieren: „Immer wenn die Eigenschaft gesetzt wird *hust, Setter, hust*, dann müssen wir PropertyChanged auslösen“.
Dies könnte so aussehen:
' ... MainViewModel Private _firstName As String Public Property FirstName As String Get Return _firstName End Get Set (value as String) ' nur wenn sich wirklich was geändert hat.. If _firstName = value Then Return End If ' .. sagen, dass sich was geändert hat! _firstName = value RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(FirstName))) End Set End Property ' ...
// ... MainViewModel private string _firstName; public string FirstName { get { return _firstName; } set { // nur wenn sich wirklich was geändert hat.. if (_firstName == value) return; // .. sagen, dass sich was geändert hat! _firstName = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FirstName))); } } // ...
Nun kann die Eigenschaft ruhig aus verschiedenen Quellen gesetzt werden und trotzdem haben wir nur an einer zentralen Stelle die Auslösung des „PropertyChanged“-Ereignisses.
Verbesserung 2 – Vererbung der Funktionalität
Du kannst Dir nun sicherlich vorstellen, Welchen Aufwand wir haben, wenn wir (wie für die meisten Applikationen üblich) mehrere ViewModels haben. In jedem der ViewModels müssten wir das INotifyPropertyChanged implementieren und den Code immer und immer wieder schreiben – bäh!
PropertyChangedBase-Klasse
Deshalb kommen wir zu einer weiteren Verbesserung, wir vererben die erstellte Funktionalität einfach weiter und bauen uns sogar noch eine zentrale Methode zum Auslösen des „PropertyChanged“-Ereignisses. Erstelle also nun die folgende Klasse, in einem „Utils“ Unterordner Deines Projekts, von der Du dann anschließend in folgenden Projekten und ViewModels erben kannst. Beachte, dass Du Diese natürlich in den passenden Namespace packen solltest!
Imports System.ComponentModel Imports System.Runtime.CompilerServices Namespace Utils Public MustInherit Class PropertyChangedBase Implements INotifyPropertyChanged Protected Sub NotifyOfPropertyChange(<CallerMemberName> Optional propertyName As String = Nothing) RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName)) End Sub Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged End Class End Namespace
using System.Runtime.CompilerServices; using System.ComponentModel; namespace Utils; public abstract class PropertyChangedBase : INotifyPropertyChanged { protected void NotifyOfPropertyChange([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged; }
Die „NotifyOfPropertyChange“-Methode bietet uns hier neben dem einfachen Auslösen des Ereignisses auch noch weitere Vorteile. Wir verwenden das „CallerMemberName“-Attribut für die Erfassung des „Property“-Namens. Wir müssen dann nicht zwangsweise den Namen der Eigenschaft angeben, er erkennt die aufrufende Property automatisch. Nur wenn wir bei einer Änderung des Vornamens, z. B. die Änderung der darauf basierenden „FullName Readonly Property“ kommunizieren wollen würden, müssen wir den Namen explizit angeben.
Ein ViewModel mit der Basis-Klasse
Deine folgenden ViewModels könntest Du nun also wie folgt erstellen. Passe hierbei Dein View ggf. noch an, indem Du zwei weitere Textblöcke mit passenden Bindungen hinzufügst. Ich habe testweise 1 weitere Eigenschaften, namens „LastName“ und eine auf beiden vorherigen basierende Eigenschaft namens „FullName“ hinzugefügt.
Imports <YOURPROJECTNAME>.Utils Namespace ViewModels Public Class NewMainViewModel Inherits PropertyChangedBase Private _firstName As String Public Property FirstName As String Get Return _firstName End Get Set(value As String) If _firstName = value Then Return End If _firstName = value NotifyOfPropertyChange() NotifyOfPropertyChange(NameOf(FullName)) End Set End Property Private _lastName As String Public Property LastName As String Get Return _lastName End Get Set(value As String) If _lastName = value Then Return End If _lastName = value NotifyOfPropertyChange() NotifyOfPropertyChange(NameOf(FullName)) End Set End Property Public ReadOnly Property FullName As String Get Return $"{FirstName} {LastName}" End Get End Property Sub New() FirstName = "Robert" LastName = "Skibbe" ChangeAfterSeconds() End Sub Private Async Sub ChangeAfterSeconds() Await Task.Delay(2000) FirstName = "Hans" LastName = "Mustermann" End Sub End Class End Namespace
using <YOURPROJECTNAME>.Utils; namespace ViewModels { public class NewMainViewModel : PropertyChangedBase { private string _firstName; public string FirstName { get { return _firstName; } set { if (_firstName == value) return; _firstName = value; NotifyOfPropertyChange(); NotifyOfPropertyChange(nameof(FullName)); } } private string _lastName; public string LastName { get { return _lastName; } set { if (_lastName == value) return; _lastName = value; NotifyOfPropertyChange(); NotifyOfPropertyChange(nameof(FullName)); } } public string FullName { get { return $"{FirstName} {LastName}"; } } public NewMainViewModel() { FirstName = "Robert"; LastName = "Skibbe"; ChangeAfterSeconds(); } private async void ChangeAfterSeconds() { await Task.Delay(2000); FirstName = "Hans"; LastName = "Mustermann"; } } }