MVVM Code mit Source Generatoren vereinfachen

Wer meinen Blog verfolgt und sich meine Beispiel-Apps angeschaut hat, wird immer mal wieder mit MVVM konfrontiert. Dabei handelt es sich um das Design-Pattern Model-View-ViewModel. Man entkoppelt somit die UI von der eigenen Businesslogik und verbindet UI und Models mit der Hilfe von ViewModels. Diese stellen dann zum Beispiel Properties und Commands bereit, um Dinge anzuzeigen bzw. ausführen zu können. In diesem Beitrag möchte ich euch nun das CommunityToolkit.MVVM Package vorstellen, welche das Erstellen von ViewModels mit der Hilfe von Source Generatoren stark vereinfacht.

Als Beispiel habe ich eine leere Xamarin.Forms App erstellt, aber das Package funktioniert für alle .NET Anwendungen. Hier habe ich dann ein neues ViewModel erstellt, welches das Interface INotifyPropertyChanged implementiert. Anschließend noch ein paar Properties und einen Command hinzugefügt. Somit erhalten wir jetzt folgenden Aufbau für das ViewModel.

using MyAwesomeApp.Services;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Xamarin.CommunityToolkit.ObjectModel;

namespace MyAwesomeApp.ViewModels
{
    internal class MainPageViewModel : INotifyPropertyChanged
    {
        private readonly IDialogService _dialogService;

        private string _firstName;
        public string FirstName
        {
            get => _firstName;
            set { _firstName = value; OnPropertyChanged(); OnPropertyChanged(nameof(FullName)); GreetUserCommand.RaiseCanExecuteChanged(); }
        }

        private string _lastName;
        public string LastName
        {
            get => _lastName;
            set { _lastName = value; OnPropertyChanged(); OnPropertyChanged(nameof(FullName)); GreetUserCommand.RaiseCanExecuteChanged(); }
        }

        public string FullName => $"{FirstName} {LastName}";


        private IAsyncCommand _greetUserCommand;
        public IAsyncCommand GreetUserCommand => _greetUserCommand ?? (_greetUserCommand = new AsyncCommand(GreetUserAsync, CanGreetUser));

        public MainPageViewModel(IDialogService dialogService)
        {
            _dialogService = dialogService;
        }

        private bool CanGreetUser()
            => !string.IsNullOrEmpty(FirstName) && !string.IsNullOrEmpty(LastName);

