Training - Beratung - Projektarbeiten

www.David-Tielke.de

OnPropertyChanged 2.0

Für all diejenigen die ihre Entity-Objekte mit einer Schnittstelle zur Änderungsbenachrichtigung ausstatten möchten, bietet sich das Frameworkinterface INotifyPropertyChanged an. Dieses spatanisch definierte Interface hat außer einem Event nicht viel zu bieten:

public interface INotifyPropertyChanged
{
  event PropertyChangedEventHandler PropertyChanged;
}

Das PropertyChanged Event wird immer dann ausgelöst, wenn sich die Eigenschaft eines Objektes verändert hat. Bindet man nun ein UI-Element an die Eigenschaft X solch eines Objektes, abonniert die UI gleichzeitig auch das PropertyChanged-Event und horcht darauf, ob sich die Eigenschaft X geändert hat, an die es gebunden wurde. Wird das Event für die Eigenschaft X ausgelöst, kann die UI den neuen Wert aus der Eigenschaft auslesen und darstellen.

Wie in .NET üblich lösen wir die Methode nicht direkt aus, sondern über eine sogn. eventauslösende Methode:

protected virtual void OnPropertyChanged(string propertyName)
{
    var del = PropertyChanged;
    if (del != null)
    {
        del(this, new PropertyChangedEventArgs(propertyName));
    }
}

Damit haben wir einen funktionierenden Benachrichtigungsmechanismus implementiert und können bei Veränderung einer Eigenschaft darauf zurückgreifen:

public int X
{
    get { return _x; }
    set
    {
        _x = value;
        OnPropertyChanged("X");
    }
}

Das funktioniert und wurde genau so schon etliche Male von vielen Entwicklern so gelöst.

Leider ist die Lösung nicht wirklich optimal, da der Name der Eigenschaft als String übergeben wird. Daraus ergeben sich einige Nachteile:

Kein IntelliSense zur Entwicklungszeit. Auch wenn es in diesem Beispiel nicht wirklich ersichtlich ist, kann sich bei längeren Eigenschaftennamen schon mal ein Fehler einschleichen der natürlich nicht vom Compiler erkannt werden kann. Daraufhin wird zwar das Event gefeuert, allerdings mit dem falschen Namen. Das hat zur folge das alle behandelnden Objekte das Event ignorieren, da sie sich nicht an AnzhalPersonen sondern an AnzahlPersonen gebunden haben. Leider ist dies eine der Dinge,  die schwierig zu debuggen sind und immer unnötig viel Zeit zum beheben erfordern.

Darüber hinaus kann die “Rename and replace”-Option in Visual Studio nicht genutzt werden. Wird die Eigenschaft X umgenannt in Y, so ändert sich natürlich der string “X” nicht. Daher kann man schnell, durch die Umbenennung einer Eigenschaft, ein bestehenden Programm unbrauchbar machen. Auch hier fällt der Fehler nicht zur compile-time auf, sondern erst, wenn plötzlich ein Objekt die Änderungen nicht mehr darstellt.

CallerMemberName-Attribute

Wie lösen wir das ganze nun? Seit.NET 4.5 gibt es das CallerMemberName-Attribut welches sich in System.Runtime.CompilerServices befindet. Wird dieses Attribut vor einen Methodenparameter von Typ string geschrieben, wird beim Aufruf der Methode, der Name der aufrufenden Methode an den Parameter übergeben. Damit kann unsere OnPropertyChanged-Methode wie folgt abgeändert werden:

protected virtual void OnPropertyChanged([CallerMemberName] string propertyName=null)
{
    var del = PropertyChanged;
    if (del != null)
    {
        del(this, new PropertyChangedEventArgs(propertyName));
    }
}

Die eventauslösende Methode hat nun einen optionalen Parameter. Wird dieser ohne Argument aufgerufen, wird automatisch der Name des Aufrufers dem Parameter propertyName übergeben. Wird jedoch ein Parameter von Außen übergeben, bleibt die Funktionalität wie zuvor. Deshalb eignet sich diese Lösung hervorragend für die Migration aus altem Code heraus.

Wir ändern unsere Eigenschaft X wie folgt ab und nutzen fortan unsere neue Implementierung:

public int X
{
    get { return _x; }
    set
    {
        _x = value;
        OnPropertyChanged();
    }
}

Lambda-Expressions

Die Lösung mit dem CallerMemberName-Attribut ist schon um einiges besser, aber oftmals muss nicht nur ein Event für die aktuelle Eigenschaft ausgelöst werden, sondern auch für andere Eigenschaften. Nehmen wir mal folgendes Beispiel:

public int Y
{
    get
    {
        return X + 10;
    }
}

Y ist ein computed property, hat also keinen Setter. Damit haben wir auch keine Möglichkeit dort das Event zu feuern, die Änderungsbenachrichtigung ist alleine von X abhängig. Daher muss für die Eigenschaft Y das Event auch in X gefeuert werden. Nun könnte man selbstverständlich die Überladung mit dem string-Parameter nehmen, hätte dann allerdings wieder die beschriebenen Nachteile. Die Lösung ist ein Expression-Objekt:

protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
    var body = propertyExpression.Body as MemberExpression;
    var expression = body.Expression as ConstantExpression;
    OnPropertyChanged(body.Member.Name);
}

Damit kann anstelle eines strings ein Lambda-Ausdruck als Parameter angegeben werden, für den es IntelliSense Unterstützung gibt und welcher bei der Änderung des Eigenschaftennamens ebenfalls von Visual Studio berücksichtigt wird.

Das Auslösen der beiden Events sieht damit wie folgt aus:

public int X
{
    get { return _x; }
    set
    {
        _x = value;
        OnPropertyChanged();
        OnPropertyChanged(() => this.Y);
    }
}

Zusammenfassung

Das Auslösen des PropertyChanged-Events mit string-Objekten ist ein hohes Risiko zur Entwicklungszeit und bei späteren Veränderungen innerhalb der Software. Je nachdem welche Anforderungen an die Eventauslösende Methode gestellt werden, bietet sich die Lösung mit dem CallerMemberName-Attribut oder einem Expression-Objekt an. Während ersteres nur ab .NET 4.5 funktioniert und auf das Auslösen des Events für die jeweils aktuelle Eigenschaft begrenzt ist, funktioniert die Lösung mit dem Expression-Objekt ab .NET 3.5 und kann von jeder Eigenschaft für jede Eigenschaft ausgelöst werden.

Kommentare (2) -

  • David

    22.08.2013 22:33:10 | Antwort

    Cool Smile Danke Christopher.

Pingbacks and trackbacks (1)+

Kommentar schreiben

Loading