mcpField
M
mvvm-toolkit-di

mvvm-toolkit-di

github.com/github开发40.5k94 次安装今天更新
代码仓库

关于 mvvm-toolkit-di

面向 XAML 应用开发者的 MVVM Toolkit 与 Microsoft.Extensions.DependencyInjection 配置技能,涵盖依赖注册、构造函数注入和生命周期选择,并说明如何避免服务定位器问题。

主要功能

  1. 01配置 Generic Host 组合根
  2. 02使用构造函数注入依赖
  3. 03选择服务与 ViewModel 生命周期
  4. 04注册并注入 IMessenger
  5. 05配置键控服务
  6. 06处理依赖注入常见问题

适用场景

  1. 01为新的 XAML 应用搭建依赖注入
  2. 02按页面或文档管理 ViewModel 实例
  3. 03在 ObservableRecipient 中使用 IMessenger
  4. 04排查启动时的依赖解析错误

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/mvvm-toolkit-di确认已加载。

附带文件references/dependency-injection.md

详细介绍

CommunityToolkit.Mvvm + Microsoft.Extensions.DependencyInjection

MVVM Toolkit 有意不附带 DI 容器——它与 Microsoft.Extensions.DependencyInjection 组合使用,后者正是 ASP.NET Core、Worker 服务和 .NET 泛型主机所用的同一个容器。

TL;DR。 在启动时一次性构建服务提供程序(优先使用 Host.CreateDefaultBuilder())。注册服务和 ViewModel。 通过构造函数注入。避免在用户代码中调用 Ioc.Default.GetService<T>()。


何时使用本技能

  • 为新 XAML 应用(WPF、WinUI 3、 MAUI、Uno、Avalonia)搭建组合根
  • 选择服务/VM 生命周期
  • 一次性接好 IMessenger 并将其注入到 ObservableRecipient ViewModel 中
  • 解析页面的 ViewModel,而不与服务定位器耦合
  • 诊断「Unable to resolve service for type X while attempting to activate Y」

关于源生成器和 ViewModel 模式,请参阅 mvvm-toolkit 技能。关于 Messenger 的发布/订阅,请参阅 mvvm-toolkit-messenger。


推荐的组合根(泛型主机)

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>();
}

泛型主机的优势:

  • 通过 Microsoft.Extensions.Configuration 绑定 appsettings.json
  • 通过 Microsoft.Extensions.Logging 进行日志记录
  • 使用托管服务(IHostedService)处理后台工作
  • 在开发版本中进行作用域验证

WPF 和 Windows Forms 必须将主机生命周期与应用 生命周期集成——请参阅 Use the .NET Generic Host in a WPF app。

不使用泛型主机

当你只需要一个服务容器,并且希望零额外依赖时:

var services = new ServiceCollection();
services.AddSingleton<IFilesService, FilesService>();
services.AddTransient<ContactViewModel>();
ServiceProvider provider = services.BuildServiceProvider();

构造函数注入

通过构造函数注入服务和子 ViewModel:

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!);
    }
}

为什么构造函数注入优于服务定位器:

  • 依赖关系在调用处明确可见
  • 单元测试可直接注入伪造对象/模拟对象
  • DI 容器在启动时验证依赖关系图
  • 缺少注册会立即抛出异常,而不是等到首次使用时

生命周期

生命周期方法XAML 应用中的典型用途
SingletonAddSingleton<T>Shell/主窗口 VM、设置、文件/HTTP 服务、共享的 IMessenger、应用级缓存
TransientAddTransient<T>每页面或每文档的 ViewModel(每次解析都创建新实例)
ScopedAddScoped<T>客户端应用中很少需要;与显式 IServiceScope 配合时有用(例如每窗口作用域)
services.AddSingleton<ShellViewModel>();   // 1 instance for app lifetime
services.AddTransient<NoteViewModel>();    // new instance per resolve
services.AddScoped<DialogService>();       // 1 per scope (rare)

