Xamarin.Forms 中的依赖项解析

本文介绍如何将依赖项解析方法注入 Xamarin.Forms,以便应用程序的依赖项注入容器可以控制自定义呈现器、效果和 DependencyService 实现的创建和生存期。

在使用 Model-View-ViewModel (MVVM) 模式的 Xamarin.Forms 应用程序中,依赖项注入容器可用于注册和解析视图模型,以及注册服务并将其注入视图模型。 在创建视图模型期间,容器会注入所需的任何依赖项。 如果未创建这些依赖项,容器会先创建并解析依赖项。 有关依赖项注入的详细信息,包括将依赖项注入视图模型的示例,请参阅依赖项注入

在平台项目中控制类型的创建和生存期通常由 Xamarin.Forms 执行,Activator.CreateInstance 方法用于创建自定义呈现器、效果和 DependencyService 实现的实例。 遗憾的是,这限制了开发人员对这些类型的创建和生存期的控制,以及向其注入依赖项的能力。 可以通过将依赖项解析方法注入 Xamarin.Forms 来更改此行为,该方法控制类型的创建方式 – 由应用程序依赖项注入容器或由 Xamarin.Forms 创建。 但是,请注意,无需将依赖项解析方法注入 Xamarin.Forms。 如果未注入依赖项解析方法,Xamarin.Forms 将继续在平台项目中创建和管理类型的生存期。

注意

虽然本文重点介绍如何将依赖项解析方法注入 Xamarin.Forms,该方法使用依赖项注入容器解析已注册的类型,但也可以注入使用工厂方法解析已注册类型的依赖项解析方法。

注入依赖项解析方法

DependencyResolver 类提供了使用 ResolveUsing 方法将依赖项解析方法注入 Xamarin.Forms 的功能。 然后,当 Xamarin.Forms 需要特定类型的实例时,将有机会提供实例的依赖项解析方法。 如果依赖项解析方法返回请求类型的 null,Xamarin.Forms 会回退到尝试使用 Activator.CreateInstance 方法创建类型实例本身。

以下示例演示如何使用 ResolveUsing 方法设置依赖项解析方法:

using Autofac;
using Xamarin.Forms.Internals;
...

public partial class App : Application
{
    // IContainer and ContainerBuilder are provided by Autofac
    static IContainer container;
    static readonly ContainerBuilder builder = new ContainerBuilder();

    public App()
    {
        ...
        DependencyResolver.ResolveUsing(type => container.IsRegistered(type) ? container.Resolve(type) : null);
        ...
    }
    ...
}

在此示例中,依赖项解析方法设置为 lambda 表达式,该表达式使用 Autofac 依赖项注入容器解析已注册到容器的任何类型。 否则,将返回 null,这将导致 Xamarin.Forms 尝试解析类型。

注意

依赖项注入容器使用的 API 特定于容器。 本文中的代码示例使用 Autofac 作为依赖项注入容器,该容器提供 IContainerContainerBuilder 类型。 可以同样使用备用依赖项注入容器,但会使用不同于此处提供的 API。

请注意,在应用程序启动期间,无需设置依赖项解析方法。 可以随时设置它。 唯一的约束是,当应用程序尝试使用存储在依赖项注入容器中的类型时,Xamarin.Forms 需要了解依赖项解析方法。 因此,如果在启动期间应用程序需要依赖注入容器中的服务,则必须在应用程序的生命周期早期设置依赖项解析方法。 同样,如果依赖项注入容器管理特定 Effect 的创建和生存期,则 Xamarin.Forms 在尝试创建使用该 Effect 的视图之前,需要知道依赖项解析方法。

警告

使用依赖项注入容器注册和解析类型具有性能成本,因为容器使用反射来创建每种类型,尤其是在应用程序中正在重新构造每个页面导航的依赖项时。 如果存在许多或深度依赖关系,则创建成本会显著增加。

注册类型

必须先向依赖项注入容器注册类型,然后才能通过依赖项解析方法解析类型。 以下代码示例显示了示例应用程序在 Autofac 容器的 App 类中公开的注册方法:

using Autofac;
using Autofac.Core;
...

public partial class App : Application
{
    static IContainer container;
    static readonly ContainerBuilder builder = new ContainerBuilder();
    ...

    public static void RegisterType<T>() where T : class
    {
        builder.RegisterType<T>();
    }

