CommunityToolkit.Mvvm + Microsoft.Extensions.DependencyInjection
Le MVVM Toolkit ne contient volontairement aucun conteneur DI — il s'intègre avec
Microsoft.Extensions.DependencyInjection, le même conteneur qu'utilisent ASP.NET
Core, les services Worker et le .NET Generic Host.
TL;DR. Construisez le fournisseur de services une fois au démarrage (préférez
Host.CreateDefaultBuilder()). Enregistrez les services et ViewModels. Injectez via les constructeurs. ÉvitezIoc.Default.GetService<T>()dans le code utilisateur.
Quand utiliser cette compétence
- Mettre en place la racine de composition pour une nouvelle application XAML (WPF, WinUI 3, MAUI, Uno, Avalonia)
- Choisir les durées de vie des services/VMs
- Câbler
IMessengerune fois et l'injecter dans les ViewModelsObservableRecipient - Résoudre le ViewModel d'une page sans couplage à un service locator
- Diagnostiquer « Unable to resolve service for type X while attempting to activate Y »
Pour les générateurs de source et les patterns ViewModel, voir la compétence mvvm-toolkit. Pour la pub/sub Messenger, voir mvvm-toolkit-messenger.
Racine de composition recommandée (Generic Host)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using CommunityToolkit.Mvvm.Messaging;
public partial class App : Application
{
public IHost Host { get; }
public App()
{
Host = Microsoft.Extensions.Hosting.Host
.CreateDefaultBuilder()
.ConfigureServices((_, services) =>
{
services.AddSingleton<IFilesService, FilesService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
services.AddSingleton<ShellViewModel>();
services.AddTransient<ContactViewModel>();
services.AddTransient<EditorViewModel>();
})
.Build();
}
public static T GetService<T>() where T : class =>
((App)Current).Host.Services.GetRequiredService<T>();
}
Avantages du Generic Host :
- Liaison
appsettings.jsonviaMicrosoft.Extensions.Configuration - Journalisation via
Microsoft.Extensions.Logging - Services hébergés (
IHostedService) pour les tâches de fond - Validation des scopes dans les versions de développement
WPF et Windows Forms doivent intégrer la durée de vie du host avec celle de l'application — voir Use the .NET Generic Host in a WPF app.
Sans Generic Host
Quand vous avez besoin uniquement d'un conteneur de services et voulez zéro dépendance supplémentaire :
var services = new ServiceCollection();
services.AddSingleton<IFilesService, FilesService>();
services.AddTransient<ContactViewModel>();
ServiceProvider provider = services.BuildServiceProvider();
Injection par constructeur
Injectez les services et les ViewModels enfants via le constructeur :
public sealed partial class ContactViewModel(
IFilesService files,
IMessenger messenger,
ILogger<ContactViewModel> logger)
: ObservableRecipient(messenger)
{
[ObservableProperty]
private string? name;
[RelayCommand]
private async Task SaveAsync()
{
logger.LogInformation("Saving {Name}", Name);
await files.SaveAsync(Name!);
}
}
Pourquoi l'injection par constructeur est meilleure qu'un service locator :
- Les dépendances sont explicites et visibles au point d'appel
- Les tests unitaires injectent directement les fakes/mocks
- Le conteneur DI valide le graphe de dépendances au démarrage
- Les enregistrements manquants lèvent une exception immédiatement, pas à la première utilisation
Durées de vie
| Durée de vie | Méthode | Utilisation typique dans les apps XAML |
|---|---|---|
| Singleton | AddSingleton<T> |
Shell/VM de fenêtre principale, paramètres, services de fichier/HTTP, le IMessenger partagé, caches applicatifs |
| Transient | AddTransient<T> |
ViewModels par page ou par document (une nouvelle instance à chaque résolution) |
| Scoped | AddScoped<T> |
Rarement nécessaire dans les apps clientes ; utile avec IServiceScope explicite (par ex., scopes par fenêtre) |
services.AddSingleton<ShellViewModel>(); // 1 instance pour la durée de vie de l'app
services.AddTransient<NoteViewModel>(); // nouvelle instance par résolution
services.AddScoped<DialogService>(); // 1 par scope (rare)
Résolution dans une View
Résolvez le ViewModel racine de la page dans le code-behind, puis laissez-le extraire ses propres dépendances :
public sealed partial class ContactPage : Page
{
public ContactViewModel ViewModel { get; }
public ContactPage()
{
ViewModel = App.GetService<ContactViewModel>();
InitializeComponent();
}
}
Liez en XAML avec {x:Bind ViewModel.Xxx} (compiled bindings) ou
{Binding Xxx} contre DataContext.
Pour les frameworks de navigation (WinUI 3 Frame.Navigate, MAUI Shell, Prism,
MVVMCross), laissez le framework résoudre la page et la page résout son
ViewModel à partir de DI. Ne créez pas manuellement de ViewModels avec new.
Enregistrement de IMessenger
Enregistrez le messager que vous voulez une fois, injectez IMessenger partout :
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
// ou
services.AddSingleton<IMessenger>(StrongReferenceMessenger.Default);
Puis :
public sealed partial class MyViewModel(IMessenger messenger)
: ObservableRecipient(messenger) { }
Pour les messagers par fenêtre, enregistrez avec des services avec clé ou comme instances scoped et injectez dans les ViewModels par fenêtre.
Voir la compétence mvvm-toolkit-messenger pour la surface du messager.
Services avec clé (.NET 8+)
Résolvez différentes implémentations de la même interface par clé :
services.AddKeyedSingleton<IExporter, CsvExporter>("csv");
services.AddKeyedSingleton<IExporter, JsonExporter>("json");
public sealed partial class ExportViewModel(
[FromKeyedServices("csv")] IExporter csvExporter,
[FromKeyedServices("json")] IExporter jsonExporter)
: ObservableObject { /* ... */ }
Points de test
Les dépendances injectées par constructeur sont triviales à remplacer dans les tests. Avec
Moq :
[Fact]
public async Task Save_calls_files_service()
{
var files = new Mock<IFilesService>();
var messenger = new WeakReferenceMessenger();
var logger = NullLogger<ContactViewModel>.Instance;
var vm = new ContactViewModel(files.Object, messenger, logger)
{
Name = "Ada"
};
await vm.SaveCommand.ExecuteAsync(null);
files.Verify(f => f.SaveAsync("Ada"), Times.Once);
}
Si vous mockez Ioc.Default ou l'état statique, le ViewModel utilise un
service locator — refactorisez pour l'injection par constructeur.
Héritage : Ioc.Default
CommunityToolkit.Mvvm.DependencyInjection.Ioc est une échappatoire pour
les cas où l'injection par constructeur est impossible — VMs instanciés par XAML
pour les données au moment de la conception, ValueConverters, modèles de contrôle.
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddSingleton<IFilesService, FilesService>()
.AddTransient<ContactViewModel>()
.BuildServiceProvider());
var files = Ioc.Default.GetRequiredService<IFilesService>();
Traitez-le comme le dernier recours. À l'intérieur des ViewModels, services et toute classe que le conteneur DI peut construire, préférez l'injection par constructeur.
Pièges courants
Ioc.Default.GetService<T>()à l'intérieur du constructeur d'un VM. Cache la dépendance, casse les tests unitaires, empêche la validation du graphe au démarrage.- Tout en
Singleton. Un VM « par document » enregistré comme singleton devient un état partagé entre tous les documents — corruption de données subtile. UtilisezAddTransientpour les VMs par instance. - Plusieurs appels à
BuildServiceProvider(). Chaque appel crée un conteneur frais — les singletons ne sont pas partagés. Construisez une seule fois au démarrage. - Capturer
IServiceProviderdans des objets de longue durée. Indique un pattern service-locator. Injectez les dépendances spécifiques que vous avez besoin. - Pas de validation des scopes en développement. Utilisez
Host.CreateDefaultBuilder()(qui définitValidateScopesetValidateOnBuilden développement) pour que les erreurs d'enregistrement échouent au démarrage, pas à la première utilisation. - Résoudre des services scoped à partir du fournisseur racine. Ils sont
effectivement promus à la durée de vie singleton — l'avertissement est silencieux
sans validation des scopes. Soit changez la durée de vie, soit résolvez à partir
d'un
IServiceScopeexplicite.
Références
| Sujet | Fichier |
|---|---|
| Examen approfondi (configuration Generic Host, durées de vie, services avec clé, patterns de test, Ioc hérité) | references/dependency-injection.md |
Externe :
- Vue d'ensemble DI : https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection
- Utilisation DI : https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-usage
- Page MVVM Toolkit Ioc : https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/ioc
- Generic Host : https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host