将 xamarin 表单与 IServiceProvider 一起使用

Posted

技术标签:

【中文标题】将 xamarin 表单与 IServiceProvider 一起使用【英文标题】:using xamarin forms with IServiceProvider 【发布时间】:2018-12-12 11:39:11 【问题描述】:

我正在研究 xamarin 表单上的“依赖注入”,并发现了一些使用 ContainerBuilder 之类的概念。在网上找到的解决方案,例如this,讨论了如何进行 DI 设置并将它们注入到您的视图模型中。然而,就个人而言,我没有发现这个或整个视图模型和绑定的概念非常整洁,原因有几个。我宁愿创建可以被业务逻辑重用的服务,这似乎使代码更干净。我觉得实现IServiceProvider 会导致更简洁的实现。我正计划实施这样的服务提供者:

IServiceProvider Provider = new ServiceCollection()
                            .AddSingleton<OtherClass>()
                            .AddSingleton<MyClass>()
                            .BuildServiceProvider();

首先,我不确定为什么没有这样的 xamarin 示例。所以,我不确定朝着这个方向前进是否有什么问题。我研究了ServiceCollection 类。它来自的包Microsoft.Extensions.DependencyInjection 的名称中没有“aspnetcore”。但是,它的所有者是“aspnet”。我不完全确定 ServiceCollection 是否仅适用于 Web 应用程序,或者将其用于移动应用程序是否有意义。

只要我使用所有单例,使用 IServiceProviderServiceCollection 是否安全?有什么问题(在性能或内存方面)我失踪了吗?

更新

在 Nkosi 的 cmets 之后,我再次查看了链接并注意到了几件事:

    文档链接的日期与 Microsoft.Extensions.DependencyInjection 仍处于测试阶段的时间差不多 据我所知,文档中“使用依赖注入容器的几个优点”列表中的所有要点也适用于 DependencyInjectionAutofac 过程似乎围绕着我试图避免使用的 ViewModel。

更新 2

在导航功能的帮助下,我设法让 DI 直接进入页面的后面代码:

public static async Task<TPage> NavigateAsync<TPage>()
    where TPage : Page

    var scope = Provider.CreateScope();
    var scopeProvider = scope.ServiceProvider;
    var page = scopeProvider.GetService<TPage>();
    if (navigation != null) await navigation.PushAsync(page);
    return page;

【问题讨论】:

本例中的服务提供者是容器。 Microsoft.Extensions.DependencyInjection 是一个独立的模块。虽然它在 ASP.Net-Core 中用作开箱即用的 DI 容器。它可以在任何其他支持它的解决方案中单独使用。我相信应该可以在 Xamarin 中使用。但请注意,内置服务容器旨在满足框架的基本需求以及基于该框架构建的大多数消费者应用程序。 @Nkosi 你知道要为ContainerBuilder 导入的nuget 吗?我在网上找不到。我不想通过添加这个事实来延长我的问题 该示例似乎使用了 Autofac 的 ContainerBuilder。应该可以很容易地在 Nuget 上找到它。 好的.. 谢谢你。但是您所说的内置服务容器是什么意思? Autofac 似乎是第三方 是的。我已经用过几次了。我现在正在开发一个独立的库,它可以让你用一个流利的库来做到这一点。 【参考方案1】:

我知道这个问题已在 2 年前提出,但我可能有一个与您的要求相匹配的解决方案。

在过去的几个月里,我一直在使用 Xamarin 和 WPF 开发应用程序,并使用 Microsoft.Extensions.DependencyInjection 包将构造函数依赖注入添加到我的视图模型中,就像 ASP.NET 控制器一样。这意味着我可以有类似的东西:

public class MainViewModel : ViewModelBase

    private readonly INavigationService _navigationService;
    private readonly ILocalDatabase _database;

    public MainViewModel(INavigationService navigationService, ILocalDatabase database)
    
        _navigationService = navigationService;
        _database = database;
    

为了实现这种过程,我使用IServiceCollection 添加服务并使用IServiceProvider 检索注册的服务。

要记住的重要一点是,IServiceCollection 是您将注册依赖项的容器。然后在构建此容器时,您将获得一个IServiceProvider,它将允许您检索服务。

为此,我通常会创建一个Bootstrapper 类来配置服务并初始化应用程序的主页。

基本实现

此示例演示如何将依赖项注入 Xamarin 页面。对于任何其他类,该过程保持不变。 (ViewModel 或其他类)

在您的项目中创建一个名为 Bootstrapper 的简单类,并初始化 IServiceCollectionIServiceProvider 私有字段。

public class Bootstrapper

    private readonly Application _app;
    private IServiceCollection _services;
    private IServiceProvider _serviceProvider;

    public Bootstrapper(Application app)
    
        _app = app;
    

    public void Start()
    
        ConfigureServices();
    

    private void ConfigureServices()
    
        _services = new ServiceCollection();

        // TODO: add services here

        _serviceProvider = _services.BuildServiceProvider();
    

