using xamarin forms with IServiceProvider
Asked Answered
N

2

17

I was looking into "Dependency Injection" on xamarin forms and found some concepts that use something like ContainerBuilder. The solutions found online such as this, talk about how you can have DI setup and inject them into your view models. However, personally, I didn't find this or the whole concept of view models and binding very tidy for several reasons. I would rather create services that can be reused by the business logic, which seems to make the code a lot cleaner. I felt that implementing an IServiceProvider would result in a much cleaner implementation. I was planning on implementing a service provider something like this:

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

Firstly, I am not sure why there are no xamarin examples of these. So, I am not sure if there is anything wrong with going towards this direction. I have looked into ServiceCollection class. The package it is from, Microsoft.Extensions.DependencyInjection, doesn't have "aspnetcore" in its name. It, however, has its owner as "aspnet". I am not entirely sure if ServiceCollection is only meant for web applications or it would make sense to use it for mobile apps.

Is it safe to use IServiceProvider with ServiceCollection as long as I use all singletons? is there any concern (in terms of performance or ram) I am missing?

Update

After the comments from Nkosi, I have taken another look at the link and noticed a couple of things:

  1. The documentation link is dated around the same time Microsoft.Extensions.DependencyInjection was still in beta
  2. All points in the list under "several advantages to using a dependency injection container" in the documentation also apply to DependencyInjection as far as I can see.
  3. Autofac process seems to revolve around ViewModels which I am trying to avoid using.

Update 2

I managed to get DI directly into the behind code of pages with the help of a navigation function something like this:

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;
}
Nert answered 4/7, 2018 at 6:2 Comment(9)
The service provider in this case is the container. Microsoft.Extensions.DependencyInjection is an independent module. While it is used as the out of the box DI container in ASP.Net-Core. It can be used on its own in any other solution that supports it. I believe is should be able to be used in Xamarin. Note however that the built-in services container is meant to serve the basic needs of the framework and most consumer applications built on it.Dougald
@Dougald do you know the nuget to import for ContainerBuilder? I couldn't find it online. I didn't want to lengthen my question by adding that factNert
That example appears to be using Autofac's ContainerBuilder. Should be able to find it on Nuget easily.Dougald
ok.. thanks for those. but what did you mean by build-in services container? Autofac seems third partyNert
Yes. I have used this a couple of times. I am now working on an independent library that lets you do this with a fluent library.Nert
I just made that repo public. github.com/neville-nazerane/DevOps-Manager. its the "Xamarin.FluentInjector" project. once it gets a little mature i'll move it to a separate repo and make a NuGetNert
could you add the answer as multiple points here and remove that?Nert
too long as a comment ...Condor
yeah i meant to post it as three separate commentsNert
D
4

This implementation uses Splat and some helper/wrapper classes to conveniently access the container.

The way how the services are registered is a bit verbose but it could cover all uses cases I have encountered so far; and the life cycle can be changed quite easily as well, e.g. switching to a lazy creation of a service.

Simply use the ServiceProvider class to retrieve any instances from the IoC container anywhere in your code.

Registering of your Services

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 ....
    }

Usage of 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>();

Implementation of 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;
    }
}

Here is my csproj file. Nothing special, the only nuget package I added was Spat

Shared Project 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>
Duplessis answered 14/2, 2020 at 19:54 Comment(3)
This approach can also be used in conjunction with ReactiveUI as a MVVM frameworkDuplessis
@MoneyOrientedProgrammer, please add this requirement to your question. It was not clear to me from reading your question that 3rd party packages are not an optionDuplessis
It is not my question. I only grant the bounty to one who provides an answer only with Microsoft.Extensions.DependencyInjection.Condor
A
1

I know the question has been asked 2 years ago, but I might have a solution that could match what you are asking for.

In the past few months I've been working on apps using Xamarin and WPF and I used the Microsoft.Extensions.DependencyInjection package to add constructor dependency injection to my view models, just like an ASP.NET Controller. Which means that I could have something like:

public class MainViewModel : ViewModelBase
{
    private readonly INavigationService _navigationService;
    private readonly ILocalDatabase _database;

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

To implement this kind of process I use the IServiceCollection to add the services and the IServiceProvider to retrieve the registered services.

What is important to remember, is that the IServiceCollection is the container where you will register your dependencies. Then when building this container, you will obtain a IServiceProvider that will allow you to retrieve a service.

To do so, I usually create a Bootstrapper class that will configure the services and initialize the main page of the application.

The basic implementation

This example show how to inject dependencies into a Xamarin page. The process remains the same for any other class. (ViewModels or other classes)

Create a simple class named Bootstrapper in your project and intitialize a IServiceCollection and IServiceProvider private fields.

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

Here in the ConfigureServices() method we just create a new ServiceCollection where we are going to add our services. (See https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollection?view=dotnet-plat-ext-3.1) Once our services have been added, we build the service provider that will allow us to retrieve the previously registered services.

Then in your App class constructor, create a new Bootstrapper instance and call the start method to initialize the application.

public partial class App : Application
{
    public App()
    {
        InitializeComponent();

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

With this piece of code, you have setup your service container, but we still need to initialize the MainPage of the application. Go back to the bootstrapper's Start() method and create a new instance of the wanted main page.

public class Bootstrapper
{
    ...

    public void Start()
    {
        ConfigureServices();

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

        _app.MainPage = new NavigationPage(mainPageInstance);
    }
}

Here we use the ActivatorUtilities.CreateInstance<TInstance>() method to create a new MainPage instance. We give the _serviceProvider as parameter, because the ActivatorUtilities.CreateInstance() method will take care of creating your instance and inject the required services into your object.

Note that this is what ASP.NET Core using to instanciate the controllers with contructor dependency injection.

To test this, create a simple service and try to inject it into your MainPage contructor:

public interface IMySimpleService
{
    void WriteMessage(string message);
}

public class MySimpleService : IMySimpleService
{
    public void WriteMessage(string message)
    {
        Debug.WriteLine(message);
    }
}

Then register it inside the ConfigureServices() method of the Bootstrapper class:

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

    _services.AddSingleton<IMySimpleService, MySimpleService>();

    _serviceProvider = _services.BuildServiceProvider();
}

And finally, go to your MainPage.xaml.cs, inject the IMySimpleService and call the WriteMessage() method.

public partial class MainPage : ContentPage
{
    public MainPage(IMySimpleService mySimpleService)
    {
        mySimpleService.WriteMessage("Hello world!");
    }
}

There you go, you have successfully registered a service and injected it into your page.

The real magic with constructor injection really occurs using the ActivatorUtilities.CreateInstance<T>() method by passing a service provider. The method will actually check the parameters of your constructor and try to resolve the dependencies by trying to get them from the IServiceProvider you gave him.

Bonus : Register platform specific services

Well this is great right? You are able to inject services into any classes thanks to the ActivatorUtilities.CreateInstance<T>() method, but sometimes you will also need to register some platform specific services (Android or iOS).

With the previous method is not possible to register platform-specific services, because the IServiceCollection is initialized in the Bootstrapper class. No worries, the workaround is really simple.

You just need to extract the IServiceCollection initialization to the platform-specific code. Simply initialize the service collection on the MainActivity.cs of your Android project and in the AppDelegate of your iOS project and pass it to your App class that will forward it to the 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 (Common)

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

And that's all, you are now able to register platform-specific services and inject the interface into your pages / view models / classes easily.

Astrodynamics answered 30/7, 2020 at 11:54 Comment(4)
This looks fantastic! I really hope I can make it work. But I'm getting an error in the XAML of any view that tries to reference a ViewModel with parameters in the constructor. It says: Type 'AboutViewModel' is not usable as an object element because it is not public or does not define a public parameterless constructor or a type converter. \Views\AboutPage.xaml Did you experience this and how did you fix it please?Wardrobe
I'm glad you like it. Instead of working with pages, you could try to adapt the code to get it work with ViewModels instead. Basically, instead of creating a new page instance with the dependency injection, you could use the same mecanism with the ActivatorUtilities to create a ViewModel and then set it to your page.Astrodynamics
That's exactly what I'm working on. Then I'm going to extract all the view models outside of the Xamarin project, remove any dependencies on Xamarin libraries, and try to reuse the same ViewModels in a Blazor WASM application and Xamarin application at the same time!Wardrobe
I'm using a MVVM first approach, where I have a ViewFactory where I will register a pair of ViewModel associated with a View (IDictionnary<TViewModel, TView>) and then, I have a NavigationService that allows you to navigate by passing a ViewModel type or an existing view model. Inside of this navigation service, I create a new page associated with the given view model, and then create a new (or use an existing one) ViewModel (creation with ActivatorUtilities so I can inject dependencies). Check out this article :blog.kloud.com.au/2018/05/15/…Astrodynamics

© 2022 - 2024 — McMap. All rights reserved.