依赖项注入

.NET 多平台应用 UI (.NET MAUI) 提供的内置支持有利于使用依赖项注入。 依赖项注入是控制反转 (IoC) 模式的专用版本,在该模式中,所反转的关注点是获取所需依赖项的过程。 通过依赖关系注入,另一个类负责在运行时将依赖项注入到对象中。

通常会在实例化对象时调用类构造函数,并将对象所需的任何值作为参数传递给构造函数。 这是一个称为构造函数注入的依赖关系注入示例。 对象所需的依赖项注入到构造函数中。

注意

还有其他类型的依赖项注入,例如属性资源库注入和方法调用注入,但它们不太常用。

通过将依赖项指定为接口类型,依赖关系注入可以将具体类型与依赖于这些类型的代码分离。 它通常使用一个容器来保存接口和抽象类型之间的注册和映射列表,以及实现或扩展这些类型的具体类型。

依赖项注入容器

如果一个类没有直接实例化所需的对象,则另一个类必须承担这一责任。 请考虑以下示例,此示例显示了一个需要构造函数参数的视图模型类:

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel(ILoggingService loggingService, ISettingsService settingsService)
    {
        _loggingService = loggingService;
        _settingsService = settingsService;
    }
}

在此示例中,MainPageViewModel 构造函数需要两个接口对象实例作为由另一个类注入的参数。 MainPageViewModel 类中的唯一依赖项是接口类型。 因此,MainPageViewModel 类对于负责实例化接口对象的类一无所知。

同样,请考虑以下示例,此示例显示了一个需要构造函数参数的页面类:

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

在此示例中,MainPage 构造函数需要一个具体类型作为由另一个类注入的参数。 MainPage 类中的唯一依赖项依赖于 MainPageViewModel 类型。 因此,MainPage 类无法感知负责实例化具体类型的类。

在这两种情况下,负责实例化依赖项并将其插入到依赖类中的类称为依赖项注入容器

依赖关系注入容器通过提供一种工具来实例化类实例并根据容器的配置管理它们的生命周期,从而减小了对象之间的耦合度。 在创建对象期间,容器会注入对象所需的任何依赖项。 如果这些依赖项尚未创建,则容器会首先创建并解析其依赖项。

使用依赖关系注入容器有几个优点:

  • 容器不再需要类去定位其依赖项并管理其生存期。
  • 容器允许映射已实现的依赖项,而不会影响类。
  • 容器通过允许模拟依赖项来促进可测试性。
  • 容器通过允许将新类轻松添加到应用来提高可维护性。

在使用模型-视图-视图模型 (MVVM) 模式的 .NET MAUI 应用的上下文中,依赖项注入容器通常用于注册和解析视图、注册和解析视图模型以及注册服务并将其注入视图模型。 有关 MVVM 模式的详细信息,请参阅模型-视图-视图模型 (MVVM)

有许多适用于 .NET 的依赖项注入容器。 .NET MAUI 的内置支持使用 Microsoft.Extensions.DependencyInjection 来管理应用中的视图、视图模型和服务类的实例化。 Microsoft.Extensions.DependencyInjection 有助于生成松散耦合的应用,并提供依赖关系注入容器中常见的所有功能,包括注册类型映射和对象实例、解析对象、管理对象生存期以及将依赖对象注入它解析的对象的构造函数的方法。 有关 Microsoft.Extensions.DependencyInjection 的详细信息,请参阅 .NET 中的依赖关系注入

在运行时,容器必须知道请求的是依赖项的哪个实现,以便为请求的对象实例化它们。 在上面的示例中,需要先解析 ILoggingServiceISettingsService 接口,然后才能实例化 MainPageViewModel 对象。 这涉及到容器执行以下操作:

  • 决定如何实例化实现接口的对象。 这称为“注册”。 有关详细信息,请参阅注册
  • 实例化实现所需接口的对象和 MainPageViewModel 对象。 这称为“解决方法”。 有关详细信息,请参阅解析