ConfigureServices() 方法中,我们只是创建了一个新的ServiceCollection,我们将在其中添加我们的服务。 (见https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollection?view=dotnet-plat-ext-3.1) 添加服务后,我们将构建服务提供者,以便我们检索之前注册的服务。

然后在您的App 类构造函数中,创建一个新的Bootstrapper 实例并调用start 方法来初始化应用程序。

public partial class App : Application

    public App()
    
        InitializeComponent();

        var bootstrapper = new Bootstrapper(this);
        bootstrapper.Start();
    
    ...

通过这段代码,您已经设置了服务容器,但我们仍然需要初始化应用程序的MainPage。返回引导程序的 Start() 方法并创建所需主页的新实例。

public class Bootstrapper

    ...

    public void Start()
    
        ConfigureServices();

        // Real magic happens here
        var mainPageInstance = ActivatorUtilities.CreateInstance<MainPage>(_serviceProvider);

        _app.MainPage = new NavigationPage(mainPageInstance);
    

这里我们使用ActivatorUtilities.CreateInstance&lt;TInstance&gt;() 方法来创建一个新的MainPage 实例。我们将_serviceProvider 作为参数,因为ActivatorUtilities.CreateInstance() 方法将负责创建您的实例并将所需的服务注入您的对象。

请注意,这是 ASP.NET Core 使用构造函数依赖注入实例化控制器的方法。

要对此进行测试,请创建一个简单的服务并尝试将其注入您的 MainPage 构造函数:

public interface IMySimpleService

    void WriteMessage(string message);


public class MySimpleService : IMySimpleService

    public void WriteMessage(string message)
    
        Debug.WriteLine(message);
    

然后在Bootstrapper类的ConfigureServices()方法中注册:

private void ConfigureServices()

    _services = new ServiceCollection();

    _services.AddSingleton<IMySimpleService, MySimpleService>();

    _serviceProvider = _services.BuildServiceProvider();

最后,转到您的MainPage.xaml.cs,注入IMySimpleService 并调用WriteMessage() 方法。

public partial class MainPage : ContentPage

    public MainPage(IMySimpleService mySimpleService)
    
        mySimpleService.WriteMessage("Hello world!");
    

到此,您已成功注册服务并将其注入您的页面。

构造函数注入的真正魔力是使用ActivatorUtilities.CreateInstance&lt;T&gt;() 方法通过传递服务提供者来实现的。该方法实际上会检查您的构造函数的参数并尝试通过尝试从您给他的IServiceProvider 中获取它们来解决依赖关系。

奖励:注册平台特定服务

这很好,对吧?借助ActivatorUtilities.CreateInstance&lt;T&gt;() 方法,您可以将服务注入任何类,但有时您还需要注册一些特定于平台的服务(androidios)。

用前面的方法是不可能注册特定平台的服务的,因为IServiceCollection是在Bootstrapper类中初始化的。不用担心,解决方法非常简单。

您只需要将IServiceCollection 初始化提取到特定于平台的代码。只需在您的 Android 项目的 MainActivity.cs 和您的 iOS 项目的 AppDelegate 上初始化服务集合,然后将其传递给您的 App 类,该类会将其转发给 Bootstrapper

MainActivity.cs (Android)

public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity

    protected override void OnCreate(Bundle savedInstanceState)
    
        ...

        var serviceCollection = new ServiceCollection();

        // TODO: add platform specific services here.

        var application = new App(serviceCollection);

        LoadApplication(application);
    
    ...

AppDelegate.cs (iOS)

public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate

    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    
        global::Xamarin.Forms.Forms.Init();

        var serviceCollection = new ServiceCollection();

        // TODO: add platform specific services here.

        var application = new App(serviceCollection);

        LoadApplication(application);

        return base.FinishedLaunching(app, options);
    

App.xaml.cs(通用)

public partial class App : Application

    public App(IServiceCollection services)
    
        InitializeComponent();

        var bootstrapper = new Bootstrapper(this, services);
        bootstrapper.Start();
    
    ...

Bootstrapper.cs(通用)

public class Bootstrapper

    private readonly Application _app;
    private readonly IServiceCollection _services;
    private IServiceProvider _serviceProvider;

    public Bootstrapper(Application app, IServiceCollection services)
    
        _app = app;
        _services = services;
    

    public void Start()
    
        ConfigureServices();

        var mainPageInstance = ActivatorUtilities.CreateInstance<MainPage>(_serviceProvider);

        _app.MainPage = new NavigationPage(mainPageInstance);
    

    private void ConfigureServices()
    
        // TODO: add services here.
        _serviceCollection.AddSingleton<IMySimpleService, MySimpleService>();

        _serviceProvider = _services.BuildServiceProvider();
    

仅此而已,您现在可以注册特定于平台的服务并将接口轻松地注入您的页面/视图模型/类中。

