Juli 2016

Band 31, Nummer 7

Datenbindung – Eine bessere Möglichkeit der Implementierung der Datenbindung in .NET

Von Mark Sowul

Datenbindung ist eine leistungsstarke Technik beim Entwickeln von Benutzeroberflächen: Sie erleichtert das Trennen der Ansichtslogik von der Geschäftslogik sowie das Testen des resultierenden Codes. Zwar ist Datenbindung von Anfang an ein Bestandteil von Microsoft .NET Framework gewesen, ihre Bedeutung hat jedoch mit dem Erscheinen von Windows Presentation Foundation (WPF) und XAML noch zugenommen, da sie das Bindeglied zwischen „View“ und „ViewModel“ im MVVM-Muster darstellt (Model-View-ViewModel).

Der Pferdefuß beim Implementieren von Datenbindung war immer die Notwendigkeit von „Magic Strings“ und Codebausteinen, um einerseits Änderungen an den Eigenschaften zu übertragen und andererseits Elemente der Benutzeroberfläche an sie zu binden. Im Lauf der Jahre wurden eine Reihe von Toolkits und Techniken zur Linderung der Probleme verfügbar; dieser Artikel hat es sich zum Ziel gesetzt, den Prozess noch weiter zu vereinfachen.

Zunächst möchte ich die Grundlagen beim Implementieren von Datenbindung sowie einige gängige Techniken zur Vereinfachung durchgehen (wenn Ihnen das Thema bereits vertraut ist, können Sie diese Abschnitte überspringen). Anschließend lege ich eine Technik dar, die Sie möglicherweise nicht in Betracht gezogen haben („Ein dritter Weg“) und stelle Lösungen für damit zusammenhängende Entwurfsprobleme beim Entwickeln von Anwendungen vor, die MVVM verwenden. Sie erhalten die abschließende Version des Frameworks, das ich hier darlege, im begleitenden Codedownload oder können Ihren eigenen Projekten das NuGet-Paket „SolSoft.DataBinding“ hinzufügen.

Grundlagen: INotifyPropertyChanged

Die Implementierung von INotifyPropertyChanged stellt das bevorzugte Verfahren dar, um die Bindung eines Objekts an eine Benutzeroberfläche zu ermöglichen. Sie ist recht einfach und enthält nur ein Element: das Ereignis PropertyChanged. Das Objekt soll dieses Ereignis auslösen, wenn sich eine bindungsfähige Eigenschaft ändert, um die Ansicht zu benachrichtigen, dass sie ihre Darstellung des Werts der Eigenschaft aktualisieren muss.

Die Schnittstelle ist einfach, ihre Implementierung jedoch nicht. Das manuelle Auslösen des Ereignisses mithilfe von hartcodierten Namen von Texteigenschaften ist eine Lösung, die nicht gut skaliert und sich auch nicht für Refactoring eignet: Sie müssen peinlich genau dafür sorgen, dass die Textnamen mit den Eigenschaftsnamen im Code synchron bleiben. Damit werden Sie sich bei Ihren Nachfolgern kaum beliebt machen. Im Folgenden finden Sie ein Beispiel:

public int UnreadItemCount
{
  get
  {
    return m_unreadItemCount;
  }
  set
  {
    m_unreadItemCount = value;
    OnNotifyPropertyChanged(
      new PropertyChangedEventArgs("UnreadItemCount")); // Yuck
  }
}

Eine Reihe von Techniken wurden als Reaktion auf diese Herausforderung entwickelt, um die Integrität zu wahren (sehen Sie sich beispielsweise die Frage in Stack Overflow unter bit.ly/24ZQ7CY an); die meisten davon lassen sich unter einem von zwei Typen subsumieren.

Beliebte Technik 1: Basisklasse

Eine Möglichkeit, die Situation zu vereinfachen, ist eine Basisklasse, um einen Teil der Logik des Codebausteins erneut zu verwenden. Dadurch stehen auch einige Wege offen, den Eigenschaftsnamen programmgesteuert abzurufen, statt ihn hart codieren zu müssen.

Abrufen des Eigenschaftsnamens mit Ausdrücken: Seit .NET Framework 3.5 sind Ausdrücke verfügbar, mit denen eine Untersuchung der Codestruktur zur Laufzeit möglich ist. LINQ verwendet diese API mit großer Wirkung, um beispielsweise .NET LINQ-Abfragen in SQL-Anweisungen zu übersetzen. Einfallsreiche Entwickler haben diese API außerdem zum Untersuchen von Eigenschaftsnamen genutzt. Beim Einsatz einer Basisklasse für die Durchführung dieser Untersuchung könnte der vorstehende Setter in dieser Form umgeschrieben werden:

public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged(() => UnreadItemCount);
}

Dadurch wird dann beim Umbenennen von UnreadItemCount auch der Ausdrucksverweis umbenannt, sodass der Code nach wie vor funktioniert. Die Signatur von RaiseNotifyPropertyChanged wäre dann wie folgt:

void RaiseNotifyPropertyChanged<T>(Expression<Func<T>> memberExpression)

Es bestehen verschiedene Techniken zum Abrufen des Eigenschaftsnamens aus memberExpression. Der C#-MSDN-Blog unter bit.ly/25baMHM zeigt ein einfaches Beispiel:

public static string GetName<T>(Expression<Func<T>> e)
{
  var member = (MemberExpression)e.Body;
  return member.Member.Name;
}

StackOverflow bietet unter bit.ly/23Xczu2 eine umfangreichere Auflistung. In jedem Fall hat diese Technik einen Nachteil: Das Abrufen des Ausdrucksnamens erfolgt mithilfe von Reflexion, und Reflexion ist langsam. Der Mehraufwand an Leistung kann erheblich sein, je nach der auftretenden Menge der Änderungsbenachrichtigungen für Eigenschaften.

Abrufen des Eigenschaftsnamens mit CallerMemberName: Mit C# 5.0 und .NET Framework 4.5 wurde ein weiteres Verfahren zum Abrufen des Eigenschaftsnamens verfügbar, nämlich unter Verwendung des CallerMemberName-Attributs (mithilfe des NuGet-Pakets „Microsoft.Bcl“ steht dieser Weg auch in älteren Versionen von .NET Framework offen). Dabei wird die ganze Arbeit vom Compiler erledigt, daher entsteht zur Laufzeit kein Mehraufwand. Bei diesem Ansatz erhält die Methode diese Form:

void RaiseNotifyPropertyChanged<T>([CallerMemberName] string propertyName = "")
And the call to it is:
public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged();
}

Das Attribut weist den Compiler an, den Aufrufernamen UnreadItemCount als Wert des optionalen Parameters propertyName einzusetzen.

Abrufen des Eigenschaftsnamens mit nameof: Das CallerMemberName-Attribut wurde vermutlich eigens für diesen Zweck erstellt (Auslösen von PropertyChanged in einer Basisklasse), in C# 6 hat das Compilerteam aber endlich etwas eingeführt, das sich in einem viel weiteren Kontext verwenden lässt: das Schlüsselwort „nameof“. Nameof ist für viele Zwecke nützlich; wenn in diesem Fall der ausdrucksbasierte Code durch nameof ersetzt wird, bleibt wieder die ganze Arbeit beim Compiler (kein Mehraufwand zur Laufzeit). Dabei sollte bedacht werden, dass es sich hier ausschließlich um ein Feature der Compilerversion handelt, nicht um ein Feature der .NET-Version: Diese Technik kann auch mit .NET Framework 2.0 als Zielplattform eingesetzt werden. Allerdings müssen Sie (und alle Mitglieder Ihres Teams) mindestens Visual Studio 2015 verwenden. Bei Verwendung von nameof ergibt sich nun:

public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged(nameof(UnreadItemCount));
}

Allerdings tritt bei allen Techniken, die auf einer Basisklasse aufbauen, ein allgemeines Problem auf: Sie ruinieren die Basisklasse („burn the base class“ im US-Originaltext). Wenn Sie Ihr Ansichtsmodell auf eine andere Klasse ausweiten möchten, haben Sie Pech gehabt. Und die bewirken auch nichts in der Behandlung von „abhängigen“ Eigenschaften (z. B. eine Eigenschaft FullName, die aus FirstName und LastName verkettet ist: Jede Änderung an FirstName oder LastName muss auch eine Änderung von FullName auslösen).

Beliebte Technik 2: Aspektorientierte Programmierung

Aspektorientierte Programmierung (AOP) führt im Wesentlichen eine „Nachverarbeitung“ des kompilierten Codes aus, entweder zur Laufzeit oder in einem nach der Kompilierung erfolgenden Schritt, um bestimmte Verhalten (die als „Aspekt“ bezeichnet werden) hinzuzufügen. Normalerweise verfolgt dies das Ziel, wiederkehrende Codebausteine zu ersetzen, wie etwa bei der Protokollierung oder Ausnahmebehandlung (so genannte „cross-cutting concerns“). Wie nicht weiter überraschend, stellt die Implementierung von INotifyPropertyChanged einen geeigneten Fall dar.

Für diesen Ansatz ist eine Reihe von Toolkits verfügbar. PostSharp ist einer davon (bit.ly/1Xmq4n2). Ich konnte angenehm überrascht feststellen, dass es abhängige Eigenschaften (z. B. die zuvor beschriebene FullName-Eigenschaft) ordnungsgemäß behandelt. Ein Open Source-Framework mit dem Namen Fody ist ähnlich (bit.ly/1wXR2VA).

Dies ist ein attraktiver Ansatz; seine Nachteile sind möglicherweise unerheblich. Einige Implementierungen fangen das Verhalten zur Laufzeit ab, wodurch die Leistung beeinträchtigt wird. Die nach der Kompilierung ansetzenden Frameworks sollten demgegenüber keinen Mehraufwand zur Laufzeit mit sich bringen, es kann jedoch ein gewisser Installations- oder Konfigurationsaufwand erforderlich werden. PostSharp wird zurzeit als Extension für Visual Studio angeboten. Dessen kostenlose Express-Edition schränkt die Verwendung des INotifyPropertyChanged-Aspekts auf 10 Klassen ein, sodass diese Wahl vermutlich mit finanziellen Kosten einhergeht. Fody stellt demgegenüber ein kostenloses NuGet-Paket dar, was es als überzeugende Wahl erscheinen lässt. Davon abgesehen sollten Sie bedenken, dass bei allen AOP-Frameworks der von Ihnen erstellte (und debuggte) Code nicht exakt der ausgeführte Code ist.

Ein dritter Weg

Eine alternative Möglichkeit zum Lösen des Problems ist die Nutzung des objektorientierten Entwurfs: Geben Sie den Eigenschaften selbst die Zuständigkeit für das Auslösen der Ereignisse! Das ist keine besonders revolutionäre Idee, aber ich habe sie außerhalb meiner eigenen Projekte nicht angetroffen. In der einfachsten Form kann das etwa so aussehen:

public class NotifyProperty<T>
{
  public NotifyProperty(INotifyPropertyChanged owner, string name, T initialValue);
  public string Name { get; }
  public T Value { get; }
  public void SetValue(T newValue);
}

Der Grundgedanke ist, dass Sie die Eigenschaft mit ihrem Namen und einem Verweis auf ihren Besitzer ausstatten und sie die das Auslösen des PropertyChanged-Ereignisses übernehmen lassen – etwa in dieser Art:

public void SetValue(T newValue)
{
  if(newValue != m_value)
  {
    m_value = newValue;
    m_owner.PropertyChanged(m_owner, new PropertyChangedEventArgs(Name));
  }
}

Das Problem ist, dass das praktisch nicht funktioniert: Ich kann in dieser Weise kein Ereignis aus einer anderen Klasse auslösen. Ich benötige eine Art Vertrag mit der besitzenden Klasse, der mir erlaubt, deren PropertyChanged-Ereignis auszulösen: Das ist genau die Rolle einer Schnittstelle. Also erstelle ich eine:

public interface IRaisePropertyChanged
{
  void RaisePropertyChanged(string propertyName)
}

Sobald ich über diese Schnittstelle verfüge, kann ich Notify­Property.SetValue real implementieren:

public void SetValue(T newValue)
{
  if(newValue != m_value)
  {
    m_value = newValue;
    m_owner.RaisePropertyChanged(this.Name);
  }
}

Implementierung von IRaisePropertyChanged: Vom Besitzer der Eigenschaft die Implementierung einer Schnittstelle zu verlangen, bedeutet, dass jede ViewModel-Klasse einen Codebaustein in geeigneter Form benötigt, wie in Abbildung 1 zu sehen. Der erste Teil ist für jede Klasse zum Implementieren von INotifyPropertyChanged erforderlich; der zweite Teil ist für die neue IRaisePropertyChanged-Schnittstelle spezifisch. Beachten Sie, dass ich es aufgrund der Tatsache, dass die RaisePropertyChanged-Methode nicht für den allgemeinen Gebrauch bestimmt ist, vorgezogen habe, sie explizit zu implementieren.

Abbildung 1 Erforderlicher Code zum Implementieren von IRaisePropertyChanged

// PART 1: required for any class that implements INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
  // In C# 6, you can use PropertyChanged?.Invoke.
  // Otherwise I'd suggest an extension method.
  var toRaise = PropertyChanged;
  if (toRaise != null)
    toRaise(this, args);
}
// PART 2: IRaisePropertyChanged-specific
protected virtual void RaisePropertyChanged(string propertyName)
{
  OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
// This method is only really for the sake of the interface,
// not for general usage, so I implement it explicitly.
void IRaisePropertyChanged.RaisePropertyChanged(string propertyName)
{
  this.RaisePropertyChanged(propertyName);
}

Ich könnte diesen Codebaustein in einer Basisklasse unterbringen und sie erweitern, was mich anscheinend zu der früheren Diskussion zurückbringt. Wenn ich CallerMemberName auf die RaisePropertyChanged-Methode anwende, habe ich ja eigentlich nur die erste Technik neu erfunden, also wo liegt der Vorteil? In beiden Fällen könnte ich einfach den Codebaustein in andere Klassen kopieren, wenn diese nicht aus einer Basisklasse ableiten können.

Ein wichtiger Unterschied zur früheren Basisklassentechnik besteht darin, dass der Codebaustein in diesem Fall keine echte Logik enthält; die gesamte Logik ist in der NotifyProperty-Klasse verkapselt. Die Überprüfung, ob sich der Eigenschaftswert geändert hat, bevor das Ereignis ausgelöst wird, ist einfache Logik, trotzdem ist es besser, sie nicht zu duplizieren. Betrachten Sie, was geschehen würde, wenn Sie einen anderen IEqualityComparer für die Prüfung verwenden möchten. Bei diesem Modell brauchen Sie nur die NotifyProperty-Klasse zu ändern. Selbst wenn Sie mehrere Klassen mit dem gleichen IRaisePropertyChanged-Codebaustein verwenden, profitiert jede Implementierung von den Änderungen an NotifyProperty, ohne dass Codeänderungen ihrerseits erforderlich wären. Abgesehen von eventuellen Verhaltensänderungen, die Sie vielleicht implementieren möchten, sind Änderungen am Code von IRaisePropertyChanged sehr unwahrscheinlich.

Und jetzt alles zusammen: Wir verfügen jetzt über die Schnittstelle, die für das ViewModel implementiert werden muss, und über die NotifyProperty-Klasse, die für die Eigenschaften mit Datenbindung verwendet wird. Der letzte Schritt besteht in der Konstruktion von NotifyProperty; zu diesem Zweck muss immer noch in irgendeiner Form der Eigenschaftsname übergeben werden. Wenn Sie das Glück haben, in C# 6 zu arbeiten, erledigt sich das ganz einfach mit dem nameof-Operator. Andernfalls können Sie NotifyProperty mithilfe von Ausdrücken erstellen, etwa durch die Verwendung einer Erweiterungsmethode (unglücklicherweise gibt es diesmal keinen Platz für Caller­MemberName):

public static NotifyProperty<T> CreateNotifyProperty<T>(
  this IRaisePropertyChanged owner,
  Expression<Func<T>> nameExpression, T initialValue)
{
  return new NotifyProperty<T>(owner,
    ObjectNamingExtensions.GetName(nameExpression),
    initialValue);
}
// Listing of GetName provided earlier

Bei diesem Ansatz zahlen Sie immer noch den Preis für Reflexion, jedoch nur, wenn ein Objekt erstellt wird, statt bei jeder Wertänderung einer Eigenschaft. Wenn das noch zu teuer wird (weil Sie viele Objekte erstellen), können Sie immer noch einen Aufruf von GetName zwischenspeichern und ihn als statischen schreibgeschützten Wert in der ViewModel-Klasse aufbewahren. Für beide Fälle zeigt Abbildung 2 ein Beispiel für ein einfaches ViewModel.

Abbildung 2 Einfaches ViewModel mit einer NotifyProperty-Eigenschaft

public class LogInViewModel : IRaisePropertyChanged
{
  public LogInViewModel()
  {
    // C# 6
    this.m_userNameProperty = new NotifyProperty<string>(
      this, nameof(UserName), null);
    // Extension method using expressions
    this.m_userNameProperty = this.CreateNotifyProperty(() => UserName, null);
  }
  private readonly NotifyProperty<string> m_userNameProperty;
  public string UserName
  {
    get
    {
      return m_userNameProperty.Value;
    }
    set
    {
      m_userNameProperty.SetValue(value);
    }
  }
  // Plus the IRaisePropertyChanged code in Figure 1 (otherwise, use a base class)
}

Bindung und Umbenennen: Da wir über Namen reden, ist jetzt der richtige Zeitpunkt, um über einen anderen Aspekt von Datenbindung zu sprechen. Das sichere Auslösen des PropertyChanged-Ereignisses ohne eine hartcodierte Zeichenfolge ist die halbe Miete, wenn es darum geht, ein Refactoring zu überstehen; die andere Hälfte ist die eigentliche Datenbindung. Wenn Sie in XAML eine Eigenschaft umbenennen, die für eine Bindung verwendet wird, ist der Erfolg – ich sag's mal nett: nicht sicher (ein Beispiel bietet bit.ly/1WCWE5m).

Die Alternative besteht darin, den Code für die Datenbindungen manuell in der CodeBehind-Datei zu erstellen. Beispiel:

// Constructor
public LogInDialog()
{
  InitializeComponent();
  LogInViewModel forNaming = null;
  m_textBoxUserName.SetBinding(TextBox.TextProperty,
    ObjectNamingExtensions.GetName(() => forNaming.UserName);
  // Or with C# 6, just nameof(LogInViewModel.UserName)
}

Es wirkt schon etwas seltsam, das NULL-Objekt nur zu dem Zweck einzuführen, die Ausdrucksfunktionalität zu nutzen, es funktioniert aber (wenn Sie Zugang zu nameof haben, brauchen Sie sich damit nicht abzugeben).

Ich finde diese Technik nützlich, aber der Preis ist mir bewusst. Auf der Habenseite steht, dass Refactoring höchst wahrscheinlich funktioniert, wenn die UserName-Eigenschaft umbenannt wird. Ein weiterer wichtiger Vorzug besteht darin, dass „Alle Verweise suchen“ wie erwartet funktioniert.

Auf der Sollseite steht, dass dieses Verfahren nicht so einfach und natürlich ist wie das Erstellen der Bindung in XAML und es mich daran hindert, den Entwurf der Benutzeroberfläche „unabhängig“ zu halten. Beispielsweise kann ich nicht einfach das Aussehen im Blend-Tool ändern, ohne zugleich auch Code zu ändern. Außerdem funktioniert diese Technik nicht in Verbindung mit Datenvorlagen; Sie können eine Vorlage in ein benutzerdefiniertes Steuerelement extrahieren, das macht aber mehr Arbeit.

In der Summe gewinne ich Flexibilität bei Änderungen auf der Seite des „Datenmodells“, um den Preis einer verringerten Flexibilität auf der Ansichtsseite. Alles in allem bleibt es Ihre Entscheidung, ob die Vorteile eine Deklaration der Bindungen in dieser Weise rechtfertigen.

„Abgeleitete“ Eigenschaften

Früher in diesem Artikel habe ich ein Szenario beschrieben, in dem das Auslösen des PropertyChanged-Ereignisses besonders unkomfortabel ist, nämlich bei Eigenschaften, deren Wert von anderen Eigenschaften abhängt. Ich habe das einfache Beispiel einer FullName-Eigenschaft erwähnt, die von FirstName und LastName abhängt. Mein Ziel bei der Implementierung dieses Szenarios ist es, diese grundlegenden NotifyProperty-Objekte (FirstName und LastName) sowie die Funktion zum Berechnen des aus ihnen abgeleiteten Werts aufzugreifen (z. B. FirstName.Value + " " + LastName.Value) und aus diesen Bestandteilen ein Eigenschaftsobjekt zu erstellen, das den Rest automatisch erledigt. Zu diesem Zweck nehme ich zwei Anpassungen an meiner ursprünglichen NotifyProperty-Klasse vor.

Die erste Aufgabe besteht darin, ein separates ValueChanged-Ereignis der NotifyProperty-Eigenschaft verfügbar zu machen. Die abgeleitete Eigenschaft lauscht an ihren zugrundeliegenden Eigenschaften auf dieses Ereignis und reagiert durch das Berechnen eines neuen Werts (und das Auslösen des entsprechenden PropertyChanged-Ereignisses für sich selbst). Die zweite Aufgabe besteht darin, eine Schnittstelle, IProperty<T>, zu extrahieren, um die allgemeine NotifyProperty-Funktionalität zu verkapseln. Neben einer Reihe anderer Dinge ermöglicht mir dieses Vorgehen, mit abgeleiteten Eigenschaften zu arbeiten, die ihrerseits aus abgeleiteten Eigenschaften stammen. Die resultierende Schnittstelle ergibt sich in gerader Linie und ist hier aufgeführt (die entsprechenden Änderungen an NotifyProperty sind sehr einfach, daher führe ich sie hier nicht auf):

public interface IProperty<TValue>
{
  string Name { get; }
  event EventHandler<ValueChangedEventArgs> ValueChanged;
  TValue Value { get; }
}

Das Erstellen der DerivedNotifyProperty-Klasse scheint auch ganz einfach, bis Sie versuchen, die Einzelteile zusammenzufügen. Die Grundidee war, die zugrundeliegenden Eigenschaften und eine Funktion zum Berechnen eines neuen Werts aus diesen einzubeziehen, aber das führt sofort zu Schwierigkeiten aufgrund von Generics. Es gibt keine praktische Möglichkeit, mehrere verschiedene Eigenschaftstypen einzubeziehen:

// Attempted constructor
public DerivedNotifyProperty(IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)

Ich kann die erste Hälfte des Problems (das Akzeptieren mehrerer generischer Typen) umschiffen, indem ich stattdessen statische Create-Methoden verwende:

static DerivedNotifyProperty<TDerived> CreateDerivedNotifyProperty
  <T1, T2, TDerived>(this IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)

Die abgeleitete Eigenschaft muss aber trotzdem an jeder Basiseigenschaft auf das ValueChanged-Ereignis lauschen. Für die Lösung sind zwei Schritte erforderlich. Zunächst extrahiere ich das ValueChanged-Ereignis in eine separate Schnittstelle:

public interface INotifyValueChanged // No generic type!
{
  event EventHandler<ValueChangedEventArgs> ValueChanged;
}
public interface IProperty<TValue> : INotifyValueChanged
{
  string Name { get; }
  TValue Value { get; }
}

Dadurch kann die DerivedNotifyProperty-Klasse die nicht generische INotifyValueChanged anstelle der generischen Schnittstelle IProperty<T> aufnehmen. Im zweiten Schritt muss der neue Wert ohne Generics berechnet werden: Ich nehme die ursprüngliche derivedValueFunction, die die zwei generischen Parameter akzeptiert und erstelle ausgehend davon eine neue anonyme Funktion, die keinerlei Parameter erfordert – sie verweist stattdessen auf die Werte der zwei übergebenen Eigenschaften. Anders gesagt, erstelle ich einen Abschluss. Sie können diesen Vorgang im folgenden Code verfolgen:

static DerivedNotifyProperty<TDerived> CreateDerivedNotifyProperty
  <T1, T2, TDerived>(this IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)
{
  // Closure
  Func<TDerived> newDerivedValueFunction =
    () => derivedValueFunction (property1.Value, property2.Value);
  return new DerivedNotifyProperty<TValue>(owner, propertyName,
    newDerivedValueFunction, property1, property2);
}

Die neue Funktion für den abgeleiteten Wert ist einfach Func<TDerived> ohne Parameter; die DerivedNotifyProperty-Eigenschaft benötigt nun keine Kenntnis der zugrundeliegenden Eigenschaftstypen, ich kann daher ganz einfach eine von mehreren Eigenschaften verschiedener Typen erstellen.

Die andere Raffinesse besteht darin, wann dies Funktion für den abgeleiteten Wert tatsächlich aufgerufen wird. Eine naheliegende Implementierung wäre, an jeder zugrundeliegenden Eigenschaft auf das ValueChanged-Ereignis zu lauschen und die Funktion immer dann aufzurufen, wenn sich eine Eigenschaft ändert, aber das ist wenig effizient, wenn sich innerhalb des gleichen Vorgangs mehrere zugrundeliegende Eigenschaften ändern (stellen Sie sich eine Schaltfläche „Zurücksetzen“ vor, um ein Formular von Einträgen zu leeren). Es ist sinnvoller, den Wert auf Abruf zu erzeugen (und ihn zwischenzuspeichern) und für ungültig zu erklären, wenn sich eine der zugrundeliegenden Eigenschaften ändert. Lazy<T> stellt eine perfekte Möglichkeit dar, dieses Verhalten zu implementieren.

In Abbildung 3 sehen Sie eine abgekürzte Auflistung der DerivedNotifyProperty-Klasse. Beachten Sie, dass die Klasse eine beliebige Anzahl Eigenschaften aufnimmt, an denen sie lauscht – obwohl ich nur die Create-Methoden für zwei zugrundeliegende Eigenschaften aufführe, kann ich weitere Überladungen erstellen, die eine zugrundeliegende Eigenschaft, drei zugrundeliegende Eigenschaften usw. einbeziehen.

Abbildung 3 Kernimplementierung von DerivedNotifyProperty

public class DerivedNotifyProperty<TValue> : IProperty<TValue>
{
  private readonly IRaisePropertyChanged m_owner;
  private readonly Func<TValue> m_getValueProperty;
  public DerivedNotifyProperty(IRaisePropertyChanged owner,
    string derivedPropertyName, Func<TValue> getDerivedPropertyValue,
    params INotifyValueChanged[] valueChangesToListenFor)
  {
    this.m_owner = owner;
    this.Name = derivedPropertyName;
    this.m_getValueProperty = getDerivedPropertyValue;
    this.m_value = new Lazy<TValue>(m_getValueProperty);
    foreach (INotifyValueChanged valueChangeToListenFor in valueChangesToListenFor)
      valueChangeToListenFor.ValueChanged += (sender, e) => RefreshProperty();
  }
  // Name property and ValueChanged event omitted for brevity 
  private Lazy<TValue> m_value;
  public TValue Value
  {
    get
    {
      return m_value.Value;
    }
  }
  public void RefreshProperty()
  {
    // Ensure we retrieve the value anew the next time it is requested
    this.m_value = new Lazy<TValue>(m_getValueProperty);
    OnValueChanged(new ValueChangedEventArgs());
    m_owner.RaisePropertyChanged(Name);
  }
}

Beachten Sie, dass die zugrundeliegenden Eigenschaften von verschiedenen Besitzern stammen können. Nehmen Sie beispielsweise an, Sie verfügen über ein Adressen-ViewModel mit einer IsAddressValid-Eigenschaft. Sie haben außerdem ein Order-ViewModel, das zwei Address-ViewModels für Rechnungs- und Versandadressen enthält. Es wäre vernünftig, eine IsOrderValid-Eigenschaft für das übergeordnete Order-ViewModel zu erstellen, das die IsAddressValid-Eigenschaften der untergeordneten Address-ViewModels kombiniert, damit die Bestellung nur dann versendet werden kann, wenn beide Adressen gültig sind. Zu diesem Zweck würde das Address-ViewModel sowohl „bool IsAddressValid { get; }“ und „IProperty<bool> IsAddressValidProperty { get; }“ verfügbar machen, sodass das Order-ViewModel eine DerivedNotifyProperty erstellen kann, die auf die untergeordneten IsAddressValidProperty-Objekte verweist.

Der Nutzen von DerivedNotifyProperty

Das FullName-Beispiel, das ich für eine abgeleitete Eigenschaft angegeben habe, ist ziemlich konstruiert, ich möchte aber einige echte Einsatzfälle erörtern und diese mit Entwurfsprinzipien verbinden. Ich habe gerade ein Beispiel angestoßen: IsValid. Das ist eine ziemlich einfache und leistungsstarke Möglichkeit, beispielsweise die Schaltfläche „Speichern“ auf einem Formular zu deaktivieren. Beachten Sie, dass nichts Sie dazu zwingt, den Einsatz dieser Technik auf den Kontext eines UI-ViewModels zu beschränken. Sie können sie auch zum Validieren von Geschäftsobjekten verwenden; sie müssen lediglich IRaisePropertyChanged implementieren.

Eine zweite Situation, in der abgeleitete Eigenschaften äußerst nützlich sind, stellen Drilldownszenarien dar. Stellen Sie sich als einfaches Beispiel ein Kombinationsfeld zum Auswählen eines Landes vor, bei dem durch die Auswahl eines Landes eine Liste mit Städten aufgefüllt wird. SelectedCountry kann eine NotifyProperty sein, und wenn eine GetCitiesForCountry-Methode vorhanden ist, können Sie AvailableCities als eine DerivedNotifyProperty erstellen, die automatisch mit einer erfolgenden Änderung des ausgewählten Landes synchronisiert wird.

Ein dritter Bereich, für den ich NotifyProperty-Objekte verwendet habe, ist die Angabe, ob ein Objekt „beschäftigt“ ist. Während ein Objekt als beschäftigt betrachtet wird, sollten bestimmte Funktionen der Benutzeroberfläche deaktiviert sein, und es kann sinnvoll sein, für den Benutzer eine Statusanzeige darzustellen. Dies ist ein scheinbar einfaches Szenario, aber hier gibt es eine ganze Menge an Raffinesse hinter den Kulissen zu entdecken.

Der erste Teil besteht in der Nachverfolgung, ob das Objekt beschäftigt ist; im einfachen Fall kann das mithilfe einer booleschen NotifyProperty erfolgen. Was jedoch oft geschieht, ist, dass ein Objekt aus einem von mehreren Gründen „beschäftigt“ sein kann: Nehmen wir an, dass ich mehrere Datenbereiche parallel lade. Der Gesamtstatus „beschäftigt“ sollte davon abhängen, ob einer dieser Vorgänge noch ausgeführt wird. Das hört sich nach einem Einsatzgebiet für abgeleitete Eigenschaften an, tatsächlich wäre es aber schwerfällig (wenn nicht gar unmöglich): Ich würde für jeden möglichen Vorgang eine Eigenschaft benötigen, um nachzuverfolgen, ob er noch ausgeführt wird. Stattdessen möchte ich etwas in der folgenden Art für jeden der Vorgänge ausführen und dazu nur eine einzelne IsBusy-Eigenschaft verwenden:

try
{
  IsBusy.SetValue(true);
  await LongRunningOperation();
}
finally
{
  IsBusy.SetValue(false);
}

Zu diesem Zweck erstelle ich eine IsBusyNotifyProperty-Klasse, die NotifyProperty<bool> erweitert und unterhalte darin einen „Beschäftigt-Zähler“. Ich überschreibe SetValue in der Weise, dass der Zähler durch SetValue(true) herauf- und durch Set­Value(false) herabgesetzt wird. Nur in dem Fall, dass der Zähler von 0 zu 1 wechselt, rufe ich base.SetValue(true) auf, dagegen base.SetValue(false), wenn er von 1 zu 0 wechselt. Auf diese Weise führt das Starten mehrerer ausstehender Vorgänge nur einmal dazu, dass IsBusy wahr wird, und anschließend wird es nur wieder falsch, wenn sie alle abgeschlossen sind. Die Implementierung können Sie aus dem Codedownload ersehen.

Damit wird der „Beschäftigt“-Aspekt unseres Vorhabens bewältigt: Ich kann „ist beschäftigt“ an die Sichtbarkeit eines Statusindikators binden. Zum Deaktivieren des UI-Elements benötigt ich jedoch das Gegenteil. Wenn „ist beschäftigt“ wahr ist, sollte „UI aktiviert“ falsch sein.

In XAML gibt es das Konzept des IValueConverters, der einen Wert in eine angezeigte Darstellung konvertiert (oder ihn aus ihr konvertiert). Ein allfälliges Beispiel ist BooleanToVisibilityConverter – in XAML wird die „Sichtbarkeit“ eines Elements nicht durch einen booleschen Wert sondern durch einen Aufzählungswert ausgedrückt. Das bedeutet auch, dass es nicht möglich ist, die Sichtbarkeit eines Elements direkt an eine boolesche Eigenschaft (wie IsBusy) zu binden; Sie müssen den Wert binden und darüber hinaus einen Konverter verwenden. Beispiel:

<StackPanel Visibility="{Binding IsBusy,
  Converter={StaticResource BooleanToVisibilityConverter}}" />

Ich habe schon erwähnt, dass „UI aktivieren“ das Gegenteil von „ist beschäftigt“ ist; es scheint nahe zu liegen, einen Wertkonverter zu erstellen, um eine boolesche Eigenschaft zu invertieren und sie für diesen Zweck zu verwenden:

<Grid IsEnabled="{Binding IsBusy,
   Converter={StaticResource BooleanToInverseConverter}}" />

Bevor ich eine DerivedNotifyProperty-Klasse erstellt hatte, war das auch tatsächlich der einfachste Weg. Es war ziemlich mühsam, eine separate Eigenschaft zu erstellen, sie mit dem Kehrwert von „IsBusy“ zu verbinden und das passende PropertyChanged-Ereignis auszulösen. Jetzt ist das ein triviales Unterfangen und ohne das künstliche Hindernis (in Gestalt von Faulheit) habe ich ein besseres Verständnis davon, wo der Einsatz von IValueConverter sinnvoll ist.

Idealer Weise sollte die Ansicht – unabhängig von ihrer Implementierung (z. B. WPF oder Windows Forms; aber selbst eine Konsolenanwendung stellt eine Art Ansicht dar) – eine Visualisierung (gewissermaßen eine Projektion) der Abläufe in der zugrundeliegenden Anwendung sein und ihrerseits nicht für die Mechanismen und Geschäftsregeln zuständig sein, die die Abläufe regeln. In diesem Fall ist die Tatsache, dass IsBusy und IsEnabled so eng mit einander zusammenhängen, lediglich ein Implementierungsdetail; der Zusammenhang zwischen dem Deaktivieren der Benutzeroberfläche und dem Auslastungsstatus der Anwendung ist nicht zwangsläufig.

Bei der Lage der Dinge betrachte ich das als eine Grauzone und würde Ihnen nicht widersprechen, wenn Sie sich für die Implementierung mithilfe eines Wertkonverters entscheiden sollten. Ich kann jedoch ein viel stärkeres Argument bringen, indem ich dem Beispiel noch ein weiteres Element hinzufüge. Legen wir fest, dass die Anwendung die Benutzeroberfläche auch dann deaktivieren soll, wenn sie die Netzwerkverbindung verliert (und diese Situation durch Anzeige eines Bedienfelds bekanntgeben soll). Und damit haben wir drei Situationen: Wenn die Anwendung ausgelastet ist, soll sie die Benutzeroberfläche deaktivieren (und eine Statusanzeige darstellen). Wenn die Anwendung die Netzwerkverbindung verliert, soll sie ebenfalls die Benutzeroberfläche deaktivieren (und ein Bedienfeld „Keine Netzwerkverbindung“ anzeigen). Die dritte Situation liegt vor, wenn die Anwendung über eine Netzwerkverbindung verfügt und nicht ausgelastet ist und daher Eingaben annehmen kann.

Dies ohne eine separate IsEnabled-Eigenschaft zu implementieren, ist im besten Fall plump; Sie könnten eine MultiBinding-Instanz verwenden, das ist aber immer noch ungelenk und wird nicht in allen Umgebungen unterstützt. Letzten Endes ist diese Art Plumpheit normalerweise ein Hinweis darauf, dass es eine bessere Möglichkeit gibt, und jetzt wissen wir auch, dass das so ist: Diese Logik lässt sich innerhalb des ViewModels besser verarbeiten. Es ist an diesem Punkt trivial, zwei NotifyProperties verfügbar zu machen – IsBusy und IsDisconnected – und dann eine DerivedNotifyProperty IsEnabled zu erstellen, die nur wahr ist, wenn beide falsch sind.

Wenn Sie auf den IValueConverter-Pfad abgebogen sind und den Enabled-Status der Benutzeroberfläche direkt an IsBusy gebunden haben (mit einem zwischengeschalteten Konverter, um den Wert zu invertieren), haben Sie jetzt ein ziemliches Stück Arbeit vor sich. Wenn Sie stattdessen eine separate, abgeleitete IsEnabled-Eigenschaft verfügbar gemacht haben, macht das Hinzufügen dieses neuen Stücks Programmlogik viel weniger Arbeit, und die eigentliche IsEnabled-Bindung muss noch nicht einmal geändert werden. Das ist ein gutes Zeichen dafür, dass Sie auf dem richtigen Weg sind.

Zusammenfassung

Beim Entwickeln dieses Frameworks mussten wir einen recht langen Weg zurücklegen, der Lohn der Mühe ist aber, dass Benachrichtigungen über Eigenschaftsänderungen ohne wiederholte Codebausteine, ohne Magic Strings und mit Refactoringunterstützung implementiert werden können. Für meine ViewModels ist keine Logik aus einer bestimmten Basisklasse erforderlich. Ich kann ohne großen Zusatzaufwand abgeleitete Eigenschaften erstellen, die auch die passenden Änderungsbenachrichtigungen auslösen. Und schließlich ist der sichtbare Code auch der ausgeführte Code. All das erhalte ich durch die Entwicklung eines ziemlich einfachen Frameworks mit objektorientiertem Layout. Ich hoffe, Sie finden das nützlich für eigene Projekte.


Mark Sowulteilt als begeisterter .NET-Entwickler seit den Anfängen sein breites Wissen zur Architektur und Leistung von Microsoft .NET-Framework und SQL Server über sein in New York ansässiges Beratungsunternehmen SolSoft Solutions. Sie erreichen ihn unter mark@solsoftsolutions.com. Wenn Sie seine Ideen fesselnd finden und seinen Newsletter abonnieren möchten, registrieren Sie sich unter eepurl.com/_K7YD.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Francis Cheung (Microsoft) und Charles Malm (Zebra Technologies)
Francis Cheung ist leitender Entwickler der Microsoft Patterns & Practices-Gruppe. Francis war an verschiedensten Projekten beteiligt, einschließlich Prism. Sein derzeitiger Schwerpunkt liegt auf Hilfestellung in Azure-bezogenen Kontexten.

Charles Malm ist Softwareentwickler für Spiele, .NET und das Web und Mitbegründer von RealmSource, LLC.