Training - Beratung - Projektarbeiten

www.David-Tielke.de

Webcast: Logging in .NET-Anwendungen

Im Rahmen eines Webinars der Developer Media zum Thema “Softwarequalität für Fortgeschrittene”, hatte ich den Themenplan etwas zu optimistisch verfasst und so musste ein wichtiges Thema leider ausfallen: Logging. Wie versprochen reiche ich das Thema nun als Webcasts nach.

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.

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.