Beim Entwickeln von Applikationen im MVVM-Umfeld spielt die INotifyPropertyChanged-Schnittstelle eine große Rolle. Bei der Umsetzung der ViewModels muss sichergestellt werden, dass deren Eigenschaften das PropertyChanged-Event bei Änderungen auch aufrufen, damit die Benutzerschnittstelle die angezeigten Daten aktualisiert. Neben INotifyPropertyChanged spielt bei MVVM auch die ICommand-Schnittstelle eine zentrale Rolle.

Über Commands werden Aktionen im ViewModel definiert, die später über eine Datenbindung mit Buttons auf der View verbunden werden können. Diese Bindung stellt sicher, dass der Command vom ViewModel ausgeführt wird, wenn der Benutzer auf die jeweiligen Steuerelemente klickt.

Die ICommand-Schnittstelle besteht aus drei Elementen:

  • Die Methode Execute(object) wird aufgerufen, wenn der Command ausgelöst wurde. Über den Parameter können zusätzliche Informationen übergeben werden
  • Die Methode CanExecute(object) gibt einen boolschen Wert zurück. Ist dieser true, dann kann der Command ausgeführt werden. Das ist nützlich für XAML-Elemente wie Buttons, die automatisch deaktiviert werden, wenn false zurückgegeben wird.
  • Das CanExecuteChanged-Event, das aufgerufen werden muss, wenn sich eine Eigenschaft, die zur Ausführung des Commands relevant ist, ändert. Datengebundene Elemente prüfen nach dessen Aufruf erneut über die CanExecute-Methode, ob der Command aufgerufen werden kann.

Im Hinblick auf die ICommand-Schnittstelle ist der Aufruf des CanExecuteChanged-Events dementsprechend wichtig und sollte bei Änderung einzelner Eigenschaften, die zur Ausführbarkeit von einzelnen Commands beitragen, nicht vergessen werden. Andernfalls bekommen die datengebundenen Elemente auf der Benutzerschnittstelle die Veränderung nicht mit und bleiben aktiv bzw. inaktiv.

Im Zuge diverser UnitTests, die ich für ein Windows Phone Projekt geschrieben habe, bin ich auf die AssertHelper-Klasse des WPF Application Frameworks (WAF) gestoßen. Diese Hilfsklasse stellt einige Methoden bereit, um die oben gezeigten Mechanismen durch Tests überprüfen zu können. Diese Methoden werden in diesem Artikel vorgestellt.

Die Klasse AssertHelper

Sowohl bei PropertyChanged, als auch bei CanExecuteChanged handelt es sich um Events, die im jeweiligen Szenario aufgerufen werden müssen, um das gewünschte Verhalten der Applikation sicherzustellen. Betrachten wir noch kurz den klassischen Weg, wie man beispielsweise den korrekten Einsatz von PropertyChanged bei einer Eigenschaft (hier Name) über einen Test abdecken kann:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[TestMethod]
public void SetName_PropertyChangedEventShouldGetRaised()
{
    bool raised = false;
    var viewModel = new MainViewModel();
    viewModel.PropertyChanged += (sender, e) =>
    {
        if (e.PropertyName == "Name")
        {
            raised = true;
        }
    };

    viewModel.Name = "Neuer Name";
    Assert.IsTrue(raised);
}

Dieser Weg ist etwas umständlich und kann, je nach Anzahl der Eigenschaften im ViewModel, zu einer sehr großen Menge an Code führen, der die Tests schnell unübersichtlich macht. Hier setzt die Hilfsklasse AssertHelper des WPF Application Frameworks an und bietet für UnitTests unter anderem folgende zwei Methoden an, um das Testing zu erleichtern:

  • PropertyChangedEvent(T, Expression<Func<T, object>>, Action)
  • CanExecuteChangedEvent(ICommand, Action)

Durch diese beiden Hilfsmethoden kann der obige Testcode auf zwei Zeilen reduziert werden, was viel Copy & Paste erspart und zur Übersichtlichkeit in den Testklassen beiträgt.

1. Das PropertyChangedEvent