最终,应用将不再使用 MainPageViewModel 对象,可以对该对象进行垃圾回收。 此时,如果其他类不共享相同的实例,则垃圾回收器应该会处理任何短暂的接口实现。

注册

在将依赖项注入到对象中之前,必须先将依赖项的类型注册到容器。 注册类型通常涉及向容器传递一个具体类型,或者传递一个接口和实现该接口的具体类型。

向容器注册类型和对象主要有两种方法:

  • 向容器注册类型或映射。 这称为暂时性注册。 如果需要,容器将生成指定类型的实例。
  • 将容器中的现有对象注册为单一实例。 如果需要,容器将返回对现有对象的引用。

注意

依赖项注入容器并不总是适合.NET MAUI 应用。 依赖项注入引入了额外的复杂性和要求,对于较小的应用来说可能不适合或没有用。 如果一个类没有任何依赖项,或者不是其他类型的依赖项,那么将其放入容器中可能没有意义。 此外,如果一个类有一组对该类型不可或缺且永远不会改变的依赖项,那么将它们放入容器中可能没有意义。

需要依赖项注入的类型的注册应该在应用中以单一方法执行。 应在应用的生命周期的早期调用此方法,以确保它了解其类之间的依赖关系。 应用通常应在 MauiProgram 类的 CreateMauiApp 方法中执行此操作。 MauiProgram 类调用 CreateMauiApp 方法来创建 MauiAppBuilder 对象。 MauiAppBuilder 对象具有类型为 IServiceCollectionServices 属性,该属性提供了一个位置来注册类型,例如视图、视图模型和依赖项注入服务:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        builder.Services.AddTransient<ILoggingService, LoggingService>();
        builder.Services.AddTransient<ISettingsService, SettingsService>();
        builder.Services.AddSingleton<MainPageViewModel>();
        builder.Services.AddSingleton<MainPage>();

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

调用 MauiAppBuilder.Build() 时,使用 Services 属性注册的类型将提供给依赖项注入容器。

注册依赖项时,需要注册所有依赖项,包括任何需要依赖项的类型。 因此,如果你有一个将依赖项作为构造函数参数的视图模型,则需将该视图模型与其所有依赖项一起注册。 类似地,如果你有一个将视图模型依赖项作为构造函数参数的视图,则需注册该视图和视图模型及其所有依赖项。

提示

依赖项注入容器非常适合创建视图模型实例。 如果视图模型具有依赖项,它会管理任何必需服务的创建和注入。 只需确保在 MauiProgram 类的 CreateMauiApp 方法中注册视图模型及其可能有的任何依赖项。

在 Shell 应用中,无需将页面注册到依赖项注入容器,除非你想要影响页面相对于容器AddSingletonAddTransient的生存期(或AddScoped方法)。 有关详细信息,请参阅 依赖项生存期

依赖项生存期

根据应用的需求,你可能需要注册具有不同生存期的依赖项。 下表列出了可以用来注册依赖项的主要方法及其注册生存期:

方法 说明
AddSingleton<T> 创建对象的单个实例,该实例将在应用的整个生存期内保留。
AddTransient<T> 在解析过程中收到请求时创建对象的新实例。 暂时性对象没有预定义的生存期,但通常遵循其主机的生存期。
AddScoped<T> 创建与其主机共享生存期的对象实例。 当主机超出范围时,其依赖项也会超出范围。 因此,在同一范围内多次解析相同的依赖项会产生相同的实例,而在不同范围内解析相同的依赖项会产生不同的实例。

注意

如果对象不是从接口继承的(例如视图或视图模型就是如此),则只需向 AddSingleton<T>AddTransient<T>AddScoped<T> 方法提供其具体类型即可。

MainPageViewModel 类在应用的根附近使用,应该始终可用,因此使用 AddSingleton<T> 注册它是有益的。 其他视图模型可以根据情况导航至应用或稍后在应用中使用。 如果你有一种类型可能并非总是在使用,或者它是内存或计算密集型的,或者需要即时数据,那么它可能是 AddTransient<T> 注册的更好候选者。

