使用Xamarin Forms和IServiceProvider

18

我在研究Xamarin Forms上的"依赖注入",发现一些概念使用了ContainerBuilder之类的东西。像这里这样在线找到的解决方案讲述了如何设置DI并将它们注入到您的视图模型中。然而,就个人而言,我认为这种方法或者整个视图模型和绑定的概念并不是很整洁,有好几个原因。我宁愿创建可以被业务逻辑重复使用的服务,这似乎可以使代码更加清晰。我认为实现IServiceProvider会得到更干净的实现方式。我计划实现一个像这样的服务提供程序:

IServiceProvider Provider = new ServiceCollection()
                            .AddSingleton<OtherClass>()
                            .AddSingleton<MyClass>()
                            .BuildServiceProvider();
首先,我不确定为什么没有针对这些的 xamarin 示例。因此,我不确定朝这个方向前进是否有问题。我已经研究了 ServiceCollection 类。它来自的包是 Microsoft.Extensions.DependencyInjection,其名称中没有 "aspnetcore"。然而,它的所有者是 "aspnet"。我不完全确定 ServiceCollection 是否只适用于 Web 应用程序,还是在移动应用程序中使用它是有意义的。
如果我使用所有单例,那么在 ServiceCollection 中使用 IServiceProvider 是否安全?我是否遗漏了任何关注点(性能或 RAM 方面)?
更新
在 Nkosi 的评论后,我又看了一眼链接,注意到了几件事情:
1. 文档链接的日期大约是 Microsoft.Extensions.DependencyInjection 还处于 beta 阶段时。 2. 在文档中“使用依赖注入容器的多个优点”列表下的所有点也适用于 DependencyInjection,据我所见。 3. Autofac 进程似乎围绕着我试图避免使用的 ViewModels 展开。
更新 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
@Nkosi,你知道导入“ContainerBuilder”的NuGet包是什么吗?我在网上找不到它。我不想通过添加这个事实来延长我的问题。 - Neville Nazerane
那个例子似乎在使用Autofac的ContainerBuilder。应该可以很容易地在Nuget上找到它。 - Nkosi
好的,谢谢这些信息。但是你所说的“内置服务容器”是什么意思?Autofac似乎是第三方的。 - Neville Nazerane
too long as a comment ... - Second Person Shooter
显示剩余4条评论
2个回答

4

这个实现使用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文件。没什么特别的,我只添加了一个名为Spat的NuGet包。
共享项目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>

这种方法也可以与ReactiveUI作为MVVM框架一起使用。 - Mouse On Mars
@MoneyOrientedProgrammer,请将此要求添加到您的问题中。从阅读您的问题中,我并不清楚第三方软件包不是一个选项。 - Mouse On Mars
1
这不是我的问题。我只会授予赏金给那些仅使用 Microsoft.Extensions.DependencyInjection 提供答案的人。 - Second Person Shooter

1

我知道这个问题是两年前提出的,但是我可能有一个解决方案可以匹配你所要求的。

在过去的几个月里,我一直在使用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://learn.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<TInstance>()方法创建一个新的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<T>() 方法,通过传递一个服务提供程序,构造函数注入的真正魔力实际上发生在这里。该方法将检查您的构造函数参数,并尝试通过从您给定的 IServiceProvider 中获取它们来解析依赖项。

奖励:注册特定于平台的服务

很棒吧?由于 ActivatorUtilities.CreateInstance<T>() 方法,您可以将服务注入到任何类中,但有时您还需要注册一些特定于平台的服务(Android 或 iOS)。

使用先前的方法无法注册特定于平台的服务,因为 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 (Common)

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();
    }
}

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


2
这看起来很棒!我真的希望我能让它工作。但是,当任何视图尝试引用具有构造函数参数的ViewModel时,我在XAML中遇到了错误。它说:类型“AboutViewModel”不能用作对象元素,因为它不是公共的,也没有定义公共的无参数构造函数或类型转换器。\Views\AboutPage.xaml您是否遇到过这种情况,如何修复它? - benjamin
我很高兴你喜欢它。与其使用页面,你可以尝试调整代码以使用ViewModels。基本上,不要使用依赖注入创建新的页面实例,而是使用ActivatorUtilities机制创建一个ViewModel,然后将其设置为你的页面。 - Eastrall
这正是我正在从事的工作。然后,我将提取所有视图模型,使其脱离 Xamarin 项目,并尝试在 Blazor WASM 应用程序和 Xamarin 应用程序中同时重用相同的 ViewModel,杜绝对 Xamarin 库的任何依赖关系! - benjamin
我使用MVVM优先方法,其中我有一个ViewFactory,在其中我将注册一对与视图相关联的ViewModel(IDictionnary <TViewModel,TView>),然后,我有一个NavigationService,它允许您通过传递ViewModel类型或现有视图模型来导航。在这个导航服务内部,我创建一个与给定视图模型相关联的新页面,然后创建一个新的(或使用现有的)ViewModel(使用ActivatorUtilities进行创建,以便我可以注入依赖项)。请查看此文章:https://blog.kloud.com.au/2018/05/15/data-first-approach-in-xamarin-forms/ - Eastrall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接