Injection de dépendances

L’interface utilisateur de l’application multiplateforme .NET (.NET MAUI) fournit une prise en charge intégrée de l’utilisation de l’injection de dépendances. L’injection de dépendances est une version spécialisée du modèle d’inversion de contrôle (IoC), où le problème à inverser est le processus d’obtention de la dépendance requise. Avec l’injection de dépendances, une autre classe est chargée d’injecter les dépendances dans un objet au moment de l’exécution.

En règle générale, un constructeur de classe est appelé au moment de l’instanciation d’un objet, et toutes les valeurs dont l’objet a besoin sont passées en tant qu’arguments au constructeur. Il s’agit d’un exemple d’injection de dépendances appelée injection de constructeurs. Les dépendances dont l’objet a besoin sont injectées dans le constructeur.

Remarque

Il existe également d’autres types d’injection de dépendances, tels que l’injection de setter de propriétés et l’injection d’appel de méthode, mais ils sont moins couramment utilisés.

En spécifiant des dépendances en tant que types d’interface, l’injection de dépendances permet de découpler les types concrets du code qui dépend de ces types. Elle utilise généralement un conteneur qui détient une liste d’inscriptions et de mappages entre les interfaces et les types abstraits ainsi que les types concrets qui implémentent ou étendent ces types.

Conteneurs d’injection de dépendances

Si une classe n’instancie pas directement les objets dont elle a besoin, une autre classe doit assumer cette responsabilité. Prenons l’exemple suivant, qui montre une classe de modèle d’affichage qui nécessite des arguments de constructeur :

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel(ILoggingService loggingService, ISettingsService settingsService)
    {
        _loggingService = loggingService;
        _settingsService = settingsService;
    }
}

Dans cet exemple, le MainPageViewModel constructeur nécessite deux instances d’objet d’interface en tant qu’arguments injectés par une autre classe. La seule dépendance dans la classe MainPageViewModel concerne les types d’interface. La classe MainPageViewModel ne sait donc pas quelle est la classe responsable de l’instanciation des objets d’interface.

De même, considérez l’exemple suivant qui montre une classe de page qui nécessite un argument de constructeur :

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

Dans cet exemple, le MainPage constructeur nécessite un type concret en tant qu’argument injecté par une autre classe. La seule dépendance dans la MainPage classe est sur le MainPageViewModel type. Par conséquent, la MainPage classe n’a aucune connaissance de la classe responsable de l’instanciation du type concret.

Dans les deux cas, la classe chargée d’instancier les dépendances et de les insérer dans la classe dépendante est appelée conteneur d’injection de dépendances.

Les conteneurs d’injection de dépendances réduisent le couplage entre les objets en offrant une fonctionnalité permettant d’instancier les instances de classe, et de gérer leur durée de vie en fonction de la configuration du conteneur. Pendant la création de l’objet, le conteneur injecte toutes les dépendances dont l’objet a besoin. Si ces dépendances n’ont pas été créées, le conteneur crée et résout d’abord leurs dépendances.

L’utilisation d’un conteneur d’injection de dépendances présente plusieurs avantages :

  • Avec un conteneur, une classe n’a pas besoin de localiser ses dépendances et de gérer ses durées de vie.
  • Un conteneur permet le mappage des dépendances implémentées sans affecter la classe.
  • Un conteneur facilite les tests en permettant de simuler les dépendances.
  • Un conteneur augmente la maintenabilité en facilitant l’ajout de nouvelles classes à l’application.

Dans le contexte d’une application .NET MAUI qui utilise le modèle Model-View-ViewModel (MVVM), un conteneur d’injection de dépendances est généralement utilisé pour l’inscription et la résolution des vues, l’inscription et la résolution des modèles d’affichage, ainsi que pour l’inscription des services et leur injection dans les modèles d’affichage. Pour plus d’informations sur le modèle MVVM, consultez Model-View-ViewModel (MVVM).

Il existe de nombreux conteneurs d’injection de dépendances disponibles pour .NET. .NET MAUI prend en charge l’utilisation Microsoft.Extensions.DependencyInjection intégrée pour gérer l’instanciation des vues, des modèles d’affichage et des classes de service dans une application. Microsoft.Extensions.DependencyInjection facilite la création d’applications faiblement couplées et fournit toutes les fonctionnalités couramment trouvées dans les conteneurs d’injection de dépendances, notamment des méthodes pour inscrire les mappages de types et les instances d’objets, résoudre les objets, gérer les durées de vie des objets et injecter des objets dépendants dans les constructeurs des objets qu’il résout. Pour plus d’informations sur Microsoft.Extensions.DependencyInjection, consultez Injection de dépendances dans .NET.