注册依赖项的另一种常见方式是使用 AddSingleton<TService, TImplementation>AddTransient<TService, TImplementation>AddScoped<TService, TImplementation> 方法。 这些方法有两种类型 - 接口定义和具体实现。 这种注册类型最适合基于接口实现服务的情况。

注册所有类型后,应调用 MauiAppBuilder.Build() 来创建 MauiApp 对象,并使用所有注册的类型填充依赖项注入容器。

重要

一旦调用 MauiAppBuilder.Build(),使用依赖项注入容器注册的类型将不可变,再也不能更新或修改。

使用扩展方法注册依赖项

MauiApp.CreateBuilder 方法会创建一个可用于注册依赖项的 MauiAppBuilder 对象。 如果应用需要注册许多依赖项,可以创建扩展方法来帮助提供有组织的、可维护的注册工作流:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
        => MauiApp.CreateBuilder()
            .UseMauiApp<App>()
            .RegisterServices()
            .RegisterViewModels()
            .RegisterViews()
            .Build();

    public static MauiAppBuilder RegisterServices(this MauiAppBuilder mauiAppBuilder)
    {
        mauiAppBuilder.Services.AddTransient<ILoggingService, LoggingService>();
        mauiAppBuilder.Services.AddTransient<ISettingsService, SettingsService>();

        // More services registered here.

        return mauiAppBuilder;        
    }

    public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder)
    {
        mauiAppBuilder.Services.AddSingleton<MainPageViewModel>();

        // More view-models registered here.

        return mauiAppBuilder;        
    }

    public static MauiAppBuilder RegisterViews(this MauiAppBuilder mauiAppBuilder)
    {
        mauiAppBuilder.Services.AddSingleton<MainPage>();

        // More views registered here.

        return mauiAppBuilder;        
    }
}

在此示例中,三种注册扩展方法使用 MauiAppBuilder 实例访问 Services 属性来注册依赖项。

解决方法

注册类型后,可以将其解析或作为依赖项注入。 在解析类型时,如果容器需要创建一个新实例,它会将任何依赖项注入到该实例中。

一般来说,当一个类型被解析时,会发生以下三种情况之一:

  1. 如果尚未注册该类型,容器将引发异常。
  2. 如果类型已注册为单一实例,容器将返回单一实例。 如果这是首次调用该类型,则容器会根据需要创建它,并维护对它的引用。
  3. 如果该类型已注册为暂时性类型,则容器将返回一个新实例,并且不维护对它的引用。

.NET MAUI 支持自动和显式依赖项解析。 自动依赖项解析使用构造函数注入,无需从容器显式请求依赖项。 通过从容器显式请求依赖项,可以按需进行显式依赖项解析。

自动依赖项解析

自动依赖项解析会发生在使用 .NET MAUI Shell 的应用中,前提是你已经通过依赖项注入容器注册了依赖项的类型和使用该依赖项的类型。

在基于 Shell 的导航期间,.NET MAUI 会查找页面注册,如果找到,则会创建该页面并将任何依赖项注入其构造函数中:

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

在此示例中,MainPage 构造函数接收注入的 MainPageViewModel 实例。 反过来,MainPageViewModel 实例又注入了 ILoggingServiceISettingsService 实例:

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel(ILoggingService loggingService, ISettingsService settingsService)
    {
        _loggingService = loggingService;
        _settingsService = settingsService;
    }
}

此外,在基于 Shell 的应用中,.NET MAUI 会将依赖项注入使用 Routing.RegisterRoute 方法注册的详细信息页面。

显式依赖项解析

当类型仅公开无参数构造函数时,基于 Shell 的应用不能使用构造函数注入。 或者,如果应用不使用 Shell,则需使用显式依赖项解析。