Wie im obigen Beispiel soll getestet werden, ob bei Änderung der Name-Eigenschaft auch das PropertyChanged-Event aufgerufen wird. Das ViewModel hierzu sieht im einfachsten Fall folgendermaßen aus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MainViewModel : INotifyPropertyChanged
{
    private string name;

    /// <summary>
    /// Event for property change notifications.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    public string Name
    {
        get
        {
            return this.name;
        }

        set
        {
            this.name = value;
            this.OnPropertyChanged("Name");
        }
    }

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

Zum Überprüfen, ob beim Ändern der Eigenschaft Name das Event korrekt aufgerufen wird, kann nun folgender UnitTest geschrieben werden:

1
2
3
4
5
6
[TestMethod]
public void SetName_PropertyChangedEventShouldGetInvoked()
{
    var viewModel = new MainViewModel();
    AssertHelper.PropertyChangedEvent(viewModel, x => x.Name, () => viewModel.Name = "Neuer Name");
}

Die AssertHelper-Klasse erwartet als Parameter das ViewModel, die Eigenschaft und eine Funktion, welche die jeweilige Eigenschaft ändert. Hier bietet sich ein Lambda-Ausdruck an, der kompakt als Einzeiler geschrieben werden kann. Wird der EventHandler nicht ausgeführt, wird vom AssertHelper ein Assert.Fail aufgerufen, was zum Fehlschlagen des Tests führt.

Erweiterung des ViewModels

Um allgemeine Fehler im ViewModel direkt zu vermeiden, bietet sich das CallerMemberNameAttribute an, das ab dem .NET-Framework 4.5 verfügbar ist. Dadurch muss der Name der Eigenschaft im OnPropertyChanged nicht mehr explizit angegeben werden, was Schreibfehler verhindert. Zudem wird in SetProperty automatisch überprüft, ob sich die Eigenschaft verändert hat und das OnPropertyChanged-Event überhaupt benötigt wird.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class MainViewModel : INotifyPropertyChanged
{
    private string name;

    /// <summary>
    /// Event for property change notifications.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    public string Name
    {
        get { return this.name; }
        set { this.SetProperty(ref this.name, value); }
    }

    /// <summary>
    /// Checks if a property already matches a desired value.  Sets the property and
    /// notifies listeners only when necessary.
    /// </summary>
    /// <typeparam name="T">Type of the property.</typeparam>
    /// <param name="storage">Reference to a property with both getter and setter.</param>
    /// <param name="value">Desired value for the property.</param>
    /// <param name="propertyName">Name of the property used to notify listeners.</param>
    /// <returns>True if the value was changed, false if the existing value matched the desired value.</returns>
    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (object.Equals(storage, value))
        {
            return false;
        }

        storage = value;
        this.OnPropertyChanged(propertyName);
        return true;
    }

    /// <summary>
    /// Notifies listeners that a property value has changed.
    /// </summary>
    /// <param name="propertyName">Name of the property used to notify listeners. This
    /// value is optional and can be provided automatically when invoked from compilers
    /// that support <see cref="CallerMemberNameAttribute"/>.</param>
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var eventHandler = this.PropertyChanged;
        if (eventHandler != null)
        {
            eventHandler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

2. Das CanExecuteChangedEvent

Zum Testen der Commands im Projekt kann die CanExecuteChangedEvent-Methode der Hilfsklasse verwendet werden. Für das ICommand wird üblicherweise die Implementierung des RelayCommands, bzw. DelegateCommands verwendet. In folgendem Codeabschnitt sind die Teile des ViewModels dargestellt, die den Command MyCommand betreffen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public MainViewModel()
{
    this.MyCommand = new DelegateCommand(this.MyAction, this.CanExecuteMyCommand);
}

/// <summary>
/// Gets or sets a basic command.
/// </summary>
public DelegateCommand MyCommand { get; set; }

/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name
{
    get
    {
        return this.name;
    }

    set
    {
        this.SetProperty(ref this.name, value);
        this.MyCommand.RaiseCanExecuteChanged();
    }
}

/// <summary>
/// Returns a value whether the <see cref="MyCommand"/> can be executed.
/// </summary>
/// <param name="obj">Parameter object</param>
/// <returns>True when the command can be executed</returns>
private bool CanExecuteMyCommand(object obj)
{
    return !string.IsNullOrEmpty(this.Name);
}

/// <summary>
/// Action for the <see cref="MyCommand"/>.
/// </summary>
/// <param name="obj">Parameter object</param>
private void MyAction(object obj)
{
    // Custom actions
    // ...
}

Der hier gezeigte Command MyCommand ist abhängig von der Eigenschaft Name. Wenn diese Zeichenfolge einen nicht-leeren Wert enthält, kann der Command ausgeführt werden. Der hier ausschlaggebene Befehl, der der Benutzeroberfläche mitteilt, dass CanExecute erneut evaluiert werden soll, ist im Setter von Name enthalten:

    this.MyCommand.RaiseCanExecuteChanged();

Für einzelne Eigenschaften im ViewModel ist das noch sehr übersichtlich, kann aber deutlich komplexer werden, wenn der Command von mehreren Eigenschaften abhängt. Für den Test, ob beim Ändern einzelner Eigenschaften das Event auch aufgerufen wird, wird die CanExecuteChangedEvent-Methode des AssertHelpers verwendet:

1
2
3
4
5
6
[TestMethod]
public void MyCommand_ValuesSet_ShouldRaiseExecuteChanged()
{
    var viewModel = new MainViewModel();
    AssertHelper.CanExecuteChangedEvent(viewModel.MyCommand, () => viewModel.Name = "Neuer Name");
}

Die AssertHelper-Klasse erwartet hierbei als Parameter den Command und eine Funktion, die eine Eigenschaft innerhalb des ViewModels ändert, die das CanExecuteChanged-Event aufrufen muss. Wenn das nicht der Fall ist, wieder ebenfalls wieder Assert.Fail aufgerufen.

Zusammenfassung

Über die AssertHelper-Klasse des WPF Application Frameworks kann das Schreiben von UnitTests für die Events der Schnittstellen INotifyPropertyChanged und ICommand vereinfacht werden. Hierzu sind im Vergleich zum manuellen Testcode nur wenige Zeilen notwendig, um dadurch kompaktere und übersichtlichere Tests zu ermöglichen. Die Hilfsklasse kann komplett separat vom WAF verwendet werden und ist unten über den Link verfügbar.

Beispielprojekt

Das Beispielprojekt ist hier auf GitHub zu finden.