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.

Ein internal Interface in C# implementieren ohne public Methoden

Ab und zu kommt man in die Situation, dass man ein Interface in C# sowohl für public-Zwecke (Verwendung außerhalb eines Projekts) und auch für internal-Zwecke (Verwendung in dem jeweiligen Projekt) verwendet. Dabei gibt es das Problem, dass in C# keine Zugriffsmodifizierer für Interfacemethoden zulässig sind. Wie man das Ziel trotzdem erreicht, zeigt das Listing 1.

    public interface IPublic
    {
        void Foo();
    }

    internal interface IInternal : IPublic
    {
        void Bar();
    }

    class Something : IInternal
    {
        public void Foo()
        {
            throw new NotImplementedException();
        }

        void IInternal.Bar()
        {
            throw new NotImplementedException();
        }
    }

Listing 1: Das implementieren eines internal und eines public Interfaces

Der Trick daran ist es, die Funktionalität in ein Public Interface (hier IPublic) und ein Internal Interface (hier IInternal) aufzuteilen. Da IInternal von IPublic abgeleitet ist, bietet die interne Schnittstelle alle angebotenen Methoden an und die öffentliche Schnittstelle nur die öffentlich zugänglichen. Aber warum dann nicht in ein Interface? Ganz einfach, in einem Interface kann nur der Zugriffsmodifizierer für das gesamte Interface angegeben werden, nicht für einzelne Methoden oder Eigenschaften.

Weiter ist zu beachten das wenn ein Interface implizit implementiert wird, müssen dadurch alle implementierten Methoden public sein, egal welchen Zugriffsmodifizierer das Interface hat. Daher wird die Methode Bar() aus dem Interface IInternal explizit implementiert, womit wir das gewünschte Verhalten erzielt wird.

Ist der .NET-Entwickler der neue VB6-Entwickler?

Etwas mehr als 12 Stunden sind seit der BUILD vergangen… Die Konferenz die endlich Klarheit bringen sollte, die uns Entwicklern die Marschrichtung für die Zukunft aufzeigen sollte. Das Gefühl war schon irgendwie beängstigend am Vormittag, die Nervosität bei vielen Leuten spürbar. Durch die Verschwiegenheit von Microsoft im Vorfeld wurden die wildesten Gerüchte gestreut: Wird HTML5&JS die neue primäre Sprache für Windowsentwicklung? Wird es .NET weiterhin in seiner jetzigen Form geben? Haben wir viele Jahre eine Technologie gelernt, die nun veraltet ist?

Die Keynote begann, Windows 8 wurde gezeigt und dann war es raus: Es wird eine neue Laufzeitumgebung geben, genannt Windows RT, welche für die Entwicklung von Windows 8 Apps genutzt wird. Diese Runtime kann mit diversen Sprachen genutzt werden, neben HTML/JS und C++ glücklicherweise auch C#… Das HTML gesetzt war, vermuteten viele bereits, aber eine wirkliche Bekenntnis zu C# und zu .NET gab Microsoft bis dato nicht. Aber ist jetzt alles gut für das .NET Framework? Nein – ich denke nicht!

Wirklich viele Informationen über die nächste .NET Version gab es ja nicht. Bei der Sessionlist der BUILD ist mir folgende Session aufgefallen, auf die ich meine Argumentation stütze:

image

Das .NET-Framework wird also neue Erweiterungen bekommen, um Anwendungen für Windows 8 zu schreiben, bzw. die Windows RT aus .NET heraus nutzen zu können. Das man neue Erweiterungen für ein Betriebssystem bereitstellt, kennen wir bereits von Windows 7. Allerdings wurde das damals als “Windows 7 API Code Pack” bereitgestellt und nicht als Bestandteil einer neuen Version von .NET zu Verfügung gestellt.

Das Konzept von Microsoft war es (einmal!?), ein Framework für alle Windowsplattformen zur Verfügung zu stellen. Damit war sichergestellt, das Anwendungen, die auf einer Frameworkversion auf Betriebssystem X laufen, auch auf einem Betriebssystem Y läuft, vorausgesetzt es existiert eine Runtime dazu. Sollte Microsoft also wirklich die API für Windows 8 in .NET integrieren, wäre diese Plattformunabhängigkeit Geschichte. Macht man sich da das eigene Konzept kaputt?

Schaut man sich die Sessions zur BUILD an, wird eines deutlich: Der Kernpunkt dieser Konferenz ist neben Windows 8 vor allem HTML5 und JavaScript.

  • 9 Sessions zu .NET
  • 7 Sessions zu ASP.NET
  • 7 Sessions zu C#
  • 40 (!!!) Sessions zu HTML5

Merkwürdig das HTML5 als Technologie die langjährig gefeierte Technologie .NET mit einer gigantischen Entwicklergemeinde zu überholen scheint. Schaut man sich die Sessions im Bereich .NET, ASP.NET und C# an, mangelt es an wirklichen Innovationen und es erweckt den Eindruck, das diese Frameworkversion einzig und allein für die Kompatibilität mit Windows 8 entwickelt wurde und um die Brücke für ASP.NET Entwickler hin zu HTML5 Metro UI zu schlagen.

Wir sind bald im zehnten Jahr des .NET-Zeitalters angekommen. In dieser Zeit hat sich viel Verändert, das Framework wurde massiv erweitert und um coole und innovative Features ergänzt, wie z.B. LINQ. Wir haben mit WPF eine UI-Technologie die in der Lage ist, hardwarebeschleunigte Oberflächen zu entwickeln die schick und modern aussehen. Nun hat Microsoft mit Windows 8 ihr Flaggschiff zurück auf 0 gestellt (mehr oder weniger), und so viele Bereiche neu entwickelt wie bei keinem Betriebssystem vorher. Aber womit wurden die Oberfläche von Windows entwickelt? Mit HTML5 und JavaScript! Wurde das Betriebssystem auf Basis von managed Code geschrieben? Nein, aber warum? Das Forschungsbetriebssytem Singularity von Microsoft Research hat doch gezeigt, das ein performantes managed OS möglich ist und zahlreiche Vorteile hat. Welche großen Microsoftprodukte gibt es den, die mit .NET umgesetzt wurden, wenn wir Sharepoint einmal außen vor lassen? Keins!

Diese Tatsache zeigt mir, das Microsoft selbst nicht mehr an eine Zukunft von .NET glaubt und diese Version 4.5 des Frameworks untermauert das Ganze noch einmal. Egal mit wem man im Vorfeld gesprochen hat und über ein mögliches AUS von .NET diskutierte, jeder entgegnete mit der selben Antwort “Microsoft wird doch keine Technologie sterben lassen, in die sie so viele Jahre so viel Geld investiert haben”. Aber haben die VB6 Entwickler das nicht früher vielleicht auch behauptet bevor .NET veröffentlicht wurde? Ich befürchte die .NET Entwickler werden die neuen VB6 Entwickler…