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 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):
Anschließend prüfst du, dass die LangVersion in deiner .csproj mindestens 11 ist. Die Source Generators nutzen partial-Properties und neuere C#-Syntax:
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 musspartial 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:
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.
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:
Klasse muss partial sein. Vergisst du das, schlägt der Build mit MVVMTK0034 fehl. Aktiviere die Analyzer, dann siehst du das schon im Editor.
Feldnamen konsistent halten. Empfehlung: Underscore-Präfix (_email). Misch nicht email und _email im selben Projekt, sonst stimmen die OnXChanged-Hooks nicht überein.
Niemals UI-Code im ViewModel. Kein DisplayAlert direkt im ViewModel; setze stattdessen ein IDialogService via DI ein. Das hält ViewModels testbar.
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.
NativeAOT-Tauglichkeit prüfen. Das Toolkit ist AOT-safe; vermeide aber Reflection-basierte Bibliotheken (z. B. AutoMapper-Profile) in ViewModels, wenn du AOT aktivierst.
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:
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:
Kriterium
CommunityToolkit.Mvvm
ReactiveUI
Prism
MVVMLight
Pflege durch
Microsoft / .NET Foundation
Community
Prism Library Team
Eingestellt 2018
Source Generators
Ja
Nein
Nein
Nein
NativeAOT-tauglich
Ja
Teilweise
Ja (ab 9.0)
Nein
Lernkurve
Niedrig
Hoch (Rx)
Mittel
Niedrig
DI-Container
Eigenständig (MS.DI)
Splat
Eingebaut
SimpleIoc
Navigation-Framework
Nein (nutzt Shell)
Routing-Add-on
Eingebaut
Nein
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.
Deep Linking in .NET MAUI 10 verbindet https-URLs und Custom-Schemes mit Shell-Routen. Der Praxisleitfaden zeigt Universal Links, App Links, apple-app-site-association, assetlinks.json und Deferred Deep Linking auf iOS und Android.
Praxisleitfaden zu .NET MAUI Blazor Hybrid in .NET 10: Razor-Komponenten in nativen Apps mit BlazorWebView einbetten, Code zwischen Web und Mobile teilen, native APIs nutzen und Startzeit optimieren. Mit Code-Beispielen aus produktiven Kundenprojekten.
So machen Sie .NET MAUI 10 Apps BFSG-konform: SemanticProperties, VoiceOver, TalkBack, Custom-Handler und WCAG 2.2 Level AA mit funktionierendem XAML- und Handler-Code fuer iOS und Android.