Entwurfsmuster

Probleme und Lösungen mit Model-View-ViewModel

Robert McCarter

Beispielcode herunterladen

Windows Presentation Foundation (WPF) und Silverlight bieten umfassende APIs für die Erstellung zeitgemäßer Anwendungen. Jedoch kann es sich als schwierig erweisen, alle der zahlreichen WPF-Features zu kennen und so harmonisch miteinander zu kombinieren, dass gut entworfene und leicht verwaltbare Anwendungen entstehen. Wo sollen Sie beginnen? Und wie stellen Sie Ihre Anwendung am besten zusammen?

Das MVVM (Model-View-ViewModel)-Entwurfsmuster stellt einen beliebten Ansatz zur Erstellung von WPF- und Silverlight-Anwendungen dar. Es ist sowohl ein leistungsstarkes Tool für die Anwendungserstellung als auch eine gängige Sprache für Diskussionen zum Thema Anwendungsentwürfe unter Entwicklern. Obwohl es sich bei MVVM um ein wirklich nützliches Entwurfsmuster handelt, ist das Tool noch sehr neu und wird häufig fehlinterpretiert.

Wann kann das MVVM-Entwurfsmuster angewendet werden, und wann ist das nicht sinnvoll? Wie sollte die Anwendung strukturiert werden? Wie viel Arbeit macht es, die ViewModel-Schicht zu schreiben und zu pflegen, und welche Alternativen stehen zur Codereduzierung in der ViewModel-Schicht zur Verfügung? Wie kann eine elegante Verarbeitung verwandter Eigenschaften innerhalb des Modells erzielt werden? Wie können Sie Auflistungen im Modell für die Ansicht bereitstellen? Wo werden ViewModel-Objekte instanziiert und mit Modellobjekten verbunden?

In diesem Artikel erkläre ich Ihnen, wie ViewModel funktioniert. Außerdem lege ich einige Vorteile und Probleme bei der Implementierung von ViewModel in Code dar. Zudem erläutere ich anhand von konkreten Beispielen, wie ViewModel als Dokument-Manager zur Bereitstellung von Modellobjekten in der Ansichtsschicht verwendet wird.

Modell, ViewModel und Ansicht

Jede WPF- und Silverlight-Anwendung, mit der ich bisher gearbeitet habe, wies einen Komponentenentwurf auf sehr hohem Niveau auf. Das Modell bildete dabei den Kern der Anwendung, und es wurde sehr viel Mühe darauf verwendet, es basierend auf den Best Practices von objektorientierter Analyse und Design (OOAD) zu entwerfen.

Auch für mich ist das Modell das Herz einer Anwendung, es stellt die größte und wichtigste Geschäftsressource dar, weil darin alle komplexen Geschäftselemente, ihre Beziehungen sowie ihre Funktionalität erfasst sind.

Auf dem Modell sitzt ViewModel auf. Die beiden Hauptziele von ViewModel bestehen darin, für die WPF- bzw. XAML-Ansicht ein leicht zu verarbeitendes Modell bereitzustellen und das Modell von der Ansicht zu separieren und zu kapseln. Dies sind gute Ziele, die jedoch aus pragmatischen Gründen nicht immer umgesetzt werden können.

Wenn Sie ViewModel erstellen, wissen Sie bereits im Detail, wie der Benutzer mit der Anwendung interagieren wird. Allerdings ist es ein wesentlicher Bestandteil des MVVM-Entwurfsmusters, dass bei der ViewModel-Erstellung nichts über die Ansicht bekannt ist. Somit können die Gestalter und Grafiker des Interaktionsbereichs schöne und funktionelle Benutzeroberflächen entwickeln, die auf ViewModel aufsitzen, und dabei eng mit den Entwicklern zusammenarbeiten, damit die angesprochenen Bestrebungen vom ViewModel-Entwurf unterstützt werden. Zudem ermöglicht eine Trennung von Ansicht und ViewModel, dass die ViewModel-Komponenten besser getestet und erneut verwendet werden können.

Um die strikte Trennung der Schichten von Modell, Ansicht und ViewModel zu erzielen, werde ich jede einzelne Schicht als separates Visual Studio-Projekt erstellen. Zusammen mit den erneut einsetzbaren Dienstprogrammen, der Assembly für die Hauptausführung und allen Projekten für Komponententests (und davon gibt es meist viele, richtig?) führt dies zu einer Vielzahl von Projekten und Assemblys, wie auch in Abbildung 1 veranschaulicht wird.

Figure 1 The Components of an MVVM Application

Abbildung 1 Komponenten einer MVVM-Anwendung

Bei der hohen Anzahl an Projekten ist dieser Ansatz der strikten Trennung am besten für Großprojekte geeignet. Für kleine Anwendungen, an denen nur ein oder zwei Entwickler arbeiten, wiegen die Vorteile der strikten Trennung wahrscheinlich nicht den hohen Aufwand beim Erstellen, Konfigurieren und Verwalten mehrerer Projekte auf. Eine einfache Aufteilung von Code in verschiedene Namespaces innerhalb des gleichen Projekts bietet hier sicherlich eine völlig ausreichende Trennung.

Das Schreiben und Verwalten von ViewModel ist nicht einfach und sollte nicht leichtfertig begonnen werden. Die Antwort auf die grundlegendsten Fragen – wann kann das MVVM-Entwurfsmuster angewendet werden und wann ist das nicht sinnvoll – findet sich in den meisten Fällen im Domänenmodell.

