Injection de dépendances
Le .NET Multi-Platform App UI (.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 d’inversion 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 propriété setter et la méthode d’injection d’appel, 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épendance
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 constructeur MainPageViewModel
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 constructeur MainPage
nécessite un type concret en tant qu’argument injecté par une autre classe. La seule dépendance dans la classe MainPage
se trouve sur le type MainPageViewModel
. Par conséquent, la classe MainPage
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-vue-vue modèle (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 Modèle-vue-vue modèle (MVVM).
Il existe de nombreux conteneurs d’injection de dépendances disponibles pour .NET. .NET MAUI prend en charge l’utilisation de Microsoft.Extensions.DependencyInjection 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 interfaces ILoggingService
et ISettingsService
doivent ê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 GC. À ce stade, le récupérateur de mémoire 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 méthode CreateMauiApp
dans la classe MauiProgram
. La classe MauiProgram
appelle la méthode CreateMauiApp
pour créer un objet MauiAppBuilder. L’objet MauiAppBuilder a une propriété Services 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 propriété Services sont fournis au conteneur d’injection de dépendances quand MauiAppBuilder.Build() est appelé.
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 de vue comme paramètre de construction, vous devez enregistrer la vue et le modèle de vue avec 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 CreateMauiApp
dans la classe MauiProgram
.
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 AddSingleton<T>
, AddTransient<T>
ou AddScoped<T>
.
La classe MainPageViewModel
est utilisée près de la racine de l’application et doit toujours être disponible, de sorte que l’inscrire auprès de AddSingleton<T>
est 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(e) meilleur(e) candidat(e) pour l’inscription AddTransient<T>
.
Une autre façon courante d’inscrire des dépendances consiste à utiliser les méthodes AddSingleton<TService, TImplementation>
, AddTransient<TService, TImplementation>
ou AddScoped<TService, TImplementation>
. 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() a été appelée, 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 méthode MauiApp.CreateBuilder crée un objet MauiAppBuilder 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 propriété Services 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 :
- Si le type n’a pas été inscrit, le conteneur lève une exception.
- 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.
- 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 automatique et résolution de dépendance explicite. 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 requête 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 constructeur MainPage
reçoit une instance de MainPageViewModel
injectée. À son tour, l’instance de MainPageViewModel
a ILoggingService
et ISettingsService
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 méthode Routing.RegisterRoute.
Résolution explicite des dépendances
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’un Element via sa propriété Handler.MauiContext.Service
, 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 gestionnaire d’événements HandlerChanged
garantit qu’un gestionnaire a été défini pour la page et, par conséquent, que la propriété Handler
ne sera pas null
.
Avertissement
La propriété Handler
de votre Element
peut être null
. Sachez donc que vous devrez peut-être tenir compte de cette situation. Pour obtenir plus d’informations, consultez Gestionnaires de cycle de vie.
Dans un modèle de vue, il est possible d’accéder explicitement au conteneur d’injection de dépendances par l’intermédiaire de la propriété Handler.MauiContext.Service
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 type de Application. Toutefois, cet inconvénient peut être éliminé en passant un argument IServiceProvider au constructeur de modèle d’affichage. Le IServiceProvider est résolu via 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 dépendance IServiceProvider peuvent être résolus automatiquement, à condition que le type soit inscrit auprès du conteneur d’injection de dépendances. La IServiceProvider peut ensuite être utilisée 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 instance de IServiceProvider est accessible sur chaque plateforme via la propriété IPlatformApplication.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 automatique des dépendances pour l’injecter dans le constructeur App
et la définir comme valeur de la propriété MainPage
:
public App(MyFirstAppPage page)
{
InitializeComponent();
MainPage = page;
}
Toutefois, dans ce scénario, si MyFirstAppPage
tente d’accéder à un StaticResource
déclaré en XAML dans le dictionnaire de ressources App
, une XamlParseException 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 un IServiceProvider dans votre classe App
, puis à l’utiliser pour résoudre la page à l’intérieur de la classe App
:
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.