        private async Task GreetUserAsync()
            => await _dialogService.DisplayDialogAsync("Welcome!", $"Welcome, {FullName}!", "OK");


        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Den gesamten Quellcode stelle ich euch natürlich auch über GitHub zur Verfügung. Dort seht ihr dann auch die Implementierung des DialogServices.

Nun müssen wir das NuGet-Package hinzufügen. Dafür klicken wir mit der rechten Maustaste auf die Solution und wählen den Eintrag Manage NuGet Packages aus. Hier suchen wir nun nach CommunityToolkit.MVVM und setzen das Häkchen bei Include prerelease. Anschließend installieren wir die Preview1 (da die Preview2 ein kleinen Bug bzgl. der Commands beinhaltet) in unserem .NET Standard Projekt.

Anschließend öffnen wir die CSPROJ-Datei von unserem .NET Standard Projekt und fügen in der ersten PropertyGroup den Eintrag LangVersion hinzu.

<PropertyGroup>
	<TargetFramework>netstandard2.0</TargetFramework>
	<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
	<LangVersion>10.0</LangVersion>
</PropertyGroup>

Nun öffnen wir unser ViewModel und können dieses entsprechend vereinfachen. Wir entfernen das Interface INotifyPropertyChanged, entfernen auch die Implementierung des Interfaces und fügen das Schlüsselwort partial hinzu. Anschließend fügen wir das Attribut INotifyPropertyChanged aus dem Namespace CommunityToolkit.Mvvm.ComponentModel hinzu. Damit erhalten wir jetzt folgendes Gerüst

[INotifyPropertyChanged]
internal partial class MainPageViewModel
{
}

Für die Properties benötigen wir nur noch das private Feld und fügen hier das Attribute ObservableProperty hinzu. Damit erhalten wir jetzt den folgenden Aufbau für unsere FirstName-Property.

[ObservableProperty]

private string _firstName;

Um den Rest kümmern sich jetzt die Source Generatoren, so dass trotzdem die vollständige Property mit dem Namen FirstName. Unsere Property FullName hängt ja von FirstName ab. Daher können wir noch das Attribute AlsoNotifyChangeFor verwenden und hier per nameof auf unsere FullName-Property zugreifen.

[ObservableProperty]
[AlsoNotifyChangeFor(nameof(FullName))]
private string _firstName;

Unser Command hängt auch von der Property ab. Daher können wir noch das Attribute AlsoNotifyCanExecutreFor nutzen und unseren Command-Namen übergeben.

[ObservableProperty]
[AlsoNotifyChangeFor(nameof(FullName))]
[AlsoNotifyCanExecuteFor(nameof(GreetUserCommand))]
private string _firstName;

Auch den Command können wir nun vereinfacht schreiben. Dazu nutzen wir das Attribut ICommand und können hier noch angeben, ob unser Command mehrmals aufgerufen werden darf und die Methode welche überprüft, ob ein Command aufgerufen werden kann.

[ICommand(AllowConcurrentExecutions = false, 
            CanExecute = nameof(CanGreetUser))]
private async Task GreetUserAsync()
{
    await _dialogService.DisplayDialogAsync("Welcome", 
        $"Welcome, {FullName}!", "OK");
}

Damit erhalten wir jetzt dieses ViewModel:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MyAwesomeApp.Services;
using System.ComponentModel;
using System.Threading.Tasks;

namespace MyAwesomeApp.ViewModels
{
    [INotifyPropertyChanged]
    internal partial class MainPageViewModel
    {
        private readonly IDialogService _dialogService;

        [ObservableProperty]
        [AlsoNotifyChangeFor(nameof(FullName))]
        [AlsoNotifyCanExecuteFor(nameof(GreetUserCommand))]
        private string _firstName;

        [ObservableProperty]
        [AlsoNotifyChangeFor(nameof(FullName))]
        [AlsoNotifyCanExecuteFor(nameof(GreetUserCommand))]
        private string _lastName;

        public string FullName => $"{FirstName} {LastName}";

        public MainPageViewModel(IDialogService dialogService)
        {
            _dialogService = dialogService;
        }

        private bool CanGreetUser
            => !string.IsNullOrEmpty(FirstName) && !string.IsNullOrEmpty(LastName);


        [ICommand(AllowConcurrentExecutions = false,
                    CanExecute = nameof(CanGreetUser))]
        private async Task GreetUserAsync()
        {
            await _dialogService.DisplayDialogAsync("Welcome",
                $"Welcome, {FullName}!", "OK");
        }
    }
}

Ich denke, dass direkt ersichtlich ist, dass man eine Menge an Code eingespart hat und trotzdem immer noch die gleiche Funktionalität geboten bekommt. Wie bereits oben angekündigt, gibt es mit der aktuellen Preview 2 Version einen Bug, welcher das Ausführen des Commands nur einmal erlaubt, wenn ihr einen asynchronen Command verwendet. Sobald dieses Problem behoben ist, spricht eigentlich nichts dagegen dies in allen Projekten zu verwenden, um eine Menge Code einzusparen.

UPDATE: Inzwischen wurde die Preview-Version 8.0.0-preview3 veröffentlicht, welche den angesprochenen Fehler bzgl. des Commands behebt. Daher könnt ihr jetzt auf die neueste Preview-Version updaten.

IconKitchen – App Icons erstellen Buch-Tipp: Cross-Plattform-Apps mit Xamarin.Forms entwickeln von André Krämer Lokales NuGet Package in Projekt integrieren