MVVM dans .NET MAUI 10 avec CommunityToolkit.Mvvm : Guide Complet (2026)
Implémentez MVVM dans .NET MAUI 10 avec CommunityToolkit.Mvvm 8.4 : ObservableProperty, RelayCommand, injection de dépendances, validation, messagerie WeakReferenceMessenger, navigation Shell et tests unitaires des ViewModels.
Le pattern MVVM (Model-View-ViewModel) dans .NET MAUI 10 avec CommunityToolkit.Mvvm consiste à séparer la logique de présentation (ViewModel) du rendu XAML (View) en utilisant des générateurs de source pour éliminer le code répétitif d'INotifyPropertyChanged et des commandes. En 2026, c'est l'approche officiellement recommandée par Microsoft pour construire des applications MAUI maintenables, testables et performantes. Ce guide vous montre comment l'implémenter de bout en bout, avec injection de dépendances, validation et navigation Shell.
Honnêtement, après avoir migré trois apps Xamarin.Forms vers MAUI ces derniers mois, je peux dire que CommunityToolkit.Mvvm change vraiment la donne au quotidien. Plus de boilerplate, moins de bugs.
CommunityToolkit.Mvvm 8.4+ utilise des générateurs de source Roslyn pour produire les propriétés observables et les commandes à la compilation, ce qui élimine la réflexion à l'exécution.
Hériter d'ObservableObject et annoter les champs avec [ObservableProperty] remplace toute implémentation manuelle d'INotifyPropertyChanged.
[RelayCommand] génère automatiquement des ICommand synchrones ou asynchrones, avec gestion de CanExecute et annulation via CancellationToken.
L'injection de dépendances native de .NET MAUI 10 (MauiProgram.cs) enregistre ViewModels et services en transient, scoped ou singleton.
ObservableValidator combine MVVM et DataAnnotations pour la validation déclarative des formulaires.
WeakReferenceMessenger permet la communication découplée entre ViewModels sans créer de fuites mémoire.
Qu'est-ce que MVVM dans .NET MAUI ?
MVVM est un pattern d'architecture qui divise une application en trois couches : la View (XAML, contrôles visuels), le ViewModel (état observable, commandes, logique de présentation) et le Model (entités métier, services de données). Dans .NET MAUI 10, la View se lie au ViewModel via le moteur de data binding XAML, ce qui découple complètement la couche de présentation du rendu natif.
Sans bibliothèque, implémenter MVVM exige d'écrire à la main INotifyPropertyChanged sur chaque propriété, des classes ICommand sur chaque action, et de connecter le tout via la réflexion. Avec CommunityToolkit.Mvvm, des générateurs de source Roslyn produisent ce code à la compilation. Le résultat ? Moins de bugs, un démarrage plus rapide (pas de réflexion à l'exécution) et une compatibilité totale avec le mode AOT.
En pratique, un ViewModel hérite d'ObservableObject. Le constructeur reçoit ses dépendances (services HTTP, navigation, stockage), expose des propriétés observables pour les données affichées, et publie des RelayCommand que les boutons XAML déclenchent. Ce découpage rend chaque ViewModel testable en isolation, sans démarrer l'interface graphique, un point critique abordé dans notre guide complet des tests .NET MAUI 10.
Installer CommunityToolkit.Mvvm dans un projet MAUI 10
Créez un nouveau projet ou ouvrez un projet .NET MAUI 10 existant, puis ajoutez le package NuGet. À la date de cet article, la version stable est CommunityToolkit.Mvvm 8.4.0, compatible avec net10.0-android, net10.0-ios, net10.0-maccatalyst et net10.0-windows.
Aucune configuration de runtime supplémentaire n'est requise. Les générateurs de source s'activent automatiquement dès que le package est référencé. Vérifiez que votre .csproj cible bien net10.0 avec les multi-cibles MAUI :
ObservableProperty vs INotifyPropertyChanged manuel
La principale différence entre [ObservableProperty] et INotifyPropertyChanged manuel ? Le premier génère le code de notification à la compilation à partir d'un simple champ privé, tandis que le second exige d'écrire explicitement le backing field, le getter, le setter et l'appel à OnPropertyChanged sur chaque propriété. Pour une dizaine de propriétés, vous économisez environ 60 lignes de code par ViewModel.
Voici la version manuelle, telle qu'on l'écrivait dans Xamarin.Forms :
public class ProductViewModelLegacy : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string _name = string.Empty;
public string Name
{
get => _name;
set
{
if (_name == value) return;
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
Avec CommunityToolkit.Mvvm 8.4, la même fonctionnalité tient en quelques lignes. Petite subtilité, la classe doit être partial pour que le générateur puisse étendre la propriété :
using CommunityToolkit.Mvvm.ComponentModel;
public partial class ProductViewModel : ObservableObject
{
[ObservableProperty]
private string _name = string.Empty;
[ObservableProperty]
private decimal _price;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TotalDisplay))]
private int _quantity;
public string TotalDisplay => $"{Price * Quantity:C}";
partial void OnPriceChanged(decimal value)
{
if (value < 0)
Price = 0;
}
}
Le générateur produit Name, Price, Quantity, leurs setters avec comparaison d'égalité, les appels OnPropertyChanged et les hooks partial void OnXChanged. L'attribut [NotifyPropertyChangedFor] rafraîchit automatiquement TotalDisplay quand Quantity change. Très pratique pour les propriétés calculées.
Créer des commandes avec RelayCommand et AsyncRelayCommand
L'attribut [RelayCommand] transforme une méthode du ViewModel en propriété ICommand liable depuis XAML. Pour les opérations I/O (appels HTTP, accès à la base SQLite, lecture de fichiers), utilisez une méthode asynchrone. Le générateur produit alors un AsyncRelayCommand qui gère le suivi d'exécution, la prévention de la double exécution et l'annulation.
Injection de dépendances et enregistrement des ViewModels
.NET MAUI 10 utilise Microsoft.Extensions.DependencyInjection intégré nativement. Tous les ViewModels et services s'enregistrent dans MauiProgram.cs. Préférez le scope Transient pour les ViewModels (chaque page reçoit une instance neuve) et Singleton pour les services stateless comme les clients HTTP ou les caches.
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts => fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"));
builder.Services.AddHttpClient<IProductService, ProductService>(c =>
{
c.BaseAddress = new Uri("https://api.exemple.fr/");
c.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddSingleton<IPreferencesStore, PreferencesStore>();
builder.Services.AddTransient<ProductListViewModel>();
builder.Services.AddTransient<ProductListPage>();
builder.Services.AddTransient<ProductDetailViewModel>();
builder.Services.AddTransient<ProductDetailPage>();
return builder.Build();
}
}
Dans la page, déclarez le ViewModel comme paramètre du constructeur. Le conteneur MAUI le résout automatiquement, à condition que la page soit elle-même enregistrée dans le conteneur.
public partial class ProductListPage : ContentPage
{
public ProductListPage(ProductListViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
}
Pour aller plus loin sur les choix d'architecture qui en découlent (séparation en couches, organisation des projets et règles de dépendance), consultez notre guide des performances .NET MAUI 10, qui détaille l'impact du nombre de ViewModels enregistrés sur le temps de démarrage avec NativeAOT.
Passer des paramètres entre ViewModels avec Shell
Shell Navigation est le système de routage recommandé pour .NET MAUI 10. Pour passer un paramètre (par exemple un identifiant de produit), utilisez GoToAsync avec une chaîne de requête, puis appliquez l'attribut [QueryProperty] sur le ViewModel cible.
Pour les écrans d'inscription, de connexion ou de paiement, héritez d'ObservableValidator au lieu d'ObservableObject. Cette classe expose ValidateAllProperties(), HasErrors et la collection GetErrors(). Les règles s'expriment via les attributs System.ComponentModel.DataAnnotations standards.
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class SignupViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required(ErrorMessage = "L'e-mail est obligatoire")]
[EmailAddress(ErrorMessage = "Format d'e-mail invalide")]
private string _email = string.Empty;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[MinLength(8, ErrorMessage = "Au moins 8 caractères")]
private string _password = string.Empty;
[RelayCommand]
private async Task SubmitAsync()
{
ValidateAllProperties();
if (HasErrors) return;
// Appel API d'inscription
}
}
Dans XAML, affichez les erreurs via un Label lié à la collection d'erreurs, ou utilisez un convertisseur personnalisé. [NotifyDataErrorInfo] est essentiel. Il déclenche INotifyDataErrorInfo à chaque mise à jour, ce qui rafraîchit l'UI dès qu'une frappe rend le champ valide ou invalide.
Communication entre ViewModels avec Messenger
WeakReferenceMessenger permet à un ViewModel de notifier d'autres ViewModels d'un événement sans référence directe. Sur mon dernier projet, c'est exactement ce qui m'a permis de connecter une page modale d'édition à la liste parente sans introduire de couplage en dur. Vous pouvez aussi l'utiliser pour un système d'événements global (déconnexion, changement de thème).
using CommunityToolkit.Mvvm.Messaging;
public sealed record ProductSavedMessage(int Id);
public partial class ProductEditViewModel : ObservableObject
{
[RelayCommand]
private async Task SaveAsync()
{
var id = await _service.SaveAsync(Current);
WeakReferenceMessenger.Default.Send(new ProductSavedMessage(id));
await Shell.Current.GoToAsync("..");
}
}
public partial class ProductListViewModel : ObservableObject, IRecipient<ProductSavedMessage>
{
public ProductListViewModel(IProductService service)
{
_service = service;
WeakReferenceMessenger.Default.Register(this);
}
public void Receive(ProductSavedMessage message) => _ = LoadAsync(CancellationToken.None);
}
L'utilisation de références faibles évite les fuites mémoire qui ont longtemps gangrené les applications Xamarin.Forms basées sur MessagingCenter (désormais obsolète dans .NET MAUI 10). Si vous migrez depuis Xamarin, jetez un œil à notre guide complet de migration Xamarin.Forms vers .NET MAUI qui détaille le remplacement de MessagingCenter par WeakReferenceMessenger.
Tester les ViewModels MVVM
L'un des bénéfices majeurs de MVVM, c'est la testabilité. Comme un ViewModel ne dépend d'aucun élément XAML, vous pouvez l'instancier dans un test unitaire xUnit, injecter des doubles (Moq, NSubstitute) pour ses services, et asserter sur ses propriétés et l'effet de ses commandes.
using NSubstitute;
using Xunit;
public class ProductListViewModelTests
{
[Fact]
public async Task LoadAsync_remplit_la_collection_Products()
{
var service = Substitute.For<IProductService>();
service.GetAllAsync(Arg.Any<CancellationToken>())
.Returns(new[] { new Product { Id = 1, Name = "Café" } });
var sut = new ProductListViewModel(service);
await sut.LoadCommand.ExecuteAsync(null);
Assert.Single(sut.Products);
Assert.False(sut.IsBusy);
}
}
Aucun émulateur, aucun simulateur, aucune dépendance MAUI : les tests s'exécutent en quelques millisecondes dans le pipeline CI. J'ai vu cette vitesse pousser des équipes à augmenter sérieusement leur couverture, ce qui réduit franchement les régressions au fil des versions.
Questions fréquemment posées
CommunityToolkit.Mvvm est-il compatible avec NativeAOT dans .NET MAUI 10 ?
Oui. CommunityToolkit.Mvvm utilise exclusivement des générateurs de source Roslyn et n'a recours à aucune réflexion à l'exécution. Il est donc entièrement compatible avec la publication NativeAOT activée par défaut sur iOS et disponible en option sur Android dans .NET MAUI 10.
Pourquoi ma classe ViewModel doit-elle être déclarée partial ?
Le mot-clé partial est requis parce que les générateurs de source ajoutent les propriétés publiques et les commandes dans un second fichier partiel. Sans partial, le compilateur ne peut pas fusionner les déclarations et signale une erreur CS0260.
Quelle est la différence entre ObservableObject et ObservableRecipient ?
ObservableObject fournit uniquement INotifyPropertyChanged. ObservableRecipient y ajoute l'intégration avec IMessenger, des méthodes OnActivated/OnDeactivated et la gestion du cycle de vie pour la messagerie. Utilisez ObservableRecipient uniquement si votre ViewModel reçoit des messages.
Peut-on utiliser MVVM avec Blazor Hybrid dans .NET MAUI 10 ?
Oui, mais le pattern est moins central : Blazor utilise des composants avec son propre cycle de rendu. Vous pouvez néanmoins exposer des ViewModels comme services scoped et les injecter via @inject pour partager la logique entre pages MAUI XAML et composants Blazor Razor.
Comment éviter la double exécution d'un RelayCommand asynchrone ?
AsyncRelayCommand empêche par défaut une seconde exécution tant que la première n'est pas terminée. Sa propriété IsRunning retourne true et CanExecute renvoie false. Vous pouvez aussi définir AllowConcurrentExecutions = false explicitement sur l'attribut [RelayCommand].
Guide complet de la navigation Shell dans .NET MAUI 10. Routes URI, deep links iOS/Android, injection de dépendances et passage de paramètres typés, avec exemples testés en production et pièges courants à éviter.