Au moment de l’exécution, le conteneur doit savoir quelle implémentation des dépendances sont demandées pour les instancier pour les objets demandés. Dans l’exemple ci-dessus, les ILoggingService interfaces doivent ISettingsService être résolues avant que l’objet MainPageViewModel puisse être instancié. Cela implique que le conteneur effectue les actions suivantes :

  • Choix du mode d’instanciation d’un objet qui implémente l’interface. Cela s’appelle l’inscription. Pour plus d’informations, consultez Inscription.
  • Instanciation de l’objet qui implémente l’interface nécessaire et de l’objet MainPageViewModel. Cela s’appelle la résolution. Pour plus d’informations, consultez Résolution.

Finalement, une application se termine à l’aide de l’objet MainPageViewModel et devient disponible pour le garbage collection. À ce stade, le garbage collector doit supprimer toutes les implémentations d’interface de courte durée si d’autres classes ne partagent pas les mêmes instances.

Inscription

Avant que les dépendances puissent être injectées dans un objet, les types des dépendances doivent d’abord être inscrits auprès du conteneur. L’inscription d’un type implique généralement le passage du conteneur à un type concret, ou d’une interface et d’un type concret qui implémente l’interface.

Il existe deux approches principales pour inscrire des types et des objets auprès du conteneur :

  • Inscrire un type ou un mappage auprès du conteneur. Cela s’appelle l’inscription temporaire. Le cas échéant, le conteneur crée une instance du type spécifié.
  • Inscrire un objet existant dans le conteneur en tant que singleton. Le cas échéant, le conteneur retourne une référence à l’objet existant.

Attention

Les conteneurs d’injection de dépendances ne conviennent pas toujours à une application .NET MAUI. L’injection de dépendances introduit une complexité et des exigences supplémentaires qui peuvent ne pas être appropriées ou utiles pour les applications plus petites. Si une classe n’a pas de dépendances ou n’est pas une dépendance pour d’autres types, il peut ne pas être judicieux de la placer dans le conteneur. En outre, si une classe a un ensemble unique de dépendances qui font partie intégrante du type et ne changera jamais, il peut ne pas être judicieux de les placer dans le conteneur.

L’inscription de types nécessitant une injection de dépendances doit être effectuée dans une méthode unique dans votre application. Cette méthode doit être appelée au début du cycle de vie de l’application pour s’assurer qu’elle prend en compte les dépendances entre ses classes. Les applications doivent généralement effectuer cette opération dans la CreateMauiApp méthode de la MauiProgram classe. La MauiProgram classe appelle la CreateMauiApp méthode pour créer un MauiAppBuilder objet. L’objet MauiAppBuilder a une Services propriété de type IServiceCollection, qui fournit un emplacement pour inscrire vos types, tels que les vues, les modèles d’affichage et les services pour l’injection de dépendances :

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        builder.Services.AddTransient<ILoggingService, loggingService>();
        builder.Services.AddTransient<ISettingsService, SettingsService>();
        builder.Services.AddSingleton<MainPageViewModel>();
        builder.Services.AddSingleton<MainPage>();

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

Les types inscrits auprès de la Services propriété sont fournis au conteneur d’injection de dépendances lorsqu’ils MauiAppBuilder.Build() sont appelés.

Lors de l’inscription de dépendances, vous devez inscrire toutes les dépendances, y compris tous les types qui nécessitent les dépendances. Par conséquent, si vous avez un modèle d’affichage qui prend une dépendance en tant que paramètre de constructeur, vous devez inscrire le modèle d’affichage avec toutes ses dépendances. De même, si vous avez une vue qui prend une dépendance de modèle d’affichage en tant que paramètre de constructeur, vous devez inscrire la vue et le modèle d’affichage ainsi que toutes ses dépendances.

Conseil

Un conteneur d’injection de dépendances est idéal pour créer des instances de modèle d’affichage. Si un modèle d’affichage a des dépendances, il gère la création et l’injection de tous les services requis. Assurez-vous simplement d’inscrire vos modèles d’affichage et toutes les dépendances qu’ils peuvent avoir dans la méthode dans la CreateMauiAppMauiProgram classe.

Durée de vie des dépendances

Selon les besoins de votre application, vous devrez peut-être inscrire des dépendances avec différentes durées de vie. Le tableau suivant répertorie les principales méthodes que vous pouvez utiliser pour inscrire des dépendances et leurs durées de vie d’inscription :

