maui-dependency-injection
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDependency Injection in .NET MAUI
.NET MAUI 中的依赖注入
.NET MAUI uses the same container as ASP.NET Core. All service registration happens in on . The container is built once at startup and is immutable thereafter.
Microsoft.Extensions.DependencyInjectionMauiProgram.CreateMauiApp()builder.Services.NET MAUI 使用与 ASP.NET Core 相同的 容器。所有服务注册都在 方法的 上完成。容器在启动时构建一次,之后不可更改。
Microsoft.Extensions.DependencyInjectionMauiProgram.CreateMauiApp()builder.ServicesWhen to Use
适用场景
- Registering services, ViewModels, and Pages in
MauiProgram.cs - Choosing between ,
AddSingleton, andAddTransientAddScoped - Wiring constructor injection for Pages and ViewModels
- Leveraging Shell navigation to auto-resolve DI-registered Pages
- Registering platform-specific service implementations with directives
#if - Designing interfaces for testable service layers
- 在 中注册服务、ViewModel 和页面
MauiProgram.cs - 在 、
AddSingleton和AddTransient之间选择合适的生命周期AddScoped - 为页面和ViewModel配置构造函数注入
- 利用Shell导航自动解析已注册DI的页面
- 使用 指令注册特定平台的服务实现
#if - 为可测试的服务层设计接口
When Not to Use
不适用场景
- XAML data-binding syntax or compiled bindings — use the maui-data-binding skill
- Shell route registration and query parameters — use the maui-shell-navigation skill
- Mocking frameworks or test runners — use standard .NET testing tools (xUnit, NUnit, MSTest) and mocking libraries (NSubstitute, Moq)
- XAML数据绑定语法或编译绑定——请使用 maui-data-binding 技能
- Shell路由注册和查询参数——请使用 maui-shell-navigation 技能
- 模拟框架或测试运行器——请使用标准.NET测试工具(xUnit、NUnit、MSTest)和模拟库(NSubstitute、Moq)
Inputs
输入条件
- A .NET MAUI project with a file
MauiProgram.cs - Knowledge of which services, ViewModels, and Pages need registration
- Target platforms (Android, iOS, Mac Catalyst, Windows) for conditional registrations
- 包含 文件的.NET MAUI项目
MauiProgram.cs - 了解需要注册的服务、ViewModel和页面
- 用于条件注册的目标平台(Android、iOS、Mac Catalyst、Windows)
Workflow
工作流程
- Identify all services, ViewModels, and Pages that need to participate in dependency injection.
- Choose the correct lifetime for each type — for shared services,
AddSingletonfor Pages and ViewModels.AddTransient - Register all types in on
MauiProgram.CreateMauiApp(), grouping by category (services, HTTP, ViewModels, Pages).builder.Services - Register Pages as Shell routes in so Shell navigation auto-resolves the full dependency graph.
AppShell.xaml.cs - Wire each Page to its ViewModel via constructor injection, assigning the ViewModel as .
BindingContext - Add platform-specific registrations with directives, ensuring every target platform is covered or has a fallback.
#if - Verify resolution works by running the app and confirming no dependencies or missing-registration exceptions at runtime.
null
- 确定所有需要参与依赖注入的服务、ViewModel和页面。
- 为每种类型选择正确的生命周期——共享服务使用 ,页面和ViewModel使用
AddSingleton。AddTransient - 在 的
MauiProgram.CreateMauiApp()上注册所有类型,按类别分组(服务、HTTP、ViewModel、页面)。builder.Services - 在 中将页面注册为Shell路由,以便Shell导航自动解析完整的依赖关系图。
AppShell.xaml.cs - 通过构造函数注入将每个页面与其ViewModel关联,将ViewModel赋值为 。
BindingContext - 使用 指令添加特定平台的注册,确保覆盖所有目标平台或提供回退方案。
#if - 通过运行应用验证解析是否正常工作,确认运行时没有依赖项或缺少注册的异常。
null
Lifetime Selection
生命周期选择
| Lifetime | When to Use | Typical Types |
|---|---|---|
| Shared state, expensive to create, app-wide config | |
| Lightweight, stateless, or needs a fresh instance per use | Pages, ViewModels, per-call API wrappers |
| Per-scope lifetime with manually created | Scoped unit-of-work (rare in MAUI) |
Key rule: Register Pages and ViewModels as Transient. Register shared services as Singleton.
⚠️ Avoidunless you manually manageAddScoped. MAUI has no built-in request scope like ASP.NET Core. A Scoped registration without an explicit scope silently behaves as a Singleton, leading to subtle bugs.IServiceScope
| 生命周期 | 适用场景 | 典型类型 |
|---|---|---|
| 共享状态、创建成本高、应用级配置 | |
| 轻量级、无状态或每次使用需要新实例 | 页面、ViewModel、单次调用的API包装器 |
| 手动创建 | 作用域工作单元(在MAUI中很少使用) |
核心规则: 将页面和ViewModel注册为 Transient。将共享服务注册为 Singleton。
⚠️ 除非手动管理,否则避免使用IServiceScope。 MAUI没有像ASP.NET Core那样内置的请求作用域。没有显式作用域的Scoped注册会默认表现为Singleton,导致难以发现的bug。AddScoped
Registration Pattern in MauiProgram.cs
MauiProgram.cs 中的注册模式
csharp
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Services — Singleton for shared state
builder.Services.AddSingleton<IDataService, DataService>();
builder.Services.AddSingleton<ISettingsService, SettingsService>();
// HTTP — use typed or named clients via IHttpClientFactory
// Requires NuGet: Microsoft.Extensions.Http
builder.Services.AddHttpClient<IApiClient, ApiClient>();
// ViewModels — Transient for fresh state per navigation
builder.Services.AddTransient<MainViewModel>();
builder.Services.AddTransient<DetailViewModel>();
// Pages — Transient so constructor injection fires each time
builder.Services.AddTransient<MainPage>();
builder.Services.AddTransient<DetailPage>();
return builder.Build();
}csharp
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// 服务 —— Singleton用于共享状态
builder.Services.AddSingleton<IDataService, DataService>();
builder.Services.AddSingleton<ISettingsService, SettingsService>();
// HTTP —— 通过IHttpClientFactory使用类型化或命名客户端
// 需要NuGet包: Microsoft.Extensions.Http
builder.Services.AddHttpClient<IApiClient, ApiClient>();
// ViewModel —— Transient用于每次导航的全新状态
builder.Services.AddTransient<MainViewModel>();
builder.Services.AddTransient<DetailViewModel>();
// 页面 —— Transient确保每次都触发构造函数注入
builder.Services.AddTransient<MainPage>();
builder.Services.AddTransient<DetailPage>();
return builder.Build();
}Constructor Injection
构造函数注入
Inject dependencies through constructor parameters. The container resolves them automatically when the type is itself resolved from DI.
csharp
public class MainViewModel
{
private readonly IDataService _dataService;
public MainViewModel(IDataService dataService)
{
_dataService = dataService;
}
public async Task LoadAsync() => Items = await _dataService.GetItemsAsync();
}通过构造函数参数注入依赖项。当类型本身从DI中解析时,容器会自动解析这些依赖项。
csharp
public class MainViewModel
{
private readonly IDataService _dataService;
public MainViewModel(IDataService dataService)
{
_dataService = dataService;
}
public async Task LoadAsync() => Items = await _dataService.GetItemsAsync();
}ViewModel → Page Wiring
ViewModel → 页面关联
Register both Page and ViewModel. Inject the ViewModel into the Page and assign it as :
BindingContextcsharp
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}同时注册页面和ViewModel。将ViewModel注入页面并赋值为 :
BindingContextcsharp
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}Shell Navigation Auto-Resolution
Shell导航自动解析
When a Page is registered in DI and as a Shell route, Shell resolves it (and its full dependency graph) automatically on navigation:
csharp
// MauiProgram.cs
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();
// AppShell.xaml.cs
Routing.RegisterRoute(nameof(DetailPage), typeof(DetailPage));
// Navigate — DI resolves DetailPage + DetailViewModel
await Shell.Current.GoToAsync(nameof(DetailPage));当页面在DI中注册并且作为Shell路由时,Shell会在导航时自动解析页面(及其完整的依赖关系图):
csharp
// MauiProgram.cs
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();
// AppShell.xaml.cs
Routing.RegisterRoute(nameof(DetailPage), typeof(DetailPage));
// 导航 —— DI解析DetailPage + DetailViewModel
await Shell.Current.GoToAsync(nameof(DetailPage));Platform-Specific Registration
特定平台注册
Use preprocessor directives to register platform implementations. Always cover every target platform or provide a no-op fallback to avoid runtime .
nullcsharp
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#elif WINDOWS
builder.Services.AddSingleton<INotificationService, WindowsNotificationService>();
#else
builder.Services.AddSingleton<INotificationService, NoOpNotificationService>();
#endif使用预处理器指令注册平台实现。始终覆盖所有目标平台或提供无操作回退,以避免运行时出现。
nullcsharp
#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS || MACCATALYST
builder.Services.AddSingleton<INotificationService, AppleNotificationService>();
#elif WINDOWS
builder.Services.AddSingleton<INotificationService, WindowsNotificationService>();
#else
builder.Services.AddSingleton<INotificationService, NoOpNotificationService>();
#endifExplicit Resolution (Last Resort)
显式解析(最后手段)
Prefer constructor injection. Use explicit resolution only where injection is genuinely unavailable (custom handlers, platform callbacks):
csharp
// From any Element with a Handler
var service = this.Handler.MauiContext.Services.GetService<IDataService>();For dynamic resolution, inject :
IServiceProvidercsharp
public class NavigationService(IServiceProvider serviceProvider)
{
public T ResolvePage<T>() where T : Page
=> serviceProvider.GetRequiredService<T>();
}优先使用构造函数注入。仅在确实无法注入的场景(自定义处理程序、平台回调)中使用显式解析:
csharp
// 从任何带有Handler的Element获取
var service = this.Handler.MauiContext.Services.GetService<IDataService>();对于动态解析,注入 :
IServiceProvidercsharp
public class NavigationService(IServiceProvider serviceProvider)
{
public T ResolvePage<T>() where T : Page
=> serviceProvider.GetRequiredService<T>();
}Interface-First Pattern for Testability
面向接口的可测试性模式
Define interfaces for every service so implementations can be swapped in tests:
csharp
public interface IDataService
{
Task<List<Item>> GetItemsAsync();
}
// Production registration
builder.Services.AddSingleton<IDataService, DataService>();
// Test registration — swap without touching production code
var services = new ServiceCollection();
services.AddSingleton<IDataService, FakeDataService>();为每个服务定义接口,以便在测试中替换实现:
csharp
public interface IDataService
{
Task<List<Item>> GetItemsAsync();
}
// 生产环境注册
builder.Services.AddSingleton<IDataService, DataService>();
// 测试环境注册 —— 无需修改生产代码即可替换
var services = new ServiceCollection();
services.AddSingleton<IDataService, FakeDataService>();Common Pitfalls
常见陷阱
1. Singleton ViewModels Cause Stale Data
1. Singleton ViewModel导致数据过期
csharp
// ❌ ViewModel keeps stale state across navigations
builder.Services.AddSingleton<DetailViewModel>();
// ✅ Fresh instance each navigation
builder.Services.AddTransient<DetailViewModel>();csharp
// ❌ ViewModel在多次导航间保留过期状态
builder.Services.AddSingleton<DetailViewModel>();
// ✅ 每次导航获取新实例
builder.Services.AddTransient<DetailViewModel>();2. Unregistered Page Silently Skips Injection
2. 未注册的页面会静默跳过注入
If a Page appears in Shell XAML via but is not registered in , MAUI creates it with the parameterless constructor. Dependencies are silently — no exception is thrown.
<ShellContent ContentTemplate="...">builder.Servicesnullcsharp
// ❌ Missing — injection silently skipped
// builder.Services.AddTransient<DetailPage>();
// ✅ Always register pages that need injection
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();如果页面通过出现在Shell XAML中但未在中注册,MAUI会使用无参构造函数创建它。依赖项会静默变为——不会抛出异常。
<ShellContent ContentTemplate="...">builder.Servicesnullcsharp
// ❌ 缺失注册 —— 注入被静默跳过
// builder.Services.AddTransient<DetailPage>();
// ✅ 始终注册需要注入的页面
builder.Services.AddTransient<DetailPage>();
builder.Services.AddTransient<DetailViewModel>();3. XAML Resource Parsing vs. DI Timing
3. XAML资源解析与DI时机
XAML resources in are parsed during — before the container is fully available. Defer service-dependent work to :
App.xamlInitializeComponent()CreateWindow()csharp
public partial class App : Application
{
private readonly IServiceProvider _services;
public App(IServiceProvider services)
{
_services = services;
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
// Safe — container is fully built
// Requires: builder.Services.AddTransient<AppShell>() in MauiProgram.cs
var appShell = _services.GetRequiredService<AppShell>();
return new Window(appShell);
}
}App.xamlInitializeComponent()CreateWindow()csharp
public partial class App : Application
{
private readonly IServiceProvider _services;
public App(IServiceProvider services)
{
_services = services;
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
// 安全操作 —— 容器已完全构建
// 需要在MauiProgram.cs中添加:builder.Services.AddTransient<AppShell>()
var appShell = _services.GetRequiredService<AppShell>();
return new Window(appShell);
}
}4. Service Locator Anti-Pattern
4. 服务定位器反模式
csharp
// ❌ Hides dependencies, hard to test
var svc = this.Handler.MauiContext.Services.GetService<IDataService>();
// ✅ Constructor injection — explicit and testable
public class MyViewModel(IDataService dataService) { }csharp
// ❌ 隐藏依赖项,难以测试
var svc = this.Handler.MauiContext.Services.GetService<IDataService>();
// ✅ 构造函数注入 —— 显式且可测试
public class MyViewModel(IDataService dataService) { }5. Missing Platform in Conditional Registration
5. 条件注册中遗漏平台
Forgetting a platform in blocks means returns at runtime on that platform. Always include an fallback or cover every target.
#ifGetService<T>()null#else在块中忘记某个平台意味着该平台上运行时会返回。始终包含回退或覆盖所有目标平台。
#ifGetService<T>()null#else6. AddScoped Without Manual Scope
6. 未手动管理作用域时使用AddScoped
AddScopedIServiceScopeAddTransientAddSingleton在MAUI中未手动创建的情况下使用会默认表现为Singleton行为。除非显式管理作用域,否则请改用或。
IServiceScopeAddScopedAddTransientAddSingletonChecklist
检查清单
- Every Page and ViewModel that needs injection is registered in
MauiProgram.cs - Pages and ViewModels use ; shared services use
AddTransientAddSingleton - Constructor injection used everywhere possible; service locator only as last resort
- Interfaces defined for services that need test substitution
- Platform-specific registrations cover all target platforms or include a fallback
#if - Service-dependent work deferred to , not run during XAML parse
CreateWindow() - only used alongside manually created
AddScopedIServiceScope
- 所有需要注入的页面和ViewModel都已在中注册
MauiProgram.cs - 页面和ViewModel使用;共享服务使用
AddTransientAddSingleton - 尽可能使用构造函数注入;仅在万不得已时使用服务定位器
- 为需要测试替换的服务定义了接口
- 特定平台的注册覆盖了所有目标平台或包含回退方案
#if - 依赖服务的工作推迟到,而非在XAML解析期间执行
CreateWindow() - 仅与手动创建的
AddScoped一起使用IServiceScope