MVVM in .NET MAUI mit CommunityToolkit.Mvvm: ObservableProperty und RelayCommand richtig einsetzen

Schritt-für-Schritt-Anleitung zu CommunityToolkit.Mvvm in .NET MAUI 10: ObservableProperty, RelayCommand, DI, Validation und Messenger mit produktionsreifen Code-Beispielen.

MVVM in .NET MAUI 10: Toolkit-Guide 2026

Aktualisiert: 13. Juni 2026

MVVM in .NET MAUI mit CommunityToolkit.Mvvm ist die offizielle, von Microsoft empfohlene Variante, ViewModels mit Roslyn-Source-Generators schlank zu halten. Du markierst Felder mit [ObservableProperty] und Methoden mit [RelayCommand], und der Generator erzeugt zur Compile-Zeit die INotifyPropertyChanged-Properties und ICommand-Implementierungen. Das Ergebnis: ViewModels ohne Boilerplate, voll AOT-kompatibel und in .NET MAUI 10 produktionsreif. Ich nutze das Toolkit seit der ersten Preview in fast jedem MAUI-Projekt, und ehrlich gesagt vermisse ich den alten Boilerplate-Code keine Sekunde. Diese Anleitung zeigt Setup, Patterns und die typischen Fallstricke aus echten Projekten.

  • CommunityToolkit.Mvvm 8.4 liefert ObservableObject, Source Generators für Properties und Commands sowie einen Messenger. Ein dotnet add package reicht.
  • [ObservableProperty] generiert aus einem privaten Feld _name eine öffentliche Property Name inklusive OnNameChanged- und OnNameChanging-Hooks.
  • [RelayCommand] erstellt ein IRelayCommand oder IAsyncRelayCommand mit optionaler CanExecute-Logik und FlowExceptionsToTaskScheduler-Option.
  • Dependency Injection via Microsoft.Extensions.DependencyInjection ist in .NET MAUI 10 der empfohlene Weg, ViewModels und Services zu registrieren.
  • ObservableValidator integriert DataAnnotations direkt in das ViewModel und passt zu .NET MAUI-Formularen ohne Behaviors.
  • Der WeakReferenceMessenger ersetzt eigenes Event-Aggregator-Coding für lose gekoppelte ViewModel-zu-ViewModel-Kommunikation.

Was ist CommunityToolkit.Mvvm?

Das CommunityToolkit.Mvvm (früher Microsoft.Toolkit.Mvvm) ist eine schlanke, plattformunabhängige MVVM-Bibliothek, die von Microsoft zusammen mit der .NET Foundation gepflegt wird. Sie ist bewusst UI-Framework-agnostisch: dasselbe ViewModel läuft in .NET MAUI, WPF, WinUI 3 und Uno Platform. Das Toolkit besteht aus zwei Teilen: einer Laufzeitbibliothek mit ObservableObject, RelayCommand und WeakReferenceMessenger sowie einer Roslyn-Source-Generator-Bibliothek, die aus Attributen zur Compile-Zeit den Boilerplate-Code erzeugt.

Wichtig für mobile Projekte: Der Generator emittiert statischen, sichtbaren C#-Code. Heißt: keine Reflection zur Laufzeit. Genau das brauchst du für NativeAOT, das Microsoft mit .NET 9 für iOS und ab .NET 10 auch für Android stabilisiert hat. Wer heute ein neues ViewModel-Layer baut, sollte deshalb nicht mehr selbst INotifyPropertyChanged implementieren. Stattdessen liefert das Toolkit die korrekte, getestete Standardimplementierung (inklusive der Sonderfälle wie Vergleich per EqualityComparer<T>.Default, INotifyPropertyChanging-Unterstützung und korrektes Verhalten bei OneWayToSource-Bindings).

Installation in .NET MAUI 10

In einem frischen .NET MAUI 10-Projekt fügst du das Paket über die CLI hinzu. Die aktuelle stabile Version ist 8.4.0 (Stand Juni 2026):

dotnet add package CommunityToolkit.Mvvm --version 8.4.0

Anschließend prüfst du, dass die LangVersion in deiner .csproj mindestens 11 ist. Die Source Generators nutzen partial-Properties und neuere C#-Syntax:

<PropertyGroup>
  <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
  <LangVersion>latest</LangVersion>
  <Nullable>enable</Nullable>
  <EnableMvvmToolkitAnalyzers>true</EnableMvvmToolkitAnalyzers>
</PropertyGroup>

ObservableProperty richtig nutzen