可以从 Element 通过其 Handler.MauiContext.Service 属性(类型为 IServiceProvider)显式访问依赖项注入容器:

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        HandlerChanged += OnHandlerChanged;
    }

    void OnHandlerChanged(object sender, EventArgs e)
    {
        BindingContext = Handler.MauiContext.Services.GetService<MainPageViewModel>();
    }
}

如果需要解析来自 Element 的或来自 Element 构造函数外部的依赖项,则此方法非常有用。 在此示例中,访问 HandlerChanged 事件处理程序中的依赖项注入容器可确保已为页面设置了处理程序,因此 Handler 属性不会为 null

警告

ElementHandler 属性可能是 null,因此请注意,你可能需要考虑到这种情况。 有关详细信息,请参阅处理程序生命周期

在视图模型中,可以通过 Application.Current.MainPageHandler.MauiContext.Service 属性显式访问依赖项注入容器:

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel()
    {
        _loggingService = Application.Current.MainPage.Handler.MauiContext.Services.GetService<ILoggingService>();
        _settingsService = Application.Current.MainPage.Handler.MauiContext.Services.GetService<ISettingsService>();
    }
}

在视图模型中,可以通过以下属性Window.Page显式访问Handler.MauiContext.Service依赖项注入容器:

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel()
    {
        _loggingService = Application.Current.Windows[0].Page.Handler.MauiContext.Services.GetService<ILoggingService>();
        _settingsService = Application.Current.Windows[0].Page.Handler.MauiContext.Services.GetService<ISettingsService>();
    }
}

此方法的缺点是视图模型现在依赖于 Application 类型。 但是,可以通过将 IServiceProvider 参数传递给视图模型构造函数来消除这个缺点。 IServiceProvider 是通过自动依赖项解析来解析的,无需将其注册到依赖项注入容器。 利用这种方法,只要类型在依赖项注入容器中注册,就可以自动解析类型及其 IServiceProvider 依赖项。 然后可以使用 IServiceProvider 进行显式依赖项解析:

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel(IServiceProvider serviceProvider)
    {
        _loggingService = serviceProvider.GetService<ILoggingService>();
        _settingsService = serviceProvider.GetService<ISettingsService>();
    }
}

此外,可以通过 IPlatformApplication.Current.Services 属性在每个平台上访问 IServiceProvider 实例。

XAML 资源的限制

一种常见的情况是使用依赖项注入容器注册一个页面,然后使用自动依赖项解析将其注入到 App 构造函数中并将其设置为 MainPage 属性的值:

public App(MyFirstAppPage page)
{
    InitializeComponent();
    MainPage = page;
}

常见方案是向依赖项注入容器注册页面,并使用自动依赖项解析将其 App 注入构造函数,并将其设置为在应用中显示的第一页:

MyFirstAppPage _firstPage;

public App(MyFirstAppPage page)
{
    InitializeComponent();
    _firstPage = page;
}

protected override Window CreateWindow(IActivationState? activationState)
{
    return new Window(_firstPage);
}

但是,在这种情况下,如果 MyFirstAppPage 尝试访问已在 App 资源字典的 XAML 中声明的 StaticResource,则会引发 XamlParseException,并显示类似于“Position {row}:{column}. StaticResource not found for key {key}”的消息。 发生这种情况的原因是,通过构造函数注入进行解析的页面已在应用程序级 XAML 资源初始化之前创建。

解决此问题的一个方法是将 IServiceProvider 注入到 App 类中,然后使用它来解析 App 类中的页面:

public App(IServiceProvider serviceProvider)
{
    InitializeComponent();
    MainPage = serviceProvider.GetService<MyFirstAppPage>();
}
MyFirstAppPage _firstPage;

public App(IServiceProvider serviceProvider)
{
    InitializeComponent();
    _firstPage = serviceProvider.GetService<MyFirstAppPage>();
}

protected override Window CreateWindow(IActivationState? activationState)
{
    return new Window(_firstPage);
}

此方法强制在解析页面之前创建并初始化 XAML 对象树。