Méthode Description
AddSingleton<T> Crée une instance unique de l’objet qui restera pendant la durée de vie de l’application.
AddTransient<T> Crée une instance de l’objet lorsqu’il est demandé pendant la résolution. Les objets temporaires n’ont pas de durée de vie prédéfinie, mais ils suivent généralement la durée de vie de leur hôte.
AddScoped<T> Crée une instance de l’objet qui partage la durée de vie de son hôte. Lorsque l’hôte sort de l’étendue, sa dépendance est donc effectuée. Par conséquent, la résolution de la même dépendance plusieurs fois dans la même étendue génère la même instance, tandis que la résolution de la même dépendance dans différentes étendues génère des instances différentes.

Remarque

Si un objet n’hérite pas d’une interface, telle qu’une vue ou un modèle d’affichage, seul son type concret doit être fourni à la méthode, ou AddScoped<T> à la AddSingleton<T>AddTransient<T>méthode.

La MainPageViewModel classe est utilisée près de la racine de l’application et doit toujours être disponible. Par conséquent, l’inscription est AddSingleton<T> bénéfique. D’autres modèles d’affichage peuvent être redirigés vers ou utilisés ultérieurement dans une application. Si vous avez un type qui n’est peut-être pas toujours utilisé, ou s’il s’agit d’une mémoire ou d’une quantité intensive de calcul ou nécessite des données juste-à-temps, il peut s’agir d’un meilleur candidat à AddTransient<T> l’inscription.

Une autre façon courante d’inscrire des dépendances consiste à utiliser les méthodes ou AddTransient<TService, TImplementation>AddScoped<TService, TImplementation> les AddSingleton<TService, TImplementation>méthodes. Ces méthodes prennent deux types : la définition d’interface et l’implémentation concrète. Ce type d’inscription convient mieux dans les cas où vous implémentez des services basés sur des interfaces.

Une fois tous les types inscrits, MauiAppBuilder.Build() doit être appelé pour créer l’objet MauiApp et remplir le conteneur d’injection de dépendances avec tous les types inscrits.

Important

Une fois MauiAppBuilder.Build() appelés, les types inscrits auprès du conteneur d’injection de dépendances sont immuables et ne peuvent plus être mis à jour ou modifiés.

Inscrire des dépendances avec une méthode d’extension

La MauiApp.CreateBuilder méthode crée un MauiAppBuilder objet qui peut être utilisé pour inscrire des dépendances. Si votre application doit inscrire de nombreuses dépendances, vous pouvez créer des méthodes d’extension pour vous aider à fournir un flux de travail d’inscription organisé et gérable :

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
        => MauiApp.CreateBuilder()
            .UseMauiApp<App>()
            .RegisterServices()
            .RegisterViewModels()
            .RegisterViews()
            .Build();

    public static MauiAppBuilder RegisterServices(this MauiAppBuilder mauiAppBuilder)
    {
        mauiAppBuilder.Services.AddTransient<ILoggingService, loggingService>();
        mauiAppBuilder.Services.AddTransient<ISettingsService, SettingsService>();

        // More services registered here.

        return mauiAppBuilder;        
    }

    public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder)
    {
        mauiAppBuilder.Services.AddSingleton<MainPageViewModel>();

        // More view-models registered here.

        return mauiAppBuilder;        
    }

    public static MauiAppBuilder RegisterViews(this MauiAppBuilder mauiAppBuilder)
    {
        mauiAppBuilder.Services.AddSingleton<MainPage>();

        // More views registered here.

        return mauiAppBuilder;        
    }
}

Dans cet exemple, les trois méthodes d’extension d’inscription utilisent l’instance MauiAppBuilder pour accéder à la Services propriété pour inscrire des dépendances.

Résolution

Une fois qu’un type est inscrit, il peut être résolu ou injecté en tant que dépendance. Quand un type est résolu et que le conteneur doit créer une instance, il injecte les dépendances éventuelles dans l’instance.

En règle générale, lorsqu’un type est résolu, l’un des trois scénarios se produit :

  1. Si le type n’a pas été inscrit, le conteneur lève une exception.
  2. Si le type a été inscrit en tant que singleton, le conteneur retourne l’instance de singleton. Si le type est appelé pour la première fois, le conteneur le crée le cas échéant, et gère une référence à celui-ci.
  3. Si le type a été inscrit en tant que type temporaire, le conteneur retourne une nouvelle instance et ne conserve pas de référence à celle-ci.

.NET MAUI prend en charge la résolution automatique et explicite des dépendances. La résolution automatique des dépendances utilise l’injection de constructeur sans demander explicitement la dépendance du conteneur. La résolution de dépendance explicite se produit à la demande en demandant explicitement une dépendance du conteneur.

Résolution automatique des dépendances

La résolution automatique des dépendances se produit dans les applications qui utilisent .NET MAUI Shell, à condition que vous ayez inscrit le type de dépendance et le type qui utilise la dépendance avec le conteneur d’injection de dépendances.

Pendant la navigation basée sur Shell, .NET MAUI recherche les inscriptions de pages et, le cas échéant, il crée cette page et injecte les dépendances dans son constructeur :

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

Dans cet exemple, le MainPage constructeur reçoit une MainPageViewModel instance injectée. À son tour, l’instance MainPageViewModel a ILoggingService et ISettingsService les instances injectées :

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel(ILoggingService loggingService, ISettingsService settingsService)
    {
        _loggingService = loggingService;
        _settingsService = settingsService;
    }
}

En outre, dans une application shell, .NET MAUI injectera des dépendances dans des pages de détails inscrites avec la Routing.RegisterRoute méthode.

Résolution de dépendance explicite

Une application shell ne peut pas utiliser l’injection de constructeur lorsqu’un type expose uniquement un constructeur sans paramètre. Sinon, si votre application n’utilise pas Shell, vous devez utiliser la résolution de dépendance explicite.

Le conteneur d’injection de dépendances est explicitement accessible à partir d’une Element propriété via sa Handler.MauiContext.Service propriété, qui est de type IServiceProvider:

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        HandlerChanged += OnHandlerChanged;
    }

    void OnHandlerChanged(object sender, EventArgs e)
    {
        BindingContext = Handler.MauiContext.Services.GetService<MainPageViewModel>();
    }
}

Cette approche peut être utile si vous devez résoudre une dépendance à partir d’un Element, ou en dehors du constructeur d’un Element. Dans cet exemple, l’accès au conteneur d’injection de dépendances dans le HandlerChanged gestionnaire d’événements garantit qu’un gestionnaire a été défini pour la page et, par conséquent, que la Handler propriété ne sera nullpas .

Avertissement

La Handler propriété de votre Element peut être null, donc sachez que vous devrez peut-être tenir compte de cette situation. Pour plus d’informations, consultez cycle de vie des gestionnaires.

Dans un modèle d’affichage, le conteneur d’injection de dépendances peut être explicitement accessible via la Handler.MauiContext.Service propriété de Application.Current.MainPage:

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel()
    {
        _loggingService = Application.Current.MainPage.Handler.MauiContext.Services.GetService<ILoggingService>();
        _settingsService = Application.Current.MainPage.Handler.MauiContext.Services.GetService<ISettingsService>();
    }
}

L’inconvénient de cette approche est que le modèle d’affichage a désormais une dépendance sur le Application type. Toutefois, cet inconvénient peut être éliminé en passant un IServiceProvider argument au constructeur de modèle d’affichage. Il IServiceProvider est résolu par le biais de la résolution automatique de dépendances sans avoir à l’inscrire auprès du conteneur d’injection de dépendances. Avec cette approche, un type et sa IServiceProvider dépendance peuvent être résolus automatiquement, à condition que le type soit inscrit auprès du conteneur d’injection de dépendances. Vous IServiceProvider pouvez ensuite l’utiliser pour la résolution de dépendance explicite :

public class MainPageViewModel
{
    readonly ILoggingService _loggingService;
    readonly ISettingsService _settingsService;

    public MainPageViewModel(IServiceProvider serviceProvider)
    {
        _loggingService = serviceProvider.GetService<ILoggingService>();
        _settingsService = serviceProvider.GetService<ISettingsService>();
    }
}

En outre, une IServiceProvider instance est accessible via les propriétés natives suivantes :

  • Android- MauiApplication.Current.Services
  • iOS et Mac Catalyst - MauiUIApplicationDelegate.Current.Services
  • Windows - MauiWinUIApplication.Current.Services

Limitations avec les ressources XAML

Un scénario courant consiste à inscrire une page auprès du conteneur d’injection de dépendances et à utiliser la résolution de dépendance automatique pour l’injecter dans le App constructeur et la définir comme valeur de la MainPage propriété :

public App(MyFirstAppPage page)
{
    InitializeComponent();
    MainPage = page;
}

Toutefois, dans ce scénario, si MyFirstAppPage vous tentez d’accéder à un StaticResource élément qui a été déclaré en XAML dans le App dictionnaire de ressources, un XamlParseException message est levée avec un message similaire à Position {row}:{column}. StaticResource not found for key {key}. Cela se produit parce que la page résolue par injection de constructeur a été créée avant l’initialisation des ressources XAML au niveau de l’application.

Une solution de contournement pour ce problème consiste à injecter une IServiceProvider dans votre App classe, puis à l’utiliser pour résoudre la page à l’intérieur de la App classe :

public App(IServiceProvider serviceProvider)
{
    InitializeComponent();
    MainPage = serviceProvider.GetService<MyFirstAppPage>();
}

Cette approche force la création et l’initialisation de l’arborescence d’objets XAML avant la résolution de la page.