Das wichtigste Attribut ist [ObservableProperty]. Du deklarierst ein privates Feld in camelCase, der Generator erzeugt daraus eine öffentliche Property in PascalCase, ruft korrekt OnPropertyChanging/OnPropertyChanged auf und stellt zwei partial-Hooks für Custom-Logik bereit. So sieht ein typisches Login-ViewModel in einer .NET MAUI-App aus:

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

namespace ContosoApp.ViewModels;

public partial class LoginViewModel : ObservableObject
{
    [ObservableProperty]
    private string _email = string.Empty;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(CanSubmit))]
    private string _password = string.Empty;

    [ObservableProperty]
    private bool _isBusy;

    public bool CanSubmit => !IsBusy
        && !string.IsNullOrWhiteSpace(Email)
        && Password.Length >= 8;

    partial void OnEmailChanged(string value)
    {
        // Hook: wird nach dem Setter aufgerufen, hier z.B. Telemetrie senden
        Analytics.Track("login_email_changed");
    }
}

Drei Details, die in der Praxis wichtig sind: Die Klasse muss partial sein, sonst kann der Generator sie nicht erweitern. Das Feld muss einen Underscore-Präfix oder camelCase verwenden, sonst kann der Generator den PascalCase-Namen nicht ableiten. Und [NotifyPropertyChangedFor] ist deutlich performanter und sicherer als der manuelle Aufruf von OnPropertyChanged(nameof(CanSubmit)) im Setter, weil der Generator die Reihenfolge garantiert.

Abhängige Properties und Befehle

Häufiger Fall: Ein Button soll deaktiviert sein, solange ein Formular ungültig ist. Statt der Property CanSubmit bindet man den Command direkt. Dazu nutzt man [NotifyCanExecuteChangedFor] auf den Feldern, von denen die CanExecute-Logik abhängt:

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SubmitCommand))]
private string _email = string.Empty;

[RelayCommand(CanExecute = nameof(CanSubmit))]
private async Task SubmitAsync() { /* ... */ }

private bool CanSubmit() => !string.IsNullOrWhiteSpace(Email);

RelayCommand und AsyncRelayCommand

Das zweite Attribut, das du fast immer brauchst, ist [RelayCommand]. Es erzeugt aus einer Methode ein IRelayCommand. Ist die Methode async, generiert das Toolkit ein IAsyncRelayCommand mit eingebautem IsRunning-Flag und CancelCommand. Genau das, was du in einer mobilen App brauchst, wenn der Nutzer auf „Senden“ tippt, während eine HTTP-Anfrage noch läuft.

public partial class ProductListViewModel : ObservableObject
{
    private readonly IProductService _productService;

    public ObservableCollection<Product> Products { get; } = new();

    [ObservableProperty]
    private bool _isRefreshing;

    public ProductListViewModel(IProductService productService)
    {
        _productService = productService;
    }

    [RelayCommand(IncludeCancelCommand = true,
                  FlowExceptionsToTaskScheduler = false,
                  AllowConcurrentExecutions = false)]
    private async Task LoadProductsAsync(CancellationToken cancellationToken)
    {
        IsRefreshing = true;
        try
        {
            Products.Clear();
            await foreach (var product in _productService
                .StreamAsync(cancellationToken))
            {
                Products.Add(product);
            }
        }
        catch (HttpRequestException ex)
        {
            await Shell.Current.DisplayAlert(
                "Fehler", ex.Message, "OK");
        }
        finally
        {
            IsRefreshing = false;
        }
    }
}

Im XAML bindest du an die generierten Properties LoadProductsCommand und LoadProductsCancelCommand:

<RefreshView IsRefreshing="{Binding IsRefreshing}"
             Command="{Binding LoadProductsCommand}">
    <CollectionView ItemsSource="{Binding Products}" />
</RefreshView>
<Button Text="Abbrechen"
        Command="{Binding LoadProductsCancelCommand}" />

Dependency Injection und ViewModel-Locator

Mit .NET MAUI 10 ist der MauiAppBuilder der zentrale DI-Container. Registriere ViewModels und Services dort und löse sie über den Konstruktor auf. Das ersetzt klassische ViewModel-Locator-Patterns aus Xamarin-Zeiten vollständig. Wenn du noch von Xamarin migrierst, lohnt sich ein Blick auf unseren Praxisleitfaden zur Migration von Xamarin.Forms zu .NET MAUI 10, der auch die DI-Umstellung Schritt für Schritt zeigt.

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.UseMauiApp<App>();

        // Services
        builder.Services.AddSingleton<IProductService, ProductService>();
        builder.Services.AddHttpClient<ApiClient>(c =>
            c.BaseAddress = new Uri("https://api.contoso.com/"));

        // ViewModels - transient!
        builder.Services.AddTransient<LoginViewModel>();
        builder.Services.AddTransient<ProductListViewModel>();

        // Pages
        builder.Services.AddTransient<LoginPage>();
        builder.Services.AddTransient<ProductListPage>();

        return builder.Build();
    }
}

