关于 mvvm-toolkit-di
面向 XAML 应用开发者的 MVVM Toolkit 与 Microsoft.Extensions.DependencyInjection 配置技能,涵盖依赖注册、构造函数注入和生命周期选择,并说明如何避免服务定位器问题。
主要功能
- 01配置 Generic Host 组合根
- 02使用构造函数注入依赖
- 03选择服务与 ViewModel 生命周期
- 04注册并注入 IMessenger
- 05配置键控服务
- 06处理依赖注入常见问题
适用场景
- 01为新的 XAML 应用搭建依赖注入
- 02按页面或文档管理 ViewModel 实例
- 03在 ObservableRecipient 中使用 IMessenger
- 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并将其注入到ObservableRecipientViewModel 中 - 解析页面的 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 应用中的典型用途 |
|---|---|---|
| Singleton | AddSingleton<T> | Shell/主窗口 VM、设置、文件/HTTP 服务、共享的 IMessenger、应用级缓存 |
| Transient | AddTransient<T> | 每页面或每文档的 ViewModel(每次解析都创建新实例) |
| Scoped | AddScoped<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 容器构造的类中,优先使用构造函数注入。
常见陷阱
- 在 VM 构造函数中使用
Ioc.Default.GetService<T>()。 隐藏了 依赖关系,破坏了单元测试,阻碍了启动时的依赖图验证。 - 所有服务都注册为
Singleton。 一个“按文档”的 VM 被注册为单例 后,会变成所有文档之间共享的状态 —— 导致隐蔽的数据损坏。 对于按实例的 VM,请使用AddTransient。 - 多次调用
BuildServiceProvider()。 每次调用都是全新的 容器 —— 单例不会共享。请在启动时只构建一次。 - 在长生命周期对象中捕获
IServiceProvider。 这表明使用了 服务定位器模式。请注入你所需的具体依赖项。 - 开发环境中没有作用域验证。 请使用
Host.CreateDefaultBuilder()(它在开发环境中会设置ValidateScopes和ValidateOnBuild), 这样注册错误会在启动时失败,而不是在首次使用时失败。 - 从根提供程序解析作用域服务。 它们实际上
被提升为单例生命周期 —— 在没有作用域验证的情况下,该警告是
静默的。要么更改生命周期,要么从显式的
IServiceScope中解析。
参考资料
| 主题 | 文件 |
|---|---|
| 完整深入讲解(Generic Host 设置、生命周期、键控服务、测试模式、旧版 Ioc) | references/dependency-injection.md |
外部链接:
- DI 概述:https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection
- DI 用法:https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-usage
- 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