在 View 中解析

在代码隐藏中解析页面的根 ViewModel,然后让它自行拉取其 依赖项:

public sealed partial class ContactPage : Page
{
    public ContactViewModel ViewModel { get; }

    public ContactPage()
    {
        ViewModel = App.GetService<ContactViewModel>();
        InitializeComponent();
    }
}

在 XAML 中使用 {x:Bind ViewModel.Xxx}(编译绑定)或 针对 DataContext 的 {Binding Xxx} 进行绑定。

对于导航框架(WinUI 3 Frame.Navigate、MAUI Shell、Prism、 MVVMCross),让框架解析页面,页面再从 DI 解析其 ViewModel。不要手动 new ViewModel。


IMessenger 注册

一次性注册你想要的 messenger,然后到处注入 IMessenger:

services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
// or
services.AddSingleton<IMessenger>(StrongReferenceMessenger.Default);

然后:

public sealed partial class MyViewModel(IMessenger messenger)
    : ObservableRecipient(messenger) { }

对于每窗口的 messenger,使用键控服务或作为作用域 实例注册,并注入到每窗口的 ViewModel 中。

关于 messenger 的接口范围,请参阅 mvvm-toolkit-messenger 技能。


键控服务(.NET 8+)

按键解析同一接口的不同实现:

services.AddKeyedSingleton<IExporter, CsvExporter>("csv");
services.AddKeyedSingleton<IExporter, JsonExporter>("json");

public sealed partial class ExportViewModel(
    [FromKeyedServices("csv")] IExporter csvExporter,
    [FromKeyedServices("json")] IExporter jsonExporter)
    : ObservableObject { /* ... */ }

测试接缝

通过构造函数注入的依赖项在测试中替换起来轻而易举。使用 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);
}

如果你在模拟 Ioc.Default 或静态状态,说明该 ViewModel 正在使用 服务定位器——请重构为构造函数注入。


旧版:Ioc.Default

CommunityToolkit.Mvvm.DependencyInjection.Ioc 是一种应急方案,适用于 无法使用构造函数注入的场景 —— 由 XAML 实例化的 VM (用于设计时数据)、ValueConverter、控件模板。

Ioc.Default.ConfigureServices(
    new ServiceCollection()
        .AddSingleton<IFilesService, FilesService>()
        .AddTransient<ContactViewModel>()
        .BuildServiceProvider());

var files = Ioc.Default.GetRequiredService<IFilesService>();

请将其视为最后的手段。在 ViewModel、服务以及任何 可由 DI 容器构造的类中,优先使用构造函数注入。


常见陷阱

  1. 在 VM 构造函数中使用 Ioc.Default.GetService<T>()。 隐藏了 依赖关系,破坏了单元测试,阻碍了启动时的依赖图验证。
  2. 所有服务都注册为 Singleton。 一个“按文档”的 VM 被注册为单例 后,会变成所有文档之间共享的状态 —— 导致隐蔽的数据损坏。 对于按实例的 VM,请使用 AddTransient。
  3. 多次调用 BuildServiceProvider()。 每次调用都是全新的 容器 —— 单例不会共享。请在启动时只构建一次。
  4. 在长生命周期对象中捕获 IServiceProvider。 这表明使用了 服务定位器模式。请注入你所需的具体依赖项。
  5. 开发环境中没有作用域验证。 请使用 Host.CreateDefaultBuilder() (它在开发环境中会设置 ValidateScopes 和 ValidateOnBuild), 这样注册错误会在启动时失败,而不是在首次使用时失败。
  6. 从根提供程序解析作用域服务。 它们实际上 被提升为单例生命周期 —— 在没有作用域验证的情况下,该警告是 静默的。要么更改生命周期,要么从显式的 IServiceScope 中解析。

参考资料

主题文件
完整深入讲解(Generic Host 设置、生命周期、键控服务、测试模式、旧版 Ioc)references/dependency-injection.md

外部链接:

评论

支持 Markdown