Im Page-Konstruktor injizierst du das ViewModel und setzt es als BindingContext:

public partial class ProductListPage : ContentPage
{
    public ProductListPage(ProductListViewModel vm)
    {
        InitializeComponent();
        BindingContext = vm;
    }
}

Validation mit ObservableValidator

Für Formulare nutzt du statt ObservableObject die Basisklasse ObservableValidator. Sie implementiert INotifyDataErrorInfo und arbeitet direkt mit DataAnnotations aus System.ComponentModel.DataAnnotations. Kein eigener Validator nötig.

using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;

public partial class RegisterViewModel : ObservableValidator
{
    [ObservableProperty]
    [Required(ErrorMessage = "E-Mail ist erforderlich.")]
    [EmailAddress(ErrorMessage = "Ungültige E-Mail-Adresse.")]
    [NotifyDataErrorInfo]
    private string _email = string.Empty;

    [ObservableProperty]
    [Required]
    [MinLength(8, ErrorMessage = "Mindestens 8 Zeichen.")]
    [NotifyDataErrorInfo]
    private string _password = string.Empty;

    [RelayCommand]
    private void Submit()
    {
        ValidateAllProperties();
        if (HasErrors) return;
        // ... weiter
    }
}

In .NET MAUI bindest du HasErrors oder die GetErrors-Ergebnisse an Validation-Labels. Wer DataAnnotations bewusst meiden möchte (z. B. bei sehr komplexer, kontextabhängiger Validierung), kann FluentValidation einsetzen. Die beiden Bibliotheken sind problemlos kombinierbar.

ViewModel-Kommunikation mit Messenger

Wenn zwei ViewModels (etwa CartViewModel und ProductDetailViewModel) sich gegenseitig benachrichtigen müssen, ohne direkte Referenzen zu halten, nutzt du den WeakReferenceMessenger. Er kennt zwei Modi: Send/Register für reine Benachrichtigungen und Request für synchrone Rückgabewerte.

// 1. Nachricht definieren
public sealed record ProductAddedToCartMessage(Product Product);

// 2. Sender
WeakReferenceMessenger.Default.Send(
    new ProductAddedToCartMessage(product));

// 3. Empfänger
public partial class CartViewModel : ObservableObject,
    IRecipient<ProductAddedToCartMessage>
{
    public CartViewModel()
    {
        WeakReferenceMessenger.Default.RegisterAll(this);
    }

    public void Receive(ProductAddedToCartMessage message)
    {
        Items.Add(message.Product);
    }
}

Da der Messenger auf schwachen Referenzen basiert, leakt er keine ViewModels, selbst wenn du Unregister vergisst. Für ViewModels mit komplexer Kommunikation kombinierst du den Messenger mit Offline-Sync-Mechanismen, wie wir sie in unserem Artikel über Offline-First-Architektur mit .NET MAUI und SQLite beschreiben.

Stolperfallen und Best Practices

In Code-Reviews echter .NET MAUI-Projekte tauchen immer wieder dieselben Fehler auf. Diese sechs Punkte vermeiden 90% der MVVM-Bugs:

  1. Klasse muss partial sein. Vergisst du das, schlägt der Build mit MVVMTK0034 fehl. Aktiviere die Analyzer, dann siehst du das schon im Editor.
  2. Feldnamen konsistent halten. Empfehlung: Underscore-Präfix (_email). Misch nicht email und _email im selben Projekt, sonst stimmen die OnXChanged-Hooks nicht überein.
  3. Niemals UI-Code im ViewModel. Kein DisplayAlert direkt im ViewModel; setze stattdessen ein IDialogService via DI ein. Das hält ViewModels testbar.
  4. Threading respektieren. ObservableCollection-Änderungen müssen auf dem UI-Thread passieren. In async-Methoden nach jedem await ist das normalerweise gegeben, aber bei Task.Run-Aufrufen brauchst du MainThread.BeginInvokeOnMainThread.
  5. NativeAOT-Tauglichkeit prüfen. Das Toolkit ist AOT-safe; vermeide aber Reflection-basierte Bibliotheken (z. B. AutoMapper-Profile) in ViewModels, wenn du AOT aktivierst.
  6. Compiled Bindings nutzen. Setze x:DataType auf jeder Seite, dann prüft der XAML-Compiler die Bindings und du sparst Reflection zur Laufzeit. Ein Performance-Boost, den wir in unserem Leitfaden zur Performance-Optimierung in .NET MAUI ausführlich erklären.

Beispiel aus der Praxis: Login-Fluss

