mvvm-toolkit-di

Par github · awesome-copilot

Intégrez les ViewModels CommunityToolkit.Mvvm dans Microsoft.Extensions.DependencyInjection. Couvre la composition root du .NET Generic Host, l'injection par constructeur, les durées de vie des services (Singleton / Transient / Scoped), l'enregistrement d'IMessenger, la résolution des ViewModels dans les Views, les services à clé, les points de test, et l'échappatoire legacy `Ioc.Default`. Compatible avec WPF, WinUI 3, .NET MAUI, Uno et Avalonia.

npx skills add https://github.com/github/awesome-copilot --skill mvvm-toolkit-di

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. Évitez Ioc.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 IMessenger une fois et l'injecter dans les ViewModels ObservableRecipient
  • 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.json via Microsoft.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

  1. 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.
  2. Tout en Singleton. Un VM « par document » enregistré comme singleton devient un état partagé entre tous les documents — corruption de données subtile. Utilisez AddTransient pour les VMs par instance.
  3. Plusieurs appels à BuildServiceProvider(). Chaque appel crée un conteneur frais — les singletons ne sont pas partagés. Construisez une seule fois au démarrage.
  4. Capturer IServiceProvider dans des objets de longue durée. Indique un pattern service-locator. Injectez les dépendances spécifiques que vous avez besoin.
  5. Pas de validation des scopes en développement. Utilisez Host.CreateDefaultBuilder() (qui définit ValidateScopes et ValidateOnBuild en développement) pour que les erreurs d'enregistrement échouent au démarrage, pas à la première utilisation.
  6. 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 IServiceScope explicite.

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 :

Skills similaires