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封装 |
| 配合手动创建的 | 按scope的工作单元(MAUI中很少使用) |
核心规则: 页面和ViewModel注册为Transient,共享服务注册为Singleton。
⚠️ 除非手动管理,否则避免使用IServiceScope。 MAUI没有ASP.NET Core内置的请求scope,没有显式scope的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. 未手动管理scope却使用AddScoped
AddScopedIServiceScopeAddTransientAddSingletonMAUI中未手动创建的注册会静默表现为Singleton行为,除非明确管理scope,否则请使用或。
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使用注册;共享服务使用
AddTransient注册AddSingleton - 尽可能使用构造函数注入,仅万不得已时使用服务定位器
- 需要测试替换的服务都已定义接口
- 平台专属注册覆盖了所有目标平台或包含降级实现
#if - 依赖服务的逻辑延迟到中执行,不在XAML解析阶段运行
CreateWindow() - 仅在搭配手动创建的时使用
IServiceScopeAddScoped