    public static void RegisterType<TInterface, T>() where TInterface : class where T : class, TInterface
    {
        builder.RegisterType<T>().As<TInterface>();
    }

    public static void RegisterTypeWithParameters<T>(Type param1Type, object param1Value, Type param2Type, string param2Name) where T : class
    {
        builder.RegisterType<T>()
               .WithParameters(new List<Parameter>()
        {
            new TypedParameter(param1Type, param1Value),
            new ResolvedParameter(
                (pi, ctx) => pi.ParameterType == param2Type && pi.Name == param2Name,
                (pi, ctx) => ctx.Resolve(param2Type))
        });
    }

    public static void RegisterTypeWithParameters<TInterface, T>(Type param1Type, object param1Value, Type param2Type, string param2Name) where TInterface : class where T : class, TInterface
    {
        builder.RegisterType<T>()
               .WithParameters(new List<Parameter>()
        {
            new TypedParameter(param1Type, param1Value),
            new ResolvedParameter(
                (pi, ctx) => pi.ParameterType == param2Type && pi.Name == param2Name,
                (pi, ctx) => ctx.Resolve(param2Type))
        }).As<TInterface>();
    }

    public static void BuildContainer()
    {
        container = builder.Build();
    }
    ...
}

当应用程序使用依赖项解析方法解析容器中的类型时,通常从平台项目执行类型注册。 这使平台项目能够为自定义呈现器、效果和 DependencyService 实现注册类型。

从平台项目进行类型注册后,必须生成 IContainer 对象,该对象是通过调用 BuildContainer 方法完成的。 此方法在 ContainerBuilder 实例上调用 Autofac 的 Build 方法,该方法生成包含已进行的注册的新依赖项注入容器。

在后面的部分中,将实现 ILogger 接口的 Logger 类注入到类构造函数中。 Logger 类使用 Debug.WriteLine 方法实现简单的日志记录功能,并用于演示如何将服务注入到自定义呈现器、效果和 DependencyService 实现中。

注册自定义呈现器

示例应用程序包括一个播放 Web 视频的页面,其 XAML 源显示在以下示例中:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:video="clr-namespace:FormsVideoLibrary"
             ...>
    <video:VideoPlayer Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4" />
</ContentPage>

VideoPlayer 视图由 VideoPlayerRenderer 类在每个平台上实现,该类提供播放视频的功能。 有关这些自定义呈现器类的详细信息,请参阅实现视频播放器

在 iOS 和通用 Windows 平台 (UWP)上,VideoPlayerRenderer 类具有以下构造函数,这需要 ILogger 参数:

public VideoPlayerRenderer(ILogger logger)
{
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

在所有平台上,依赖注入容器的类型注册由 RegisterTypes 方法执行,该方法在平台使用 LoadApplication(new App()) 方法加载应用程序之前调用。 以下示例演示 iOS 平台上的 RegisterTypes 方法:

void RegisterTypes()
{
    App.RegisterType<ILogger, Logger>();
    App.RegisterType<FormsVideoLibrary.iOS.VideoPlayerRenderer>();
    App.BuildContainer();
}

在此示例中,Logger 具体类型通过对其接口类型的映射进行注册,并且直接注册 VideoPlayerRenderer 类型,而无需接口映射。 当用户导航到包含 VideoPlayer 视图的页面时,将调用依赖项解析方法,以便从依赖项注入容器解析 VideoPlayerRenderer 类型,这将解析 Logger 类型并将其注入到 VideoPlayerRenderer 构造函数中。

Android 平台上的 VideoPlayerRenderer 构造函数稍微复杂一些,因为它除了 ILogger 参数外,还需要 Context 参数:

public VideoPlayerRenderer(Context context, ILogger logger) : base(context)
{
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

以下示例演示 Android 平台上的 RegisterTypes 方法:

void RegisterTypes()
{
    App.RegisterType<ILogger, Logger>();
    App.RegisterTypeWithParameters<FormsVideoLibrary.Droid.VideoPlayerRenderer>(typeof(Android.Content.Context), this, typeof(ILogger), "logger");
    App.BuildContainer();
}

在此示例中,App.RegisterTypeWithParameters 方法向依赖项注入容器注册 VideoPlayerRenderer。 注册方法可确保将 MainActivity 实例注入为 Context 参数,并将 Logger 类型作为 ILogger 参数注入。

注册效果

示例应用程序包含一个页面,该页面使用触摸跟踪效果在页面周围拖动 BoxView 实例。 Effect 使用以下代码添加到 BoxView

var boxView = new BoxView { ... };
var touchEffect = new TouchEffect();
boxView.Effects.Add(touchEffect);

TouchEffect 类是由属于 PlatformEffectTouchEffect 类在每个平台上实现的 RoutingEffect。 平台 TouchEffect 类提供在页面上四处拖动 BoxView 的功能。 有关这些效果类的详细信息,请参阅从效果调用事件

在所有平台上,TouchEffect 类具有以下构造函数,这需要 ILogger 参数:

public TouchEffect(ILogger logger)
{
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

在所有平台上,依赖注入容器的类型注册由 RegisterTypes 方法执行,该方法在平台使用 LoadApplication(new App()) 方法加载应用程序之前调用。 以下示例演示 Android 平台上的 RegisterTypes 方法:

void RegisterTypes()
{
    App.RegisterType<ILogger, Logger>();
    App.RegisterType<TouchTracking.Droid.TouchEffect>();
    App.BuildContainer();
}

在此示例中,Logger 具体类型通过对其接口类型的映射进行注册,并且直接注册 TouchEffect 类型,而无需接口映射。 当用户导航到包含附加了 TouchEffectBoxView 实例的页面时,将调用依赖项解析方法,以便从依赖项注入容器解析平台 TouchEffect 类型,该容器还将解析 Logger 类型并将其注入到 TouchEffect 构造函数中。

注册 DependencyService 实现

示例应用程序包括一个页面,该页面使用每个平台上的 DependencyService 实现,以允许用户从设备的图片库中选取照片。 IPhotoPicker 接口定义由 DependencyService 实现所实现的功能,如以下示例所示:

public interface IPhotoPicker
{
    Task<Stream> GetImageStreamAsync();
}

在每个平台项目中,PhotoPicker 类使用平台 API 实现 IPhotoPicker 接口。 有关这些依赖项服务的详细信息,请参阅从图片库选取照片

在 iOS 和 UWP 上,PhotoPicker 类具有以下构造函数,这需要 ILogger 参数:

public PhotoPicker(ILogger logger)
{
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

在所有平台上,依赖注入容器的类型注册由 RegisterTypes 方法执行,该方法在平台使用 LoadApplication(new App()) 方法加载应用程序之前调用。 以下示例演示 UWP 上的 RegisterTypes 方法:

void RegisterTypes()
{
    DIContainerDemo.App.RegisterType<ILogger, Logger>();
    DIContainerDemo.App.RegisterType<IPhotoPicker, Services.UWP.PhotoPicker>();
    DIContainerDemo.App.BuildContainer();
}

在此示例中,Logger 具体类型通过对其接口类型的映射进行注册,PhotoPicker 类型也通过接口映射注册。

Android 平台上的 PhotoPicker 构造函数稍微复杂一些,因为它除了 ILogger 参数外,还需要 Context 参数:

public PhotoPicker(Context context, ILogger logger)
{
    _context = context ?? throw new ArgumentNullException(nameof(context));
    _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

以下示例演示 Android 平台上的 RegisterTypes 方法:

void RegisterTypes()
{
    App.RegisterType<ILogger, Logger>();
    App.RegisterTypeWithParameters<IPhotoPicker, Services.Droid.PhotoPicker>(typeof(Android.Content.Context), this, typeof(ILogger), "logger");
    App.BuildContainer();
}

在此示例中,App.RegisterTypeWithParameters 方法向依赖项注入容器注册 PhotoPicker。 注册方法可确保将 MainActivity 实例注入为 Context 参数,并将 Logger 类型作为 ILogger 参数注入。

当用户导航到照片选取页并选择选择照片时,将执行 OnSelectPhotoButtonClicked 处理程序:

async void OnSelectPhotoButtonClicked(object sender, EventArgs e)
{
    ...
    var photoPickerService = DependencyService.Resolve<IPhotoPicker>();
    var stream = await photoPickerService.GetImageStreamAsync();
    if (stream != null)
    {
        image.Source = ImageSource.FromStream(() => stream);
    }
    ...
}

调用 DependencyService.Resolve<T> 方法时,将调用依赖项解析方法,以便从依赖项注入容器解析 PhotoPicker 类型,该容器还将解析 Logger 类型并将其注入到 PhotoPicker 构造函数中。

注意

通过 DependencyService 从应用程序的依赖项注入容器解析类型时,必须使用 Resolve<T> 方法。