Ein typischer Login-Fluss kombiniert alle drei Säulen des Toolkits: ObservableProperty für die Eingaben, AsyncRelayCommand für den HTTP-Call und Messenger, um andere Bildschirme über den erfolgreichen Login zu informieren. So vermeidest du einen aufgeblähten App.Current-Singleton-Zustand und behältst sauberes Single-Responsibility:

[RelayCommand(CanExecute = nameof(CanLogin),
              AllowConcurrentExecutions = false)]
private async Task LoginAsync(CancellationToken ct)
{
    IsBusy = true;
    try
    {
        var token = await _authService.SignInAsync(Email, Password, ct);
        await _secureStorage.SetAsync("auth_token", token);
        WeakReferenceMessenger.Default.Send(new UserSignedInMessage(Email));
        await Shell.Current.GoToAsync("//home");
    }
    catch (UnauthorizedAccessException)
    {
        ErrorMessage = "Anmeldedaten ungültig.";
    }
    finally { IsBusy = false; }
}

private bool CanLogin() =>
    !IsBusy && !string.IsNullOrWhiteSpace(Email) && Password.Length >= 8;

Dieses Pattern skaliert. Du brauchst kein Mediator-Pattern, keine eigenen Events und keinen statischen UserSession.Current-Zugriff. Genau das macht das Toolkit in MAUI-Projekten so produktiv: Jede ViewModel-Methode bleibt ein isolierter, testbarer Block.

CommunityToolkit.Mvvm vs. Alternativen

Es gibt eine Handvoll konkurrierender MVVM-Bibliotheken im .NET-Ökosystem. Für die meisten .NET MAUI-Projekte ist das CommunityToolkit die richtige Wahl. Die folgende Übersicht zeigt, warum:

KriteriumCommunityToolkit.MvvmReactiveUIPrismMVVMLight
Pflege durchMicrosoft / .NET FoundationCommunityPrism Library TeamEingestellt 2018
Source GeneratorsJaNeinNeinNein
NativeAOT-tauglichJaTeilweiseJa (ab 9.0)Nein
LernkurveNiedrigHoch (Rx)MittelNiedrig
DI-ContainerEigenständig (MS.DI)SplatEingebautSimpleIoc
Navigation-FrameworkNein (nutzt Shell)Routing-Add-onEingebautNein
Paketgröße~70 KB~400 KB~250 KB~80 KB

Kurz gesagt: ReactiveUI lohnt sich, wenn dein Team produktiv mit Reactive Extensions arbeitet und du komplexe Datenflüsse koordinierst. Prism ist eine gute Wahl, wenn du Module, Regions oder Dialog-Services brauchst, die das Toolkit nicht abdeckt. Für 90% aller .NET MAUI-Apps reicht das CommunityToolkit, und genau das empfiehlt auch der offizielle .NET-Blog seit dem .NET 8-Release.

Häufig gestellte Fragen

Wird CommunityToolkit.Mvvm von Microsoft entwickelt?

Ja. Das Projekt wird von Microsoft-Mitarbeitern und der .NET Foundation gepflegt und ist die offizielle Empfehlung für neue MVVM-Projekte in .NET MAUI, WinUI 3 und WPF. Der Quellcode liegt auf GitHub unter der MIT-Lizenz.

Was ist der Unterschied zwischen ObservableObject und ObservableValidator?

ObservableObject liefert nur INotifyPropertyChanged und INotifyPropertyChanging. ObservableValidator erweitert das um INotifyDataErrorInfo sowie Methoden zur DataAnnotations-Validierung, ideal für Formular-ViewModels.

Funktioniert das Toolkit mit NativeAOT auf iOS und Android?

Ja. Die Source Generators erzeugen statischen Code ohne Reflection, das Toolkit selbst ist mit IsTrimmable und IsAotCompatible markiert. Mit .NET 10 ist NativeAOT auch für Android stabil verfügbar.

Kann ich CommunityToolkit.Mvvm in einem bestehenden Xamarin.Forms-Projekt nutzen?

Technisch ja, aber empfohlen ist die Migration zu .NET MAUI 10. Xamarin.Forms ist seit Mai 2024 nicht mehr supportet. Das Toolkit selbst läuft auf .NET Standard 2.0 und damit auch in alten Xamarin-Projekten.

Wie teste ich ViewModels, die das Toolkit nutzen?

ViewModels lassen sich als reine POCOs mit xUnit oder NUnit testen. Du instanziierst das ViewModel mit Mock-Services, rufst Commands über Execute auf und prüfst Properties oder gefeuerte PropertyChanged-Events, ganz ohne UI-Framework.

Editorial Team
Über den Autor Editorial Team

Our team of expert writers and editors.