MVVM ohne Boilerplate: das CommunityToolkit in WPF

MVVM ohne Boilerplate: das CommunityToolkit in WPF

- Matt

Ein ViewModel in WPF besteht traditionell zu zwei Dritteln aus Tipparbeit. Für jede Eigenschaft ein Feld, ein Property mit OnPropertyChanged, für jede Aktion eine ICommand-Implementierung. Bei fünf Eigenschaften und drei Befehlen ist die Datei zweihundert Zeilen lang, und keine davon sagt etwas über die Anwendung aus.

CommunityToolkit.Mvvm erledigt den Teil mit einem Source Generator. Das Paket ist inzwischen der Nachfolger dessen, was früher MVVM Light war, und kommt ohne Laufzeitabhängigkeiten aus.

Vorher und nachher

Klassisch:

private string _suchbegriff = "";

public string Suchbegriff
{
    get => _suchbegriff;
    set
    {
        if (_suchbegriff == value) return;
        _suchbegriff = value;
        OnPropertyChanged();
        SuchenCommand.NotifyCanExecuteChanged();
    }
}

Mit dem Toolkit:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class SucheViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SuchenCommand))]
    private string suchbegriff = "";

    [RelayCommand(CanExecute = nameof(KannSuchen))]
    private async Task Suchen(CancellationToken token)
    {
        Ergebnisse = await _service.Suchen(Suchbegriff, token);
    }

    private bool KannSuchen() => !string.IsNullOrWhiteSpace(Suchbegriff);
}

Aus dem Feld suchbegriff wird die Eigenschaft Suchbegriff, aus der Methode Suchen ein SuchenCommand. Beides erzeugt der Generator beim Übersetzen, es steht also echter Code im Projekt und nichts wird zur Laufzeit zusammengesteckt.

Zwei Details, die den Alltag angenehm machen: [NotifyCanExecuteChangedFor] spart das manuelle Benachrichtigen, und ein RelayCommand mit Task-Rückgabe wird automatisch zu einem IAsyncRelayCommand. Der kennt eine Eigenschaft IsRunning, an die man im XAML direkt einen Ladeindikator binden kann, und verhindert nebenbei den Doppelklick auf einen laufenden Befehl.

Das partial an der Klasse ist Pflicht, sonst hat der Generator keinen Ort für seinen Teil. Die Fehlermeldung dazu ist zum Glück eindeutig.

Wo man aufpasst

Die Namenskonvention ist strikt. Aus suchbegriff wird Suchbegriff, aus _suchbegriff ebenfalls. Wer seine Felder anders benennt, bekommt Eigenschaften mit merkwürdigen Namen oder gar keine. Man gewöhnt sich daran, aber es ist eine Regel, die man kennen muss.

Unangenehmer ist das Debuggen im Zusammenspiel mit dem Designer. Ändert man ein Attribut, sieht Visual Studio den generierten Code manchmal erst nach einem Neubau, und bis dahin unterringelt der Editor Eigenschaften, die es längst gibt. Ein Rebuild löst das, aber man sucht beim ersten Mal eine Weile.

Für Nachvollziehbarkeit lohnt sich ein Blick in den erzeugten Code. Im Projektmappen-Explorer unter Abhängigkeiten, Analysatoren steht alles, was der Generator produziert hat. Wenn eine Bindung nicht aktualisiert, klärt sich dort meist in dreißig Sekunden, warum.

Seit Version 8.4 lassen sich die Attribute auch auf partielle Eigenschaften statt auf Felder setzen, was C# 13 voraussetzt. Das liest sich sauberer, weil der Name dann dasteht, statt hergeleitet zu werden. Bei bestehenden ViewModels habe ich es trotzdem nicht umgestellt, der Gewinn rechtfertigt die Änderung nicht.