Category Archives: IoC

Dependency injection using Shell in MAUI

The MauiAppBuilder (as seen in the MauiProgram class) i.e.

var builder = MauiApp.CreateBuilder();

exposes the property Services which is a collection of ServiceDescriptor objects. This is where we register our “services”. We can use AddSingleton or AddTransient or AddScoped.

So for example

builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainPageViewModel>();
  • AddSingleton
    As the name suggests, a single instance of a type is created across the lifetime of the application.
  • AddTransient
    A new instance of a type is created for each request, the equivalent of creating a new instance for every type that has a dependency, hence each gets a unique instance.
  • AddScoped
    A new instance of a type is created for a request and all dependencies within that request gets the same instance. For different requests a new instance is created, hence unique across requests.

Whilst the IoC container with MAUI Shell, isn’t as comprehensive in features as some containers, such as Autofac, it’s good enough for many. It does include constructor dependency injection so we will probably fulfil most use cases.

Okay, let’s assume with have a MainPage and on that we have a button which displays an AboutPage. As per the code above suggests. We can create a view model for each page, so we have a MainPageViewModel and AboutPageViewModel, then within the actual .xaml.cs of those pages, in the constructor we have code such as

public MainPage(MainPageViewModel vm)
{
   InitializeComponent();
   BindingContext = vm;
}


public AboutPage(AboutPageViewModel vm)
{
   InitializeComponent();
   BindingContext = vm;
}

Now all we need to do is register the pages and view models within the MauiProgram with the builder, so we have

builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainPageViewModel>();
builder.Services.AddTransient<AboutPage>();
builder.Services.AddTransient<AboutPageViewModel>();

When Maui starts up and loads MainPage.xaml it will inject the MainPageViewModel and also when we navigate to our AboutPage using something like the code below, again the view model is injected

[RelayCommand]
private async Task About() => await AppShell.Current.GoToAsync(nameof(AboutPage));

Note: The example uses the MVVM Community Toolkit, hence the RelayCommandAttribute.

We may need to resolve dependencies ourselves (in other words we’re unable to use the magic of something like constructor injection). Rather strangely I would have expected MAUI to already have code within for this, but it seems that current the solution is to create (or use an existing) ServiceProvider class. For example, the one shown below is taken from David Ortinau’s sample.

public static class ServiceProvider
{
   public static TService GetService<TService>()
      => Current.GetService<TService>();

   public static IServiceProvider Current
      =>
#if WINDOWS10_0_17763_0_OR_GREATER
	MauiWinUIApplication.Current.Services;
#elif ANDROID
        MauiApplication.Current.Services;
#elif IOS || MACCATALYST
	MauiUIApplicationDelegate.Current.Services;
#else
	null;
#endif
}

Now now we can use the static class ServiceProvider to get the Services, for example let’s remove the dependency injection in the AboutPage constructor and instead use the ServiceProvider like this

BindingContext = ServiceProvider.GetService<AboutPageViewModel>();

Ofcourse this is not so easily testable, but the option is there if required.

inversify

I wanted to use an IoC pattern/library in a project I was working on (a node server application where I would slot in the specific implementations of the data and services at runtime).

I cam across inversify, there may be other IoC’s for TypeScript, but this is the one I looked into and worked a treat for me.

yarn add inversify reflect-metadata

You will need to enable experimentalDecorators and emitDecoratorMetadata within the tsconfig.json file.

"experimentalDecorators": true,        
"emitDecoratorMetadata": true, 

as per the invertsify documentation, we should also have the following tsconfig.json properties set

"target": "es5",
"lib": ["es6"],
"types": ["reflect-metadata"],
"module": "commonjs",
"moduleResolution": "node",

As is usual in C# (at least), the best way to work with IoC is to define an interface for our abstraction and then an implementation. However for inverisfy we also define a Symbol for the interfaces. So let’s assume we’re going to have a Service interface along with RestService and WebsocketService.

First create your Service.ts file and here’s a simple example

interface Service {
  call(data: string): void;
};

export default Service;

Now let’s create a couple of implementations, RestService.ts

import "reflect-metadata";
import Service from "./Service";
import { injectable } from "inversify";

@injectable()
class RestService implements Service {
  call(data: string): void {
    console.log("RestService called");
  }
}

export default RestService;

As you can see, we decorate the class with @injectable as this class may be injected into another class.

Here’s our WebsocketService.ts

import "reflect-metadata";
import Service from "./Service";
import { injectable } from "inversify";

@injectable()
class WebsocketService implements Service {
  call(data: string): void {
    console.log("WebsocketService called");
  }
}

export default WebsocketService;

Within the types.ts (below) we create the symbols (keys/identifiers) for use with our types.

const Types = {
  Service: Symbol.for("Service"),
  Application: Symbol.for("Application")
};

export default Types;

Here’s an Application interface Application.ts

interface Application {   
  run(): void;
}

export default Application;

MyApplication.ts

import "reflect-metadata";
import { inject, injectable } from "inversify";
import Application from "./Application";
import Types from "./types";
import Service from "./Service";

@injectable()
class MyApplication implements Application {
  private readonly service: Service;

  constructor(@inject(Types.Service) service) {
    this.service = service;
  }

  run(): void {        
    this.service.call("");
  }
}

export default MyApplication; 

In the above you can see we’re injecting (via the @inject decorator on the constructor) a type of symbol type Service. Hence this is where the server implementation will be injected into.

To supply inversify with the implementations for various types, we create the file inversify.config.ts. In the example file below we bind the symbol type/identifier with an implementation.

import { Container } from "inversify";
import Service from "./Service";
import RestService from "./RestService";
import Types from "./types";
import WebsocketService from "./WebsocketService";
import Application from "./Application";
import MyApplication from "./MyApplication";

const container = new Container();

//container.bind<Service>(Types.Service).to(RestService);
container.bind<Service>(Types.Service).to(WebsocketService);
container.bind<Application>(Types.Application).to(MyApplication);

export default container;

Finally, here’s an example implementation of the index.ts

import container from "./inversify.config";
import Application from "./Application";
import Types from "./types";

const application = container.get<Application>(Types.Application);
application.run();

There’s more to inversify than just the code listed above, such as you can have multiple Service implementations and use named bindings to receive the one you want and more.