【讨论】:

这看起来棒极了!我真的希望我能成功。但是我在任何试图在构造函数中使用参数引用 ViewModel 的视图的 XAML 中都出现错误。它说:类型“AboutViewModel”不能用作对象元素,因为它不是公共的或没有定义公共无参数构造函数或类型转换器。 \Views\AboutPage.xaml 您遇到过这种情况吗?请问您是如何解决的? 很高兴你喜欢它。您可以尝试调整代码以使其与 ViewModel 一起使用,而不是使用页面。基本上,您可以使用与 ActivatorUtilities 相同的机制来创建 ViewModel,然后将其设置到您的页面,而不是使用依赖注入创建新页面实例。 这正是我正在做的事情。然后我将提取 Xamarin 项目之外的所有视图模型,删除对 Xamarin 库的所有依赖项,并尝试在 Blazor WASM 应用程序和 Xamarin 应用程序中同时重用相同的 ViewModel! 我使用的是 MVVM 第一种方法,我有一个 ViewFactory,我将在其中注册一对与 View (IDictionnary&lt;TViewModel, TView&gt;) 关联的 ViewModel,然后,我有一个 NavigationService允许您通过传递 ViewModel 类型或现有视图模型进行导航。在这个导航服务内部,我创建了一个与给定视图模型关联的新页面,然后创建一个新的(或使用现有的)ViewModel(使用ActivatorUtilities 创建,以便我可以注入依赖项)。看看这篇文章:blog.kloud.com.au/2018/05/15/…【参考方案2】:

这个实现使用 Splat 和一些帮助器/包装器类来方便地访问容器。

服务的注册方式有点冗长,但它可以涵盖我目前遇到的所有用例;并且生命周期也可以很容易地改变,例如切换到延迟创建服务。

只需使用 ServiceProvider 类从代码中任何位置的 IoC 容器检索任何实例。

注册您的服务

public partial class App : Application

    public App()
    
        InitializeComponent();
        SetupBootstrapper(Locator.CurrentMutable);
        MainPage = new MainPage();
    

    private void SetupBootstrapper(IMutableDependencyResolver resolver)
    
        resolver.RegisterConstant(new Service(), typeof(IService));
        resolver.RegisterLazySingleton(() => new LazyService(), typeof(ILazyService));
        resolver.RegisterLazySingleton(() => new LazyServiceWithDI(
            ServiceProvider.Get<IService>()), typeof(ILazyServiceWithDI));
        // and so on ....
    

ServiceProvider的使用

// get a new service instance with every call
var brandNewService = ServiceProvider.Get<IService>();

// get a deferred created singleton
var sameOldService = ServiceProvider.Get<ILazyService>();

// get a service which uses DI in its contructor
var another service = ServiceProvider.Get<ILazyServiceWithDI>();

ServiceProvider的实现

public static class ServiceProvider

    public static T Get<T>(string contract = null)
    
        T service = Locator.Current.GetService<T>(contract);
        if (service == null) throw new Exception($"IoC returned null for type 'typeof(T).Name'.");
        return service;
    

    public static IEnumerable<T> GetAll<T>(string contract = null)
    
        bool IsEmpty(IEnumerable<T> collection)
        
            return collection is null || !collection.Any();
        

        IEnumerable<T> services = Locator.Current.GetServices<T>(contract).ToList();
        if (IsEmpty(services)) throw new Exception($"IoC returned null or empty collection for type 'typeof(T).Name'.");

        return services;
    

这是我的 csproj 文件。没什么特别的,我添加的唯一 nuget 包是 Spat

共享项目 csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <DebugType>portable</DebugType>
    <DebugSymbols>true</DebugSymbols>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Splat" Version="9.3.11" />
    <PackageReference Include="Xamarin.Forms" Version="4.3.0.908675" />
    <PackageReference Include="Xamarin.Essentials" Version="1.3.1" />
  </ItemGroup>
</Project>

【讨论】:

这种方式也可以作为MVVM框架与ReactiveUI结合使用 @MoneyOrientedProgrammer,请将此要求添加到您的问题中。阅读您的问题后,我不清楚第 3 方套餐不是一种选择 这不是我的问题。我只将赏金授予仅使用Microsoft.Extensions.DependencyInjection 提供答案的人。

以上是关于将 xamarin 表单与 IServiceProvider 一起使用的主要内容,如果未能解决你的问题,请参考以下文章

如何将标签更改为与在 Xamarin 表单上显示通知相同的布局?

我需要将google驱动器与我的xamarin表单应用程序同步

将转换器与 Label 的可见性 Xamarin 表单一起使用时如何将默认值设置为 false

TeamCity 上的 Xamarin 表单:Xamarin.Forms 任务与目标不匹配

使用 Prism Xamarin 表单创建动态 TabbedPage

如何将 android 活动导航到 xamarin 表单