ViewModelBase-Klasse

Basisklasse für ViewModels nach dem MVVM-Muster, mit einigen Vereinfachungen für abgeleitete Klassen.

Wahrscheinlich gibt es in jedem WPF-Projekt, das nach dem MVVM-Muster (Model–View–ViewModel) aufgebaut ist, irgendwo eine Klasse mit dem Namen „ViewModelBase“. Sie dient als Basisklasse für alle ViewModel-Klassen der Anwendung und stellt meist eine Implementierung der INotifyPropertyChanged-Schnittstelle bereit. Im einfachsten Fall ist das neben dem Ereignis noch eine geschützte Methode wie „OnPropertyChanged“ oder auch „RaisePropertyChanged“.

Das allein macht dem Programmierer das Leben aber noch nicht einfacher, wenn in zahlreichen Klassen ebenso zahlreiche Eigenschaften implementiert werden, die noch dazu Abhängigkeiten untereinander besitzen (berechnete Eigenschaften).

Meine Implementierung dieser Klasse stellt die folgenden Vereinfachungen für WPF-Anwendungen bereit:

  • DisplayName-Eigenschaft
    Gemeinsame DisplayName-Eigenschaft für alle ViewModel-Klassen. Sie kann dafür verwendet werden, ein Objekt in Listen darzustellen. Bei Fenstern dient sie auch als Fenstertitel.
  • Infrastruktur für Commands
    Die virtuelle InitializeCommands-Methode wird vom Konstruktor aufgerufen und ermöglicht es in abgeleiteten Klassen, den Command-Code an einer Stelle zusammenzufassen.
  • GetValue, SetValue
    GetValue- und SetValue-Methoden, die die Notwendigkeit von Backing Fields in Klassen vermeiden. Sie verhalten sich ein bisschen so wie DependencyPropertys.
  • Bereinigen von Texteingaben
    Methoden zum Bereinigen von Texteingaben für Zahlen, Datumswerte usw. ViewModel-Klassen müssen oft einen speziellen Datentyp (int, DateTime…) für die Benutzereingabe in einen String konvertieren. Da dieser vom Benutzer in einer TextBox frei eingegeben werden kann, sollte er beim Aktualisieren bereinigt und neu formatiert werden.
  • Validierung
    Validierung von Eigenschaften zur Hervorhebung von Eingabefehlern und Anzeige von Fehlermeldungen. Mit Unterstützung für Listen von untergeordneten Objekten, die selbst bearbeitet werden und Fehler melden können, aber in derselben View dargestellt werden.
  • INotifyPropertyChanged-Implementierung
    OnPropertyChanged-Methode mit Unterstützung für CallerMemberName (ab C# 5), mehrere Eigenschaftsnamen in einem Aufruf, Expressions und Prüfung der angegebenen Eigenschaftsnamen auf Existenz in der Klasse (nur Debug-Build).
  • Abhängige Eigenschaften (umgekehrte Benachrichtigung)
    Deklarative Definition abhängiger Eigenschaften, für die ebenfalls eine Benachrichtigung ausgelöst werden muss, wenn sich die verwendete Eigenschaft ändert. Dafür wird die abhängige Eigenschaft mit dem NotifiesOn-Attribut gekennzeichnet, statt dass die geänderte Eigenschaft auch für alle anderen Eigenschaften benachrichtigt, die diesen Wert evtl. nutzen.
  • Abhängige Commands (umgekehrte Benachrichtigung)
    Deklarative Definition abhängiger Commands, deren Verfügbarkeit (CanExecute) sich ändert, wenn sich eine bestimmte Eigenschaft ändert. Dafür wird das abhängige Command mit dem NotifiesOn-Attribut gekennzeichnet, statt dass die geänderte Eigenschaft selbst alle Commands aktualisiert, die diesen Wert evtl. nutzen.
  • IsModified-Verwaltung
    Deklarative Kennzeichnung der Eigenschaften, bei deren Änderung die bereitgestellte IsModified-Eigenschaft auf true gesetzt wird.
  • ViewState
    Stellt einen flexiblen Speicher für den aktuellen Zustand der View beim Vor- und Zurücknavigieren bereit, vergleichbar mit der ViewBag in ASP.NET MVC.
  • Änderungsmethoden
    Deklarative Kennzeichnung von Änderungsmethoden, die aufgerufen werden sollen, wenn sich eine Eigenschaft ändert. Dafür wird die Handler-Methode mit dem PropertyChangedHandler-Attribut gekennzeichnet, das angibt, welche Eigenschaft beachtet wird.
  • Spezielle Ableitungen
    Definition spezieller ViewModel-Klassen für einfache Anwendungen: EmptyViewModel, ValueViewModel<T>

Manche Ideen sind von Steve Cadwallader übernommen, der eine ausführlichere Beschreibung in drei Artikeln bereitstellt (1, 2, 3).

Kompatibilität: .NET Ab Version 4.0

Diese Funktionalität mit noch weniger Schreibaufwand, voller Performance und geringerem Speicherverbrauch gibt es mit dem neueren Fody-Addin ViewModelKit.Fody.

Beispiel

Die folgende Beispielklasse zeigt eine Ableitung der ViewModelBase-Klasse:

public class PersonViewModel : ViewModelBase
{
    #region Data properties

    // Ersetzung der gemeinsamen DisplayName-Eigenschaft
    public override string DisplayName
    {
        get { return Vorname + " " + Nachname; }
        set { }
    }
   
    // Verwendung von GetValue und SetValue mit C# 4 (.NET 4.0, Visual Studio 2010)
    public string Vorname
    {
        get { return GetValue<string>("Vorname"); }
        set { SetValue(value, "Vorname"); }
    }

    // Verwendung von GetValue und SetValue mit C# 5 (.NET 4.5, Visual Studio 2012+)
    public string Nachname
    {
        get { return GetValue<string>(); }
        set { SetValue(value); }
    }
   
    // Änderungsmethode wird aufgerufen, wenn Nachname geändert wurde (nachdem der Wert
    // gespeichert und bevor das PropertyChanged-Ereignis ausgelöst wird)
    [PropertyChangedHandler("Nachname")]
    private void OnNachnameChanged()
    {
        if (knownNames.Contains(Nachname))
        {
            SayHelloCommand.TryExecute();
        }
    }
   
    // Neuformatierung eines Datums als Texteingabe;
    // Änderung dieser Eigenschaft setzt IsModified auf true
    [SetsModified]
    public string Geburtsdatum
    {
        get { return GetValue<string>(); }
        set { SetValue(SanitizeDate(value)); }
    }
   
    // Ändert sich mit Geburtsdatum
    [NotifiesOn("Geburtsdatum")]
    public int Alter
    {
        get
        {
            return (DateTime.Now - DateTime.Parse(Geburtsdatum)).Years;   // Pseudocode!
        }
    }

    #endregion Data properties
   
    #region Commands

    // Deklaration aller Commands;
    // SayHello ist vom aktuellen Geburtsdatum abhängig
    [NotifiesOn("Geburtsdatum")]
    public DelegateCommand SayHelloCommand { get; private set; }

    // Initialisierung der Commands, wird vom Konstruktor aufgerufen
    protected override void InitializeCommands()
    {
        SayHelloCommand = new DelegateCommand(OnSayHello, CanSayHello);
    }
   
    // Command-Handler-Methoden
    private bool CanSayHello()
    {
        return DateTime.Parse(Geburtsdatum) < DateTime.Now;
    }
   
    private void OnSayHello()
    {
        Console.WriteLine("Hallo " + DisplayName + "!");
    }

    #endregion Commands
}

Download

ViewModelBase.cs33,7 KiBQuelltext der ViewModelBase-Klasse

Hinweise zur Verwendung

Zur Verwendung der ViewModelBase-Klasse werden die CollectionDictionary-Klasse und die DelegateCommand-Klasse benötigt.

Je nachdem, ob die ViewModelBase-Klasse in einem Projekt für .NET 4.0 oder ab 4.5 eingesetzt wird, muss die Symboldefinition von CSHARP50 am Dateianfang aktiviert oder deaktiviert werden. Sie wird verwendet, um möglichst kompakten Code für beide Umgebungen zu schreiben, der das CallerMemberName-Attribut verwendet.

Änderungen

2015Aug27
  • IsModified management with a predefined property, set to true whenever a property marked with the SetsModified attribute is changed. Reset this in your code after loading and saving. The DisplayName property can be included by setting the DisplayNameSetsModified property.
  • ViewState management with a dynamic object, much like ViewBag in ASP.NET MVC. Lets you store and restore the current state of a view when navigating back and forth.
  • Dependent commands: Commands marked with the NotifiesOn attribute raise their CanExecuteChanged event when the specified property changes. (Requires the DelegateCommand and CollectionDictionary classes.)
  • Fix: Removed static reflection cache that may have caused memory leaks by keeping a reference to old ViewModel instances.
  • Code cleanup (formatting).

Lizenz und Nutzungsbedingungen

Vervielfältigung und Weiterverbreitung dieser Datei, verändert oder unverändert, sind gestattet, vorausgesetzt die Urheberrechtsangabe und dieser Hinweis bleiben erhalten. Diese Datei wird wie vorliegend ohne jegliche Garantie oder Gewährleistung angeboten. (GNU All-Permissive-Lizenz)

Statistische Daten

  • Erstellt am 2012-07-09, aktualisiert am 2015-08-27.
  • Ca. 650 Codezeilen, geschätzte Ent­wick­lungs­kos­ten: 650 - 2 600 €