Training - Beratung - Projektarbeiten

www.David-Tielke.de

Richtiges setzen von Werten in Eigenschaften mit PropertyChanged

Wie in dem letzten Beitrag “OnPropertyChanged 2.0” gezeigt, gibt es qualitativ besser Möglichkeiten die INotifyPropertyChanged – Schnittstelle zu implementieren, als mit einer eventauslösenden Methode mit einem normalen string-Parameter.

Die im letzten Beispiel gezeigte Auslösung, war wie folgt implementiert:

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

Unnötiges Setzen von Feldern

In Praxisprojekten ist diese Art der Implementierung allerdings nicht wirklich optimal, da zuvor eine Prüfung erfolgen muss, ob sich der Wert des zugrunde liegenden Feldes überhaupt geändert hat. Ist dies nicht der Fall, macht natürlich weder das Sssetzen des Feldes noch ein Auslösen des PropertyChanged-Events Sinn. Daher bietet sich eine Implementierung wie folgt an:

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

Damit ist sichergestellt, das dass PropertyChanged-Event nur dann ausgelöst wird, wenn der gesetzte Wert sich auch tatsächlich geändert hat.

Nun gibt es in einer Entitätsklasse normalerweise mehr als nur eine Eigenschaft, welche den Benachrichtigungsmechanismus von INotifyPropertyChanged nutzt und implementiert. Folgt man dem DRY-Prinzip muss der Überprüfungscode und das Auslösen des Events in eine separate Methode ausgelagert werden. In Schritt 1 lagern wir Logik zur Überprüfung von Gleichheit aus:

private void SetValue<T>(ref T field, T value)
{
    if (field.Equals(value))
    {
        return;
    }

    field = value;
}

Dabei gibt er erste Parameter das zu setzende bzw. zu überprüfende Feld an und der zweite Parameter den von außen gesetzten Wert. Wichtig hierbei ist es, den ersten Parameter als ref zu kennzeichnen, da wir in der Lage sein müssen, den Wert des zugrunde liegenden Feldes zu verändern. Sowohl für Werte- als auch für Referenztypen erreicht man dieses Verhalten bei Methodenaufrufen mit dem ref-Schlüsselwort.

Benachrichtigungsmechanismus

Als nächstes kümmern wir uns um die Benachrichtigung mit dem PropertyChanged - Event:

private void SetValue<T>(ref T field, T value, 
	[CallerMemberName] string propertyName = null)
{
    if (field.Equals(value))
    {
        return;
    }

    field = value;
    OnPropertyChanged(propertyName);
}

Erneut nutzen wir das CallerMemberName-Attribut und ermitteln den Aufrufer. Dieser wird dann an die OnPropertyChanged-Überladung aus dem "OnPropertyChanged 2.0"-Beispiel übergeben. Aber wie dort schon gezeigt, ist es oftmals erforderlich die Eigenschaft, für die das PropertyChanged-Event ausgelöst wird, explizit anzugeben. Daher überladen wir die SetValue-Methode mit einem Expression-Objekt anstatt dem string-Paramter:

private void SetValue<T>(ref T field, T value, Expression<Func<T>> propertyExpression)
{
    if (field.Equals(value))
    {
        return;
    }

    field = value;
    OnPropertyChanged(propertyExpression);
}

Nun existiert die Logik zur Überprüfung in beiden Methoden, wir refaktorisieren das Ganze und extrahieren daraus eine Methode. Die Methoden zum gleichzeitigen Auslösen des Events werden mit dem Suffix AndNotify-versehen. Zum einen würde der Name SetValue nicht die tatsächliche Aktion beschreiben und weiterhin würden wir so Probleme bekommen, da wir dann nicht mehr an die Überladung mit dem optionalen string-Parameter kommen würden. Damit erhalten wir folgende drei Methoden:

private void SetValue<T>(ref T field, T value)
{
    if (field.Equals(value))
    {
        return;
    }

    field = value;
}

private void SetValueAndNotify<T>(ref T field, T value, 
	Expression<Func<T>> propertyExpression)
{
    SetValue(ref field, value);
    OnPropertyChanged(propertyExpression);
}

private void SetValueAndNotify<T>(ref T field, T value, 
	[CallerMemberName] string propertyName = null)
{
    SetValue(ref field, value);
    OnPropertyChanged(propertyName);
}

