PropertyChanged Event ohne Stringliteral aufrufen

by St. Lange 26. August 2009 17:43

Bei der Implementierung von INotifyPropertyChanged muss der Property-Setter das PropertyChanded Event auslösen, wenn sich der Wert der Property ändert. Das sieht beispielsweise so aus:

public class ViewModel : INotifyPropertyChanged
{
  public string Name
  {
    get { return _name; }
    set
    {
      if (_name != value)
      {
        _name = value;
        OnPropertyChanged("Name");
      }
    }
  }
  string _name;

  public event PropertyChangedEventHandler PropertyChanged;

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

Den Namen der geänderten Property als Stringliteral an den Event Handler zu übergeben verhindert, dass er etwa beim Refactoring automatisch mit umbenannt wird. Ein möglicher Lösungsansatz, über den auch schon viel geschrieben wurde, besteht in Verwendung einer Lambda Expression:

OnPropertyChanged(x => x.Name);

Anstatt des Strings wird ein Expression Tree übergeben, aus dem dann der Name der Property ermittelt wird. Eine Implementierung dazu findet sich beispielsweise hier: http://www.lieser-online.de/blog/?p=130

Damit der Compiler den Typ von x herleiten kann, muss allerdings für jede ViewModel Klasse eine eigene Version von OnPropertyChanged implementiert werden. Das finde ich aber noch lästiger als den String direkt hinzuschreiben.

Eine Lösung besteht darin, zusätzlich this als ersten Parameter zu übergeben:

OnPropertyChanged(this, x => x.Name);

Der Compiler kann so den Typ von x über die Signatur der Funktionsdefintion erschließen. OnPropertyChanged kann dadurch in eine Basisklasse verschoben werden und ist wie folgt definiert:

protected void OnPropertyChanged<T, TResult>(T dummy, Expression<Func<T, TResult>> propertyExpression)
{
  OnPropertyChanged(((MemberExpression)propertyExpression.Body).Member.Name);
}

Der Parameter this wird vom Compiler dazu verwendet herzuleiten, dass x vom Typ ViewModel sein muss. So kann OnPropertyChanged vollständig generisch sein. Der Wert von this wird aber gar nicht verwendet.

Das Ganze funktioniert zwar, ist aber insofern etwas seltsam, als dass this nur zur Herleitung des Typs von x verwendet wird. Es ist also noch keine optimale Lösung.

Da wir ja nur den Namen der Property wissen wollen, können wir die Lambda Expression etwas einfacher hinschreiben:

OnPropertyChanged(() => Name);

Das passende OnPropertyChanged sieht dann so aus:

protected void OnPropertyChanged<TResult>(Expression<Func<TResult>> propertyExpression)
{
  OnPropertyChanged(((MemberExpression)propertyExpression.Body).Member.Name);
}

Diese Variante verwende ich selbst in Silverlight und WPF Projekten, da wir stets mit einer ViewModel Basisklasse arbeiten.

Hier der gesamte Quellcode:

public abstract class ViewModelBase : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;

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

  protected void OnPropertyChanged<TResult>(Expression<Func<TResult>> propertyExpression)
  {
    OnPropertyChanged(((MemberExpression)propertyExpression.Body).Member.Name);
  }
}

public class ViewModel : ViewModelBase
{
  public string Name
  {
    get { return _name; }
    set
    {
      if (_name != value)
      {
        _name = value;
        OnPropertyChanged(() => Name);
      }
    }
  }
  string _name;
}

Extension Method

Wer keine gemeinsame Basisklasse für alle seine ViewModel Klassen verwenden möchte, kann auch eine Funktion Raise als Extension Method für den Typ PropertyChangedEventHandler implementieren. Das sieht dann so aus:

public static class PropertyChangedExtensions
{
  public static void Raise<T, TResult>(this PropertyChangedEventHandler handler, T sender, Expression<Func<T, TResult>> propertyExpression)
  {
    if (handler != null)
      handler(sender, new PropertyChangedEventArgs((((MemberExpression)propertyExpression.Body).Member.Name)));
  }
}

Die Extension Method Raise wird direkt am Event Handler aufgerufen:

PropertyChanged.Raise(this, x => x.Name);
PropertyChanged.Raise(this, x => Name);  // same as above

Auch diese Schreibweise ist möglich:

PropertyChanged.Raise(this, () => Name);

Dazu muss in der Definition von Raise nur der erste Parameter des Delegates Func gelöscht werden.

Performance

Die Auswertung des Expression Trees dauert vermutlich mindestens 100 mal länger als der direkte Funktionsaufruf mit einem Stringliteral. Da es sich aber um UI Code handelt spielt das überhaupt keine Rolle. Andere UI Funktionalitäten, wie das Zuweisen von Styles oder Data Templates, dauert um so vieles länger, dass die Verwendung von Expression Trees insgesamt kaum messbar sein wird.

Beim Recherchieren zu diesem Thema habe ich jedoch einen Blogeintrag gefunden, bei dem der Autor in einer ähnlichen Lösung den übergebenen Expression Tree zunächst zerlegt, dann compiliert und schließlich ausführt, um an das sender Objekt zu gelangen. Das ist nicht nur unnötig, sondern vermutlich dann doch auch messbar inperformant.

Aspektorientierte Lösung

Eine aspektorientierte Lösung beispielsweise mit PostSharp halte ich bei diesem Problem für übertrieben. Der zusätzliche Code bei der hier gezeigten Lösung ist minimal und außerdem gut lesbar. Nur um eine Zeile Code im jedem Property-Setter zu eliminieren und im Gegenzug durch ein zusätzliches Attribut zu ersetzten erscheint mir nicht sinnvoll. Es kann aber sein, dass ich meine Meinung dazu zukünftig ändere.

Fazit

Expression Trees haben auch außerhalb von LINQ viele nützliche Anwendungsgebiete. Allerdings sind sie auch etwas komplizierter, was leicht zu einer suboptimalen Verwendung führen kann. Erst beim Schreiben dieses Blogeintrags ist mir beispielsweise eine Verbesserung aufgefallen, die ich vorher übersehen hatte. Ich hatte ursprünglich OnPropertyChanged nicht als generische Funktion geschrieben, sondern stattdessen Func<object> verwendet. Das geht zwar auch, man muss allerdings aufpassen: Wenn der Typ der Property ein Value Type ist, wird im Expression Tree so etwas ähnliches wie „Boxing“ verwendet. Man muss daher eine zusätzliche Variante von OnPropertyChanged mit Func<ValueType> implementieren, die dies durch ein entsprechendes „Unboxing“ berücksichtigt.

Tags: , ,

Silverlight | WPF

Powered by BlogEngine.NET 1.6.1.0 - Impressum