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 作为依赖项注入容器,该容器提供 IContainer
和 ContainerBuilder
类型。 可以同样使用备用依赖项注入容器,但会使用不同于此处提供的 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
类是由属于 PlatformEffect
的 TouchEffect
类在每个平台上实现的 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
类型,而无需接口映射。 当用户导航到包含附加了 TouchEffect
的 BoxView
实例的页面时,将调用依赖项解析方法,以便从依赖项注入容器解析平台 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>
方法。