Diese können wir nun nutzen um Werte in Eigenschaften mit Überprüfung zu setzen und gleichzeitig das PropertyChanged-Event auszulösen:

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

Gemeinsame Basisklasse

Da es für gewöhnlich ja mehr als eine Entitätsklasse in einem Projekt gibt, wäre die Duplizierung der o.g. Methoden mal wieder ein Verstoß gegen das DRY-Prinzip. Daher können die Methoden in eine gemeinsame Basisklasse ausgelagert werden. Da Entitäten normalerweise keine Basisklassen verwenden, kann dieses Verfahren in nahezu jedem Projekt eingesetzt werden:

internal class AbstractEntityBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

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

    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);
    }

    private void SetValue<T>(ref T field, T value)
    {
        if (field.Equals(value))
        {
            return;
        }

        field = value;
    }

    protected void SetValueAndNotify<T>(ref T field, T value, Expression<Func<T>> propertyExpression)
    {
        SetValue(ref field, value);
        OnPropertyChanged(propertyExpression);
    }

    protected void SetValueAndNotify<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        SetValue(ref field, value);
        OnPropertyChanged(propertyName);
    }
}

Da diese Basisklasse nur Abhängigkeiten zum Framework selbst enthält, bietet es sich an, die Klasse in eine Bibliothek auszulagern, die in anderen Projekten wiederverwendet werden kann.

Zusammenfassung

Das stupide Setzen von zugewiesenen Werten in Eigenschaften sollte aus Performancegründen unterlassen werden und stattdessen eine Überprüfung durchgeführt werden, ob der gesetzte Werte sich überhaupt von dem Wert im zugrundeliegenden Feld unterscheidet. Diese Überprüfung und die eigentliche Zuweisung verstoßen gegen das DRY-Prinzip und sollten nach Möglichkeit ausgelagert werden. Wird dieser Aufwand eh betrieben, kann auf diesem Wege auch direkt der Benachrichtigungsmodus mit eingebunden werden. Gibt es mehr als eine Entitätsklasse, greift auch hier wieder das DRY-Prinzip und die beschriebenen Methoden können in eine gemeinsame Basisklasse ausgelagert werden.

Kommentare (10) -

  • Jürgen Gutsch

    24.07.2013 11:12:19 | Antwort

    Hallo David,

    schöne Lösung und schöner Beitrag Smile Gilt auch für den letzten zum Thema.

    Ein Punkt stört mich allerdings etwas, zumindest im XAML- und MVVM-Umfeld:
    (Könnte hier allerdings auch nur Wortklauberei sein, weil du von "Entitäten" schreibst, ich im MVVM-Umfeld aber "Models" nutze *fg*)
    In der Regel vermeide ich es Entities an die View zu binden, sondern nutze dafür ein eigenes für die View erstelltes Model. So hat nur dieses (direkt oder indirekt ist egal) INotifyPropertyChanged implementiert.
    Die Entities bleiben dann saubere POCOs, die von der Datenquelle kommen, ohne weitere Abhängigkeiten.

    Die Basisklasse als solche für Models ist sehr gut. MVVM-Anfängern empfehle ich aber, die Basisklasse der MVVM-Frameworks zu nutzen, die nicht ganz so bequem sind und die Prüfung auf eine tatsächliche Änderung "bewusst" und von Hand zu machen. Das widerspricht zwar dem DRY-Prinzip, aber die Leute denken mehr darüber nach wann und wie Notification genutzt wird. Das kommt natürlich auch ein bisschen auf die Art und Komplexität der Software (vor allem der Views) an.

    • David

      22.08.2013 22:27:41 | Antwort

      Hallo Jürgen,

      Ja in der Tat nutze ich das Wort Entität hier etwas weiträumig, in deinem Fall wären es natürlich die Models Smile

      Was den Punkt mit den Anfängern angeht stimme ich dir absolut zu, diese Methode hier "abstrahiert" dieses Problem derart, das viele Anfänger nicht mehr darüber nachdenken was dort passiert.

  • Golo Roden

    24.07.2013 14:33:07 | Antwort

    Das stupide Setzen von zugewiesenen Werten in Eigenschaften sollte aus Performancegründen unterlassen werden und stattdessen eine Überprüfung durchgeführt werden, ob der gesetzte Werte sich überhaupt von dem Wert im zugrundeliegenden Feld unterscheidet. Diese Überprüfung und die eigentliche Zuweisung verstoßen gegen das DRY-Prinzip und sollten nach Möglichkeit ausgelagert werden.

    Sicher, dass Performance da die richtige Begründung ist?

    Ich habe es nun nicht nachgemessen, aber ich würde doch mal sehr stark davon ausgehen, dass das stupide Setzen eines Wertes deutlich schneller geht, als eine zusätzliche Überprüfung, die zudem noch in eine gesonderte Methode ausgelagert wurde.

    Oder übersehe ich da etwas?

    • David

      22.08.2013 22:29:56 | Antwort

      Hey Golo,
      ja du übersiehst etwas, es erfolgt ja noch eine Benachrichtigung durch INotifyPropertyChanged. Je nachdem wer als Listener an dem Objekt hängt, kann daraus z.B. der Redraw eines Controls resultieren, obwohl der Wert sich nicht geändert hat. Und dann würde ich es schon unperformant nennen.

      Gruß David

  • Rene Hilgers

    24.07.2013 19:01:03 | Antwort

    Meiner Meinung nach eine schöne Lösung. Wer das ganze auch unter .net 4 nutzen möchte (oder muss wegen XP Frown ),  der sollte das Microsoft BCL Portability Pack (via NuGet "Install-Package Microsoft.Bcl") installieren. Dann steht das [CallerMemberName]-Attribut auch in .net 4 zur Verfügung. Smile

    Ist es gewollt, dass in den beiden SetValueAndNotify-Methoden grundsätzlich OnPropertyChanged aufgerufen wird, auch wenn SetValue  mit return verlassen wurde, weil keine Änderung gemacht wurde?


    • David

      22.08.2013 22:31:20 | Antwort

      Hey Rene,

      nein ist nicht gewollt, du hast vollkommen recht. Da muss noch einmal nachgebessert werden. Danke für den Hinweis!

      Gruß David

  • Rene Hilgers

    26.07.2013 19:48:58 | Antwort

    Die Lösung gefällt mir sehr gut. Ich finde es vorteilhaft, wenn  man nicht mehr den Eigenschaftsnamen als String angeben muss.
    Für diejenigen, die das ganze auch unter .net 4 machen wollen (oder wegen XP noch müssen),  die sollten das Microsoft BCL Portability Pack via NuGet ("Install-Package Microsoft.Bcl") installieren.

    In der AbstractEntityBase-Klasse wird in den SetValueAndNotify-Methoden ummer OnPropertyChanged aufgerufen. Sollte OnPropertyChanged nicht nur aufgerufen werden, wenn ein Änderung der Eigenschaft stattgefunden hat (wie es ganz oben gemacht wird)?

  • Robert Glaubauf

    08.08.2013 21:01:41 | Antwort

    Ich denke mir, wenn es nur um die Zuweisung ginge, könnte man aus Performancegründen tatsächlich auf eine vorhergehende Überprüfung verzichten. Aber hier soll ja nicht nur zugewiesen, es soll ja auch informiert werden. D.h. das wahrscheinlich irgendwo ein EventHandler darauf wartet angestoßen zu werden. Und es könnte wiederum sein, dass dieser Handler einiges zu tun hat - was er nicht zu tun braucht, wenn der Wert sich ja eigentlich gar nicht geändert hat.

    Natürlich könnte jetzt auch wieder der EventHandler darauf schauen, ob sich der Wert geändert hat, bevor er weitermacht. In diesem Fall haben wir dann aber zusätzlich zum Werte-Vergleich auch noch den Eventaufruf und seine Rückkehr als Performancebremse.

    Zumindest für mich war die Ausführungszeit der PropertyChangedHandler der Aspekt, vor der Zuweisung in Properties die Wertänderung zu überprüfen.

    • David

      22.08.2013 22:32:26 | Antwort

      Hey Robert,

      Richtig, genau darauf wollte ich hinaus.

      Gruß David

  • David

    22.08.2013 22:25:11 | Antwort

    Hallo,

    entschuldigt erst einmal, dass ich die Kommentare hier nicht freigeschaltet habe. Irgendwie wurde ich nicht darüber benachrichtigt. Keine Ahnung warum nicht *grübel*...

Kommentar schreiben

Loading