Bei großen Projekten kann das Domänenmodell sehr komplex sein und Hunderte Klassen umfassen, die umsichtig für die elegante Zusammenarbeit in allen denkbaren Anwendungen entworfen wurden, darunter auch Webdienste, WPF- oder ASP.NET-Anwendungen. Im Modell können mehrere Assemblys zusammenarbeiten, und in großen Organisationen wird das Domänenmodell teilweise von einem sehr spezialisierten Entwicklungsteam erstellt und betreut.

Ist ein umfassendes und komplexes Domänenmodell vorhanden, ist es fast immer angeraten, eine ViewModel-Schicht einzuführen.

Andererseits gibt es auch einfache Domänenmodelle, die möglicherweise nur eine dünne Schicht über der Datenbank darstellen. Die Klassen werden womöglich automatisch generiert und implementieren häufig INotifyPropertyChanged. Die Benutzeroberfläche umfasst in der Regel eine Auflistung von Listen oder Rastern mit Bearbeitungsformularen, sodass der Benutzer in der Lage ist, die zugrunde liegenden Daten zu modifizieren. Das Microsoft-Toolset hat sich als bestens geeignet für die schnelle und einfache Erstellung dieser Art von Anwendung erwiesen.

Wenn Ihr Modell oder Ihre Anwendung in diese Kategorie fällt, erfordert ViewModel einen unter Umständen inakzeptablen Mehraufwand, ohne dass Ihr Anwendungsentwurf entsprechend davon profitiert.

Natürlich kann ViewModel auch in solchen Fällen sinnvoll sein. Beispielsweise eignet sich ViewModel optimal für die Implementierung der Rückgängig-Funktion. Alternativ können Sie MVVM auch nur für einen Teil der Anwendung auswählen (z. B. für die Dokumentverwaltung, ich komme später noch darauf zurück) und das Modell ganz pragmatisch direkt für die Ansicht bereitstellen.

Gründe für die Verwendung von ViewModel

Wenn ViewModel für Ihre Anwendung geeignet ist, sind noch einige Fragen zu beantworten, bevor Sie beginnen können, Code zu schreiben. Eine davon ist die Frage, wie die Anzahl der Proxyeigenschaften gesenkt werden kann.

Die durch das MVVM-Entwurfsmuster geförderte Trennung von Ansicht und Modell ist ein wichtiger und wertvoller Aspekt des Musters. Im Ergebnis heißt das, wenn eine Klasse des Modells 10 Eigenschaften hat, die in der Ansicht dargestellt werden sollen, weist in der Regel auch ViewModel 10 identische Eigenschaften auf, die den Aufruf einfach an die zugrunde liegende Modellinstanz weiterleiten. Sind die Proxyeigenschaften so festgelegt, dass sie eine Änderung an einer Eigenschaft an die Ansicht weiterleiten sollen, lösen sie meist ein Ereignis mit der geänderten Eigenschaft aus.

Nicht jede Eigenschaft des Modells muss über eine ViewModel-Proxyeigenschaft verfügen, aber für jede Modelleigenschaft, die in der Ansicht dargestellt werden soll, ist in der Regel eine Proxyeigenschaft vorhanden. Proxyeigenschaften sehen meist folgendermaßen aus:

public string Description {
  get { 
    return this.UnderlyingModelInstance.Description; 
  }
  set {
    this.UnderlyingModelInstance.Description = value;
    this.RaisePropertyChangedEvent("Description");
  }
}

Alle bedeutenden Anwendungen verfügen über Dutzende oder Hunderte von Modellklassen, die auf diese Weise über ViewModel für den Benutzer angezeigt werden sollen. Das ist absolut charakteristisch für die von MVVM gebotene Trennung.

Das Schreiben dieser Proxyeigenschaften ist ermüdend und somit sehr fehleranfällig. Ganz besonders, weil das Auslösen des Ereignisses mit der geänderten Eigenschaft eine Zeichenfolge erfordert, die dem Namen der Eigenschaft entspricht (und nicht in der automatischen Codeneustrukturierung enthalten ist). Die gängige Lösung zur Eliminierung dieser Proxyereignisse besteht darin, die Modellinstanz vom ViewModel-Wrapper direkt bereitzustellen und die INotifyPropertyChanged-Schnittstelle in das Domänenmodell zu implementieren:

public class SomeViewModel {
  public SomeViewModel( DomainObject domainObject ) {
    Contract.Requires(domainObject!=null, 
      "The domain object to wrap must not be null");
    this.WrappedDomainObject = domainObject;
  }
  public DomainObject WrappedDomainObject { 
    get; private set; 
  }
...

So kann ViewModel die Befehle und zusätzlichen Eigenschaften bereitstellen, die von der Ansicht benötigt werden, ohne Eigenschaften des Modells zu duplizieren oder zahlreiche Proxyeigenschaften zu erstellen. Dieser Ansatz hat sicherlich seinen Reiz, besonders wenn die Modellklassen bereits die INotifyPropertyChanged-Schnittstelle implementieren. Diese Schnittstelle in das Modell zu implementieren, ist eine gute Idee und sogar bei Microsoft .NET Framework 2.0- und Windows Forms-Anwendungen gängige Praxis. Sie verkompliziert jedoch das Domänenmodell und ist bei ASP.NET-Anwendungen oder Domänendiensten nicht sinnvoll.

Bei diesem Ansatz entsteht eine Abhängigkeit der Ansicht vom Modell, aber es handelt sich um eine indirekte Abhängigkeit durch Datenbindung, bei der kein Projektverweis vom Ansichtsprojekt zum Modellprojekt erforderlich ist. Aus rein pragmatischen Gründen ist dieser Ansatz in manchen Fällen sinnvoll.

Jedoch verletzt er die Intention des MVVM-Entwurfsmusters und schränkt die Möglichkeit ein, zu einem späteren Zeitpunkt neue ViewModel-spezifische Funktionen (wie z. B. die Rückgängig-Funktion) implementieren zu können. Ich habe schon mit diesem Ansatz erstellte Szenarios gesehen, bei denen sehr viel überarbeitet werden musste. Stellen Sie sich die relativ häufig vorkommende Situation vor, dass eine Datenbindung für eine tief verschachtelte Eigenschaft vorliegt. Wenn der aktuelle Datenkontext für ViewModel in „Person“ besteht und für diese eine Adresse vorliegt, sieht die Datenbindung wahrscheinlich folgendermaßen aus:

{Binding WrappedDomainObject.Address.Country}

Sollten Sie jemals zusätzliche ViewModel-Funktionen für das Adressobjekt einführen wollen, müssen Sie die Datenbindungsverweise auf WrappedDomainObject.Address entfernen und stattdessen neue ViewModel-Eigenschaften verwenden. Das ist problematisch, da Updates für die XAML-Datenbindung (und womöglich auch der Datenkontext) nur schwer getestet werden können. Die Ansicht ist die einzige Komponente, die nicht über automatisierte und umfassende Regressionstests verfügt.

Dynamische Eigenschaften

Meine Lösung für die starke Zunahme der Proxyeigenschaften ist, die neue .NET Framework 4- und WPF-Unterstützung für dynamische Objekte und dynamische Methodenverteilung zu nutzen. Letztere ermöglicht es Ihnen, zur Laufzeit zu bestimmen, wie eine Eigenschaft gelesen oder auf diese geschrieben werden soll, die in der Klasse nicht vorhanden ist. Das heißt, Sie können alle eigens geschriebenen Proxyeigenschaften aus ViewModel löschen und trotzdem das zugrunde liegende Modell kapseln. Beachten Sie jedoch, dass die Bindung an dynamische Eigenschaften in Silverlight 4 nicht unterstützt wird.

Der einfachste Weg zur Implementierung dieser Funktion besteht darin, dass die ViewModel-Basisklasse die neue System.Dynamic.DynamicObject-Klasse erweitert und die Methoden TryGetMember und TrySetMember überschreibt. Diese beiden Methoden werden von der DLR (Dynamic Language Runtime) aufgerufen, wenn die Eigenschaft, auf die verwiesen wird, in der Klasse nicht vorhanden ist. So kann von der Klasse zur Laufzeit bestimmt werden, wie die fehlenden Eigenschaften implementiert werden sollen. In Kombination mit ein bisschen Reflektion kann mit nur wenigen Codezeilen der Eigenschaftenzugriff auf die zugrunde liegende Modellinstanz von der ViewModel-Klasse dynamisch weitergeleitet werden:

public override bool TryGetMember(
  GetMemberBinder binder, out object result) {

  string propertyName = binder.Name;
  PropertyInfo property = 
    this.WrappedDomainObject.GetType().GetProperty(propertyName);

  if( property==null || property.CanRead==false ) {
    result = null;
    return false;
  }

  result = property.GetValue(this.WrappedDomainObject, null);
  return true;
}

Zunächst wird von der Methode anhand von Reflektion in der zugrunde liegenden Modellinstanz nach der Eigenschaft gesucht. (Weitere Informationen finden Sie in der Ausgabe vom Juni 2007 in der Rubrik „Tiefe Einblicke in CLR“ im Artikel „Reflexionen über Reflektion“.) Ist eine solche Eigenschaft im Modell nicht vorhanden, wird von der Methode der Wert „false“ zurückgegeben, und die Datenbindung ist nicht erfolgreich. Wenn die Eigenschaft vorhanden ist, werden von der Methode die Eigenschaftendaten verwendet, um den Eigenschaftenwert für das Modell abzurufen und zurückzugeben. Das ist ein höherer Aufwand als mit der herkömmlichen get-Methode der Proxyeigenschaft, aber dafür auch die einzige Implementierung, die Sie für alle Modelle und alle Eigenschaften schreiben müssen.

Die wahre Leistung des Ansatzes der dynamischen Proxyeigenschaft liegt in den Eigenschaftensettern. In TrySetMember können Sie bekannte Logik einschließen, z. B. das Auslösen von Ereignissen mit der geänderten Eigenschaft. Der erforderliche Code sieht folgendermaßen aus:

public override bool TrySetMember(
  SetMemberBinder binder, object value) {

  string propertyName = binder.Name;
  PropertyInfo property = 
    this.WrappedDomainObject.GetType().GetProperty(propertyName);

  if( property==null || property.CanWrite==false )
    return false;

  property.SetValue(this.WrappedDomainObject, value, null);

  this.RaisePropertyChanged(propertyName);
  return true;
}

Wieder wird von der Methode anhand von Reflektion die Eigenschaft aus der zugrunde liegenden Modellinstanz abgerufen. Ist die Eigenschaft nicht vorhanden oder schreibgeschützt, wird von der Methode der Wert „false“ zurückgegeben. Wenn die Eigenschaft im Domänenobjekt vorhanden ist, wird unter Verwendung der Eigenschaftendaten die Eigenschaft für das Modell festgelegt. Dann können Sie für alle Eigenschaftensetter eine gemeinsame Logik einrichten. In diesem Codebeispiel löse ich nur das Ereignis mit der geänderten Eigenschaft für die gerade festgelegte Eigenschaft aus, aber Sie können natürlich auch mehr machen.

Eine der Herausforderungen beim Kapseln eines Modells liegt darin, dass ein Modell häufig über das verfügt, was in UML (Unified Modeling Language) als abgeleitete Eigenschaften bezeichnet wird. So hat beispielsweise die Person-Klasse vermutlich eine BirthDate-Eigenschaft und eine abgeleitete Age-Eigenschaft. Die Age-Eigenschaft ist schreibgeschützt und berechnet automatisch auf Basis des Geburtsdatums und des aktuellen Datums das Alter:

public class Person : DomainObject {
  public DateTime BirthDate { 
    get; set; 
  }

  public int Age {
    get {
      var today = DateTime.Now;
      // Simplified demo code!
      int age = today.Year - this.BirthDate.Year;
      return age;
    }
  }
...

Erfolgt eine Änderung der BirthDate-Eigenschaft, ändert sich implizit auch die Age-Eigenschaft, da das Alter anhand des Geburtsdatums errechnet wird. Wenn also die BirthDate-Eigenschaft festgelegt wird, muss von der ViewModel-Klasse sowohl für die BirthDate-Eigenschaft als auch die Age-Eigenschaft ein Ereignis mit der geänderten Eigenschaft ausgelöst werden. Mit dem dynamischen ViewModel-Ansatz können Sie das automatisieren, indem Sie die Beziehung der beiden Eigenschaften im Modell explizit festlegen.

Dazu benötigen Sie zunächst ein benutzerdefiniertes Attribut, um die Beziehung der Eigenschaften zu erfassen:

[AttributeUsage(AttributeTargets.Property, AllowMultiple=true)]
public sealed class AffectsOtherPropertyAttribute : Attribute {
  public AffectsOtherPropertyAttribute(
    string otherPropertyName) {
    this.AffectsProperty = otherPropertyName;
  }

  public string AffectsProperty { 
    get; 
    private set; 
  }
}

Ich lege AllowMultiple auf den Wert „true“ fest, damit Szenarios unterstützt werden, in denen eine Eigenschaft mehrere andere Eigenschaften beeinflussen kann. Dann wende ich dieses Attribut gleich an, um die Beziehung zwischen BirthDate und Age direkt im Modell festzuschreiben:

[AffectsOtherProperty("Age")]
public DateTime BirthDate { get; set; }

Damit ich die neuen Metadaten des Modells in der dynamischen ViewModel-Klasse verwenden kann, aktualisiere ich die TrySetMember-Methode durch Einfügen von drei zusätzlichen Codezeilen. Der Codeabschnitt sieht nun folgendermaßen aus:

public override bool TrySetMember(
  SetMemberBinder binder, object value) {
...
  var affectsProps = property.GetCustomAttributes(
    typeof(AffectsOtherPropertyAttribute), true);
  foreach(AffectsOtherPropertyAttribute otherPropertyAttr 
    in affectsProps)
    this.RaisePropertyChanged(
      otherPropertyAttr.AffectsProperty);
}

Da die reflektierten Eigenschafteninformationen bereits zur Verfügung stehen, können von der GetCustomAttributes-Methode alle AffectsOtherProperty-Attribute für die Modelleigenschaft zurückgegeben werden. Dann werden die Attribute vom Code einfach in einer Schleife durchlaufen, wobei für jedes ein Ereignis mit der geänderten Eigenschaft ausgelöst wird. Änderungen an der BirthDate-Eigenschaft durch ViewModel lösen nun automatisch für BirthDate und Age Ereignisse mit der geänderten Eigenschaft aus.

Beachten Sie hier unbedingt, dass bei der expliziten Programmierung einer Eigenschaft für die dynamische ViewModel-Klasse (oder, noch wahrscheinlicher, für modellspezifische, abgeleitete ViewModel-Klassen) die DLR die Eigenschaften direkt aufruft, anstatt die Methoden TryGetMember und TrySetMember aufzurufen. In einem solchen Fall ist das automatische Verhalten nicht möglich. Der Codeabschnitt kann jedoch problemlos umgeschrieben werden, damit diese Funktion auch für benutzerdefinierte Eigenschaften nutzbar ist.

Nun kehren wir wieder zum Problem der Datenbindung bei einer tief geschachtelten Eigenschaft zurück (wobei ViewModel den aktuellen WPF-Datenkontext bildet), die wie folgt aussieht:

{Binding WrappedDomainObject.Address.Country}

Die Verwendung von dynamischen Proxyeigenschaften hat zur Folge, dass das umbrochene, zugrunde liegende Domänenobjekt nicht länger verfügbar ist. Folglich sieht die Datenbindung so aus:

{Binding Address.Country}

In diesem Fall würde die Address-Eigenschaft weiterhin direkt auf die zugrunde liegende Address-Instanz des Modells zugreifen. Wenn Sie nun allerdings ViewModel für Address einführen wollen, können Sie einfach eine neue Eigenschaft zur Person-Klasse von ViewModel hinzufügen. Die neue Address-Eigenschaft ist recht einfach:

public DynamicViewModel Address {
  get {
    if( addressViewModel==null )
      addressViewModel = 
        new DynamicViewModel(this.Person.Address);
    return addressViewModel;
  }
}

private DynamicViewModel addressViewModel;

Die XAML-Datenbindung muss nicht geändert werden, weil die Eigenschaft immer noch die Bezeichnung Address trägt, aber nun wird von der DLR anstelle der dynamischen TryGetMember-Methode die neue konkrete Eigenschaft aufgerufen. (Beachten Sie, dass die verzögerte Instanziierung innerhalb dieser Address-Eigenschaft nicht threadsicher ist. Allerdings sollte nur die Ansicht auf ViewModel zugreifen, und da es sich in WPF/Silverlight um eine Singlethread-Ansicht handelt, stellt dies kein Problem dar.)

Dieser Ansatz kann auch verwendet werden, wenn das Modell INotifyPropertyChanged implementiert. Dies wird von ViewModel erkannt, woraufhin entschieden wird, Ereignisse mit der geänderten Eigenschaft nicht weiterzuleiten. In einem solchen Fall ruft ViewModel sie von der zugrunde liegenden Modellinstanz ab und löst die Ereignisse selbst erneut aus. Im Konstruktor der dynamischen ViewModel-Klasse führe ich die Prüfung aus und achte auf das Ergebnis:

public DynamicViewModel(DomainObject model) {
  Contract.Requires(model != null, 
    "Cannot encapsulate a null model");
  this.ModelInstance = model;

  // Raises its own property changed events
  if( model is INotifyPropertyChanged ) {
    this.ModelRaisesPropertyChangedEvents = true;
    var raisesPropChangedEvents = 
      model as INotifyPropertyChanged;
    raisesPropChangedEvents.PropertyChanged +=
      (sender,args) => 
      this.RaisePropertyChanged(args.PropertyName);
  }
}

Um doppelte Ereignisse mit der geänderten Eigenschaft zu unterbinden, ist noch eine kleine Modifikation an der TrySetMember-Methode erforderlich.

if( this.ModelRaisesPropertyChangedEvents==false )
  this.RaisePropertyChanged(property.Name);

Mithilfe einer dynamischen Proxyeigenschaft können Sie die ViewModel-Schicht erheblich vereinfachen, da Sie die standardisierten Proxyeigenschaften nicht länger benötigen. Das sorgt für eine maßgebliche Reduzierung von Code, Tests, Dokumentation und langfristigen Verwaltungstätigkeiten. Durch das Hinzufügen neuer Eigenschaften zum Modell ist keine Aktualisierung der ViewModel-Schicht mehr nötig (außer wenn für die neue Eigenschaft eine sehr spezielle Ansichtslogik vorhanden ist). Zudem können mit diesem Ansatz komplexe Problematiken wie verwandte Eigenschaften gelöst werden. Da alle vom Benutzer vorgenommenen Eigenschaftenänderungen die TrySetMember-Methode durchlaufen, ermöglicht Ihnen diese gemeinsame Methode die Implementierung einer Rückgängig-Funktion.

Vor- und Nachteile

Viele Entwickler misstrauen der Reflektion (und der DLR) aufgrund von Bedenken bezüglich der Leistung. Bei meiner eigenen Arbeit ist bisher kein solches Problem aufgetreten. Die Leistungseinbuße, die beim Festlegen einer einzelnen Eigenschaft in der Benutzeroberfläche entsteht, ist für den Benutzer kaum spürbar. Möglicherweise ist das bei hochgradig interaktiven Benutzeroberflächen, wie z. B. Mehrfinger-Entwurfsoberflächen, anders.

Das einzig echte Leistungsproblem besteht bei der ersten Auffüllung der Ansicht, wenn viele Felder gefüllt werden müssen. Mit Rücksicht auf die Nutzbarkeit wird die Anzahl der auf einem Bildschirm angezeigten Felder naturgemäß beschränkt, sodass der Leistungsabfall bei initialen Datenbindungen mit diesem DLR-Ansatz unbemerkt bleibt.

Dennoch sollte die Leistung immer sorgfältig überwacht und nachvollzogen werden, da die Benutzerfreundlichkeit in starkem Maße davon abhängt. Der zuvor beschriebene einfache Ansatz kann mit der Reflektionszwischenspeicherung umgeschrieben werden. Weitere Informationen zu diesem Thema finden Sie im Artikel von Joel Pobar in der Ausgabe vom Juli 2005 im MSDN Magazin.

Das Argument, dass Codezuverlässigkeit und -wartbarkeit bei diesem Ansatz negativ beeinträchtigt werden, muss berücksichtigt werden, da die Ansichtsschicht auf Eigenschaften in ViewModel verweist, die gar nicht existieren. Ich denke jedoch, dass die Vorteile, die aus der Eliminierung der meisten handcodierten Proxyeigenschaften resultieren, die Probleme – besonders bei einer guten ViewModel-Dokumentation – bei weitem aufwiegen.

Durch den Ansatz der dynamischen Proxyeigenschaften wird die Möglichkeit der Verschleierung der Modellschicht reduziert oder sogar eliminiert, da nun per Name in XAML auf die Eigenschaften des Modells verwiesen wird. Das Verwenden herkömmlicher Proxyeigenschaften senkt die Verschleierungsmöglichkeit des Modells nicht, weil direkt auf die Eigenschaften verwiesen wird und so eine Verschleierung mit den restlichen Anwendungskomponenten entstehen würde. Da die meisten Tools zur Verschleierung bisher noch nicht mit XAML/BAML zusammenarbeiten, ist das im Grunde genommen irrelevant. Wenn jemand den Code knacken möchte, kann er mit XAML/BAML beginnen und sich dann so oder so in die Modellschicht vorarbeiten.

Und schließlich könnte dieser Ansatz auch fehlerhaft eingesetzt werden, indem die Modelleigenschaften mit sicherheitsrelevanten Metadaten verknüpft werden, damit per ViewModel eine höhere Sicherheit erzwungen werden kann. Der Sicherheitsaspekt gehört nicht in den ViewModel-Verantwortungsbereich, und ich denke auch, damit käme ViewModel zu viel Wichtigkeit zu. In dieser Hinsicht wäre ein im Rahmen des Modells angewendeter aspektbasierter Ansatz deutlich sinnvoller.

Auflistungen

Die Auflistungen zählen zu den schwierigsten und am wenigsten befriedigenden Aspekten des MVVM-Entwurfsmusters. Wenn eine Auflistung im zugrunde liegenden Modell vom Modell geändert wird, obliegt es ViewModel, diese Änderung auf irgendeine Art und Weise so offenzulegen, dass die Ansicht sich selbst entsprechend aktualisieren kann.

Unglücklicherweise kann das Modell keine Auflistungen bereitstellen, welche die INotifyCollectionChanged-Schnittstelle implementieren. In .NET Framework 3.5 befindet sich diese Schnittstelle in der Datei System.Windows.dll, wodurch von deren Verwendung im Modell stark abgeraten wird. Aber zum Glück ist diese Schnittstelle in .NET Framework 4 in die Datei System.dll migriert worden, sodass die Nutzung sichtbarer Auflistungen von innerhalb des Modells wesentlich natürlicher geworden ist.

Sichtbare Auflistungen im Modell eröffnen neue Möglichkeiten für die Modellentwicklung und könnten in Windows Forms- und Silverlight-Anwendungen verwendet werden. Das ist derzeit der von mir bevorzugte Ansatz, da er einfacher ist als alle anderen Methoden. Außerdem bin ich froh, dass die INotifyCollectionChanged-Schnittstelle in eine gängigere Assembly verschoben wurde.

Ohne sichtbare Auflistungen im Modell kann bestenfalls ein anderer Mechanismus – höchstwahrscheinlich benutzerdefinierte Ereignisse – für das Modell verfügbar gemacht werden, um eine Änderung in der Auflistung anzugeben. Dies sollte auf modellspezifische Art und Weise geschehen. Wenn beispielsweise die Person-Klasse über eine Adressauflistung verfügt, können folgende Ereignisse bereitgestellt werden:

public event EventHandler<AddressesChangedEventArgs> 
  NewAddressAdded;
public event EventHandler<AddressesChangedEventArgs> 
  AddressRemoved;

Dies ist dem Auslösen eines Ereignisses für eine benutzerdefinierte Auflistung, das speziell für ViewModel in WPF entworfen wurde, vorzuziehen. Dennoch bleibt es weiterhin schwierig, Änderungen der Auflistung in ViewModel offenzulegen. Die einzige Möglichkeit besteht darin, ein Ereignis mit der geänderten Eigenschaft für die gesamte ViewModel-Auflistungseigenschaft auszulösen. Das ist bestenfalls eine unbefriedigende Lösung.

Ein weiteres Problem bei Auflistungen liegt in der Bestimmung, wann oder ob die einzelnen Modellinstanzen in der Auflistung innerhalb einer ViewModel-Instanz umbrochen werden. Bei kleineren Auflistungen kann von ViewModel eine neue sichtbare Auflistung erzeugt werden, dann werden alle Daten von der zugrunde liegenden Modellauflistung in die sichtbare ViewModel-Auflistung kopiert, wobei jedes Modellelement in der Auflistung in eine entsprechende ViewModel-Instanz umbrochen wird. Möglicherweise muss von ViewModel dann auf Ereignisse zur geänderten Auflistung gewartet werden, damit eine Weitergabe der Benutzeränderungen an das zugrunde liegende Modell möglich ist.

Bei sehr großen Auflistungen, die in einer Art virtualisiertem Bereich verfügbar gemacht werden, ist die einfachste und praktischste Herangehensweise, die Modellobjekte einfach direkt bereitzustellen.

Instanziieren von ViewModel

Ein weiteres Problem besteht bei dem MVVM-Entwurfsmuster darin, dass nur selten darüber gesprochen wird, wo und wann die ViewModel-Instanzen instanziiert werden sollten. Auch bei Diskussionen zu ähnlichen Entwurfsmustern (wie z. B. MVC) kommt dieses Thema nur selten zur Sprache.

Ich ziehe es vor, ein ViewModel-Singleton-Muster zum Bereitstellen der wichtigsten ViewModel-Objekte zu schreiben, über welche die Ansicht alle anderen ViewModel-Objekte je nach Bedarf abrufen kann. Häufig stellt dieses ViewModel-Hauptobjekt die Befehlsimplementierung bereit, sodass die Ansicht das Öffnen von Dokumenten unterstützt.

Die meisten Anwendungen, mit denen ich bisher gearbeitet habe, bieten jedoch eine dokumentorientierte Schnittstelle, gewöhnlich mit einem Arbeitsbereich im Registerkartenformat, ähnlich wie in Visual Studio. Also stelle ich mir die ViewModel-Schicht als dokumentorientiert vor, und die Dokumente stellen ein oder mehrere ViewModel-Objekte mit bestimmten umbrochenen Modellobjekten bereit. Dann können die WPF-Standardbefehle in der ViewModel-Schicht die Persistenzschicht nutzen, um die erforderlichen Objekte abzurufen, in ViewModel-Instanzen umzubrechen und ViewModel-Dokument-Manager für deren Anzeige zu erstellen.

In der Beispielanwendung für diesen Artikel lautet der ViewModel-Befehl zum Erstellen einer neuen Person folgendermaßen:

internal class OpenNewPersonCommand : ICommand {
...
  // Open a new person in a new window.
  public void Execute(object parameter) {
    var person = new MvvmDemo.Model.Person();
    var document = new PersonDocument(person);
    DocumentManager.Instance.ActiveDocument = document;
  }
}

Der ViewModel-Dokument-Manager, auf den in der letzten Zeile verwiesen wird, ist ein Singleton zur Verwaltung aller offenen ViewModel-Dokumente. Die Frage ist, wie wird die Auflistung der ViewModel-Dokumente für die Ansicht bereitgestellt?

Das in WPF integrierte Registerkarten-Steuerelement bietet nicht die von den Benutzern erwartete leistungsstarke Schnittstelle für mehrere Dokumente. Aber praktischerweise sind Andockprodukte sowie Produkte für den Arbeitsbereich im Registerkartenformat von Drittanbietern verfügbar. Die meisten bemühen sich, das Dokumentlayout im Registerkartenformat von Visual Studio zu emulieren, einschließlich der andockbaren Toolfenster, geteilten Ansichten, mit Strg+Tab aufrufbaren Popupfenster (mit Miniatur-Dokumenansichten) und mehr.

Leider verfügen die meisten dieser Komponenten nicht über eine integrierte Unterstützung für das MVVM-Entwurfsmuster. Das ist jedoch kein Problem, denn mit dem Adapter-Entwurfsmuster kann der ViewModel-Dokument-Manager einfach mit der Ansichtskomponente des Drittanbieters verknüpft werden.

Adapter für Dokument-Manager

Der in Abbildung 2 gezeigte Adapterentwurf stellt sicher, dass ViewModel keinerlei Verweise auf die Ansicht benötigt, und damit wird das Hauptziel des MVVM-Entwurfsmusters respektiert. (In diesem Fall wird das Konzept eines Dokuments jedoch in der ViewModel-Schicht anstatt in der Modellschicht definiert, da es sich um ein reines Benutzeroberflächenkonzept handelt.)

Figure 2 Document Manager View Adapter

Abbildung 2 Ansichtsadapter für Dokument-Manager

Der Dokument-Manager von ViewModel ist dafür zuständig, die Auflistung offener ViewModel-Dokumente zu verwalten. Zudem muss ihm bekannt sein, welches Dokument derzeit geöffnet ist. Dieser Entwurf ermöglicht es der ViewModel-Schicht, Dokumente mit dem Dokument-Manager zu öffnen und zu schließen sowie das aktive Dokument zu ändern, ohne dass Informationen über die Ansicht bekannt sind. Die ViewModel-Seite dieses Ansatzes ist vernünftigerweise unkompliziert. Die ViewModel-Klassen der Beispielanwendung werden in Abbildung 3 dargestellt.

Figure 3 The ViewModel Layer’s Document Manager and Document Classes

Abbildung 3 Dokument-Manager und Dokumentklassen der ViewModel-Schicht

Die Document-Basisklasse stellt mehrere interne Lebenszyklusmethoden (Activated, LostActivation und DocumentClosed) bereit, die vom Dokument-Manager zur Aktualisierung des Dokuments aufgerufen werden. Vom Dokument wird auch eine INotifyPropertyChanged-Schnittstelle implementiert, sodass die Datenbindung unterstützt wird. Beispielsweise erfolgt anhand des Adapters eine Datenbindung der Title-Eigenschaft des Ansichtsdokuments mit der DocumentTitle-Eigenschaft von ViewModel.

Bei diesem Ansatz weist die Adapterklasse die größte Komplexität auf. Ich habe im Projekt zu diesem Artikel eine Arbeitskopie dazu bereitgestellt. Mit dem Adapter werden verschiedene Ereignisse für den Dokument-Manager abonniert, mit denen das Steuerelement des Arbeitsbereichs im Registerkartenformat aktualisiert wird. Wenn zum Beispiel vom Dokument-Manager vermittelt wird, dass ein neues Dokument geöffnet worden ist, empfängt der Adapter ein Ereignis, woraufhin das ViewModel-Dokument im jeweils benötigten WPF-Steuerelement umbrochen und dieses Steuerelement dann im Arbeitsbereich im Registerkartenformat angezeigt wird.

Der Adapter erfüllt noch eine weitere Aufgabe: Er sorgt dafür, dass der Dokument-Manager von ViewModel mit den Benutzeraktionen synchronisiert wird. Demzufolge muss der Adapter auch auf Ereignisse vom Steuerelement des Arbeitsbereichs im Registerkartenformat achten, damit der Dokument-Manager vom Adapter benachrichtigt werden kann, wenn der Benutzer das aktive Dokument ändert oder schließt.

Die Logik ist nicht sehr komplex, dennoch sind einige Vorsichtsmaßnahmen zu beachten. In mehreren Szenarios ist eintrittsinvarianter Code vorhanden, mit dem auf bestimmte Weise umgegangen werden muss. Wenn z. B. ViewModel den Dokument-Manager zum Schließen eines Dokuments verwendet, empfängt der Adapter das Ereignis vom Dokument-Manager und schließt das „reale“ Dokumentfenster in der Ansicht. Dadurch wird das Steuerelement des Arbeitsbereichs im Registerkartenformat veranlasst, auch ein Ereignis zum Schließen des Dokuments auszulösen, das ebenfalls vom Adapter empfangen wird. Daraufhin erfolgt über den Ereignishandler des Adapters eine Benachrichtigung an den Dokument-Manager, dass das Dokument geschlossen werden soll. Da dies bereits geschehen ist, muss der Dokument-Manager flexibel genug sein, um das zuzulassen.

Eine weitere Schwierigkeit besteht darin, dass der Ansichtsadapter in der Lage sein muss, eine Verknüpfung zwischen einem Steuerelement für ein Dokument im Registerkartenformat der Ansicht und einem ViewModel-Dokumentobjekt herzustellen. Als stabilste Lösung hat sich die Verwendung einer Abhängigkeitseigenschaft erwiesen, die an WPF angefügt ist. Vom Adapter wird eine nicht öffentliche, angefügte Abhängigkeitseigenschaft deklariert, die zum Verknüpfen des Steuerelements des Ansichtsfensters mit der zugehörigen ViewModel-Dokumentinstanz verwendet wird.

Im Beispielprojekt zu diesem Artikel kann ich eine Open-Source-Komponente namens AvalonDock für den Arbeitsbereich im Registerkartenformat nutzen, mit der meine angefügte Abhängigkeitseigenschaft dem in Abbildung 4 angezeigten Codeabschnitt entspricht.

Abbildung 4 Verknüpfen von Ansichtssteuerelement und ViewModel-Dokument

private static readonly DependencyProperty 
  ViewModelDocumentProperty =
  DependencyProperty.RegisterAttached(
  "ViewModelDocument", typeof(Document),
  typeof(DocumentManagerAdapter), null);

private static Document GetViewModelDocument(
  AvalonDock.ManagedContent viewDoc) {

  return viewDoc.GetValue(ViewModelDocumentProperty) 
    as Document;
}

private static void SetViewModelDocument(
  AvalonDock.ManagedContent viewDoc, Document document) {

  viewDoc.SetValue(ViewModelDocumentProperty, document);
}

Wenn vom Adapter ein neues Steuerelement für das Fenster der Ansicht generiert wird, wird dabei die angefügte Eigenschaft für das neue Fenstersteuerelement im zugrunde liegenden ViewModel-Dokument festgelegt (siehe Abbildung 5). Wie Sie sehen, wird hier auch die Datenbindung für den Titel konfiguriert. Außerdem erfolgt die adapterseitige Konfiguration von Datenkontext und Inhalt des Steuerelements für das Dokument in der Ansicht.

Abbildung 5 Festlegen der angefügten Eigenschaft

private AvalonDock.DocumentContent CreateNewViewDocument(
  Document viewModelDocument) {

  var viewDoc = new AvalonDock.DocumentContent();
  viewDoc.DataContext = viewModelDocument;
  viewDoc.Content = viewModelDocument;

  Binding titleBinding = new Binding("DocumentTitle") { 
    Source = viewModelDocument };

  viewDoc.SetBinding(AvalonDock.ManagedContent.TitleProperty, 
    titleBinding);
  viewDoc.Closing += OnUserClosingDocument;
  DocumentManagerAdapter.SetViewModelDocument(viewDoc, 
    viewModelDocument);

  return viewDoc;
}

Indem ich den Inhalt für das Steuerelement des Dokuments in der Ansicht festlege, kann ich WPF die komplizierte Berechnung überlassen, wie dieser bestimmte ViewModel-Dokumenttyp angezeigt werden soll. Die tatsächlichen Datenvorlagen für ViewModel-Dokumente befinden sich in einem Ressourcenwörterbuch, das in das XAML-Hauptfenster eingebunden ist.

Diesen Ansatz mit Dokument-Manager und ViewModel habe ich sowohl mit WPF als auch Silverlight erfolgreich angewendet. Der einzige Code der Ansichtsschicht ist der Adapter, und der Code kann leicht getestet und dann sich selbst überlassen werden. Bei diesem Ansatz bleibt ViewModel vollständig unabhängig von der Ansicht. Ich habe einmal den Anbieter meiner Komponente für den Arbeitsbereich im Registerkartenformat gewechselt – es waren nur minimale Änderungen an der Adapterklasse und absolut gar keine Änderungen an ViewModel oder dem Modell erforderlich.

Die Möglichkeit, mit Dokumenten in der ViewModel-Schicht zu arbeiten, ist elegant, und das Implementieren von ViewModel-Befehlen ist, wie ich hier demonstrieren konnte, wirklich einfach. Zudem haben sich ViewModel-Dokumentklassen als gut geeignet für die Bereitstellung von dokumentbezogenen ICommand-Instanzen erwiesen.

Die Ansicht stellt eine Verbindung zu diesen Befehlen her, und dann schimmern Schönheit und Eleganz des MVVM-Entwurfsmusters durch. Außerdem ist es mit dem Dokument-Manager und ViewModel möglich, den Singleton-Ansatz zu verwenden, sofern Sie Daten bereitstellen wollen, bevor der Benutzer Dokumente erstellt hat (möglicherweise in einem reduzierbaren Toolfenster).

Zusammenfassung

Das MVVM-Entwurfsmuster ist ein leistungsstarkes und nützliches Muster – aber kein Entwurfsmuster kann alle Probleme beheben. Wie ich hier veranschaulicht habe, ist eine Kombination von MVVM-Muster und -Zielen mit anderen Mustern (wie z. B. Adaptern und Singletons) bei gleichzeitiger Nutzung der neuen .NET Framework 4-Features (wie z. B. dynamische Verteilung) gut geeignet, um viele häufig auftretende Probleme in Bezug auf die Implementierung des MVVM-Entwurfsmusters zu lösen. Die richtige Nutzung und Verwendung von MVVM ermöglicht sehr viel elegantere und einfacher verwaltbare WPF- und Silverlight-Anwendungen. Weitere Informationen über MVVM finden Sie im Artikel von Josh Smith in der Ausgabe Februar 2009 im MSDN Magazin.

Robert McCarter ist ein freiberuflicher Softwareentwickler und -architekt sowie Unternehmer aus Kanada. Unter der Adresse robertmccarter.wordpress.com können Sie seinen Blog lesen.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Josh Smith