Inserimento delle dipendenze

L'interfaccia utente dell'app multipiattaforma .NET (.NET MAUI) offre il supporto predefinito per l'uso dell'inserimento delle dipendenze. L'inserimento delle dipendenze è una versione specializzata del modello Inversion of Control (IoC), in cui il problema invertito è il processo di recupero della dipendenza richiesta. Con l'inserimento delle dipendenze, un'altra classe è responsabile dell'inserimento di dipendenze in un oggetto in fase di esecuzione.

In genere, un costruttore di classe viene richiamato quando si crea un'istanza di un oggetto e tutti i valori necessari all'oggetto vengono passati come argomenti al costruttore. Questo è un esempio di inserimento delle dipendenze noto come inserimento di costruttori. Le dipendenze necessarie per l'oggetto vengono inserite nel costruttore.

Nota

Esistono anche altri tipi di inserimento delle dipendenze, ad esempio l'inserimento di setter di proprietà e l'inserimento di chiamate al metodo, ma sono meno comunemente usati.

Specificando le dipendenze come tipi di interfaccia, l'inserimento delle dipendenze consente di separare i tipi concreti dal codice che dipende da questi tipi. In genere usa un contenitore che contiene un elenco di registrazioni e mapping tra interfacce e tipi astratti e i tipi concreti che implementano o estendono questi tipi.

Contenitori di inserimento delle dipendenze

Se una classe non crea direttamente un'istanza degli oggetti necessari, un'altra classe deve assumere questa responsabilità. Si consideri l'esempio seguente, che mostra una classe view-model che richiede argomenti del costruttore:

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

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

In questo esempio, il MainPageViewModel costruttore richiede due istanze dell'oggetto di interfaccia come argomenti inseriti da un'altra classe. L'unica dipendenza nella classe MainPageViewModel è sui tipi di interfaccia. Pertanto, la classe MainPageViewModel non ha alcuna conoscenza della classe responsabile della creazione di istanze degli oggetti interfaccia.

Analogamente, si consideri l'esempio seguente che mostra una classe di pagina che richiede un argomento del costruttore:

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

In questo esempio, il MainPage costruttore richiede un tipo concreto come argomento inserito da un'altra classe. L'unica dipendenza nella MainPage classe è sul MainPageViewModel tipo . Pertanto, la MainPage classe non ha alcuna conoscenza della classe responsabile della creazione di un'istanza del tipo concreto.

In entrambi i casi, la classe responsabile della creazione di un'istanza delle dipendenze e dell'inserimento delle dipendenze nella classe dipendente è nota come contenitore di inserimento delle dipendenze.

I contenitori di inserimento delle dipendenze riducono l'accoppiamento tra oggetti fornendo una funzionalità per creare istanze di classe e gestirle in base alla configurazione del contenitore. Durante la creazione dell'oggetto, il contenitore inserisce tutte le dipendenze richieste dall'oggetto. Se tali dipendenze non sono state create, il contenitore crea e risolve prima le relative dipendenze.

L'uso di un contenitore di inserimento delle dipendenze offre diversi vantaggi:

  • Un contenitore rimuove la necessità di una classe per individuare le relative dipendenze e gestirle.
  • Un contenitore consente il mapping delle dipendenze implementate senza influire sulla classe.
  • Un contenitore facilita la testabilità consentendo la simulazione delle dipendenze.
  • Un contenitore aumenta la manutenibilità consentendo di aggiungere facilmente nuove classi all'app.

Nel contesto di un'app MAUI .NET che usa il modello Model-View-ViewModel (MVVM), in genere verrà usato un contenitore di inserimento delle dipendenze per la registrazione e la risoluzione delle visualizzazioni, la registrazione e la risoluzione dei modelli di visualizzazione e per la registrazione dei servizi e l'inserimento in modelli di visualizzazione. Per altre informazioni sul modello MVVM, vedere Model-View-ViewModel (MVVM).

Sono disponibili molti contenitori di inserimento delle dipendenze per .NET. .NET MAUI include il supporto predefinito per l'uso Microsoft.Extensions.DependencyInjection per gestire la creazione di istanze di visualizzazioni, modelli di visualizzazione e classi di servizio in un'app. Microsoft.Extensions.DependencyInjection facilita la creazione di app ad accoppiamento libero e fornisce tutte le funzionalità comunemente disponibili nei contenitori di inserimento delle dipendenze, inclusi i metodi per registrare mapping dei tipi e istanze di oggetti, risolvere gli oggetti, gestire la durata degli oggetti e inserire oggetti dipendenti nei costruttori di oggetti risolti. Per ulteriori informazioni su Microsoft.Extensions.DependencyInjection, consultare inserimento delle dipendenze in .NET.

In fase di esecuzione, il contenitore deve conoscere l'implementazione delle dipendenze richieste per crearne un'istanza per gli oggetti richiesti. Nell'esempio precedente è necessario risolvere le ILoggingService interfacce e ISettingsService prima di poter creare un'istanza dell'oggetto MainPageViewModel . Ciò comporta l'esecuzione delle azioni seguenti per il contenitore:

  • Decidere come creare un'istanza di un oggetto che implementa l'interfaccia. Questa operazione è nota come registrazione. Per altre informazioni, vedere Registrazione.
  • Creazione di un'istanza dell'oggetto che implementa l'interfaccia richiesta e l'oggetto MainPageViewModel. Questa operazione è nota come risoluzione. Per altre informazioni, vedere Risoluzione.

Alla fine, un'app terminerà l'uso dell'oggetto MainPageViewModel e diventerà disponibile per l'operazione di Garbage Collection. A questo punto, il Garbage Collector deve eliminare eventuali implementazioni di interfacce di breve durata se altre classi non condividono le stesse istanze.

Registrazione

Prima che le dipendenze possano essere inserite in un oggetto, i tipi per le dipendenze devono prima essere registrati con il contenitore. La registrazione di un tipo comporta in genere il passaggio del contenitore a un tipo concreto o un'interfaccia e un tipo concreto che implementa l'interfaccia.

Esistono due approcci principali alla registrazione di tipi e oggetti con il contenitore:

  • Registrare un tipo o un mapping con il contenitore. Questa operazione è nota come registrazione temporanea. Se necessario, il contenitore compilerà un'istanza del tipo specificato.
  • Registrare un oggetto esistente nel contenitore come singleton. Se necessario, il contenitore restituirà un riferimento all'oggetto esistente.

Attenzione

I contenitori di inserimento delle dipendenze non sono sempre adatti per un'app MAUI .NET. L'inserimento delle dipendenze introduce complessità e requisiti aggiuntivi che potrebbero non essere appropriati o utili per le app più piccole. Se una classe non ha dipendenze o non è una dipendenza per altri tipi, potrebbe non essere opportuno inserirla nel contenitore. Inoltre, se una classe ha un singolo set di dipendenze che sono integrali al tipo e non cambierà mai, potrebbe non essere opportuno inserirle nel contenitore.

La registrazione dei tipi che richiedono l'inserimento delle dipendenze deve essere eseguita in un singolo metodo nell'app. Questo metodo deve essere richiamato all'inizio del ciclo di vita dell'app per assicurarsi che sia a conoscenza delle dipendenze tra le relative classi. Le app devono in genere eseguire questa operazione nel CreateMauiApp metodo nella MauiProgram classe . La MauiProgram classe chiama nel CreateMauiApp metodo per creare un MauiAppBuilder oggetto . L'oggetto MauiAppBuilder ha una Services proprietà di tipo IServiceCollection, che fornisce una posizione in cui registrare i tipi, ad esempio visualizzazioni, modelli di visualizzazione e servizi per l'inserimento delle dipendenze:

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

I tipi registrati con la Services proprietà vengono forniti al contenitore di inserimento delle dipendenze quando MauiAppBuilder.Build() viene chiamato .

Quando si registrano le dipendenze, è necessario registrare tutte le dipendenze, inclusi tutti i tipi che richiedono le dipendenze. Pertanto, se si dispone di un modello di visualizzazione che accetta una dipendenza come parametro del costruttore, è necessario registrare il modello di visualizzazione insieme a tutte le relative dipendenze. Analogamente, se si dispone di una vista che accetta una dipendenza del modello di visualizzazione come parametro del costruttore, è necessario registrare la vista e il modello di visualizzazione insieme a tutte le relative dipendenze.

Suggerimento

Un contenitore di inserimento delle dipendenze è ideale per la creazione di istanze del modello di visualizzazione. Se un modello di visualizzazione ha dipendenze, gestirà la creazione e l'inserimento di tutti i servizi necessari. Assicurarsi di registrare i modelli di visualizzazione e le eventuali dipendenze che potrebbero avere nel CreateMauiApp metodo nella MauiProgram classe .

In un'app Shell non è necessario registrare le pagine con il contenitore di inserimento delle dipendenze, a meno che non si voglia influenzare la durata della pagina rispetto al contenitore con i AddSingletonmetodi , AddTransiento AddScoped . Per altre informazioni, vedere Durata delle dipendenze.

Durata delle dipendenze

A seconda delle esigenze dell'app, potrebbe essere necessario registrare le dipendenze con durate diverse. Nella tabella seguente sono elencati i metodi principali che è possibile usare per registrare le dipendenze e le relative durate di registrazione:

metodo Descrizione
AddSingleton<T> Crea una singola istanza dell'oggetto che rimarrà per la durata dell'app.
AddTransient<T> Crea una nuova istanza dell'oggetto quando richiesto durante la risoluzione. Gli oggetti temporanei non hanno una durata predefinita, ma in genere seguiranno la durata dell'host.
AddScoped<T> Crea un'istanza dell'oggetto che condivide la durata dell'host. Quando l'host esce dall'ambito, esegue la dipendenza. Pertanto, la risoluzione della stessa dipendenza più volte all'interno dello stesso ambito produce la stessa istanza, mentre la risoluzione della stessa dipendenza in ambiti diversi restituirà istanze diverse.

Nota

Se un oggetto non eredita da un'interfaccia, ad esempio una vista o un modello di visualizzazione, è necessario fornire solo il tipo concreto al AddSingleton<T>metodo , AddTransient<T>o AddScoped<T> .

La MainPageViewModel classe viene usata vicino alla radice dell'app e deve essere sempre disponibile, quindi la registrazione con AddSingleton<T> è utile. Altri modelli di visualizzazione possono essere spostati o usati in un secondo momento in un'app. Se si dispone di un tipo che potrebbe non essere sempre usato, o se si tratta di memoria o un uso intensivo del calcolo o richiede dati JUST-In-Time, potrebbe essere un candidato migliore per AddTransient<T> la registrazione.

Un altro modo comune per registrare le dipendenze consiste nell'usare i AddSingleton<TService, TImplementation>metodi , AddTransient<TService, TImplementation>o AddScoped<TService, TImplementation> . Questi metodi accettano due tipi: la definizione dell'interfaccia e l'implementazione concreta. Questo tipo di registrazione è ideale per i casi in cui si implementano servizi basati su interfacce.

Dopo aver registrato tutti i tipi, MauiAppBuilder.Build() deve essere chiamato per creare l'oggetto MauiApp e popolare il contenitore di inserimento delle dipendenze con tutti i tipi registrati.

Importante

Una volta MauiAppBuilder.Build() chiamato, i tipi registrati con il contenitore di inserimento delle dipendenze non saranno modificabili e non potranno più essere aggiornati o modificati.

Registrare le dipendenze con un metodo di estensione

Il MauiApp.CreateBuilder metodo crea un MauiAppBuilder oggetto che può essere utilizzato per registrare le dipendenze. Se l'app deve registrare molte dipendenze, è possibile creare metodi di estensione per fornire un flusso di lavoro di registrazione organizzato e gestibile:

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

In questo esempio i tre metodi di estensione di registrazione usano l'istanza MauiAppBuilder per accedere alla Services proprietà per registrare le dipendenze.

Risoluzione

Dopo la registrazione di un tipo, può essere risolto o inserito come dipendenza. Quando viene risolto un tipo e il contenitore deve creare una nuova istanza, inserisce eventuali dipendenze nell'istanza.

In genere, quando viene risolto un tipo, si verifica uno dei tre scenari seguenti:

  1. Se il tipo non è stato registrato, il contenitore genera un'eccezione.
  2. Se il tipo è stato registrato come singleton, il contenitore restituisce l'istanza singleton. Se è la prima volta che viene chiamato il tipo, il contenitore lo crea se necessario e mantiene un riferimento a esso.
  3. Se il tipo è stato registrato come temporaneo, il contenitore restituisce una nuova istanza e non ne mantiene un riferimento.

.NET MAUI supporta la risoluzione automatica ed esplicita delle dipendenze. La risoluzione automatica delle dipendenze usa l'inserimento del costruttore senza richiedere esplicitamente la dipendenza dal contenitore. La risoluzione esplicita delle dipendenze si verifica su richiesta richiedendo in modo esplicito una dipendenza dal contenitore.

Risoluzione automatica delle dipendenze

La risoluzione automatica delle dipendenze si verifica nelle app che usano la shell MAUI .NET, purché sia stato registrato il tipo di dipendenza e il tipo che usa la dipendenza con il contenitore di inserimento delle dipendenze.

Durante la navigazione basata su Shell, .NET MAUI cercherà le registrazioni di pagina e, se presente, creerà tale pagina e inserirà eventuali dipendenze nel relativo costruttore:

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

In questo esempio il MainPage costruttore riceve un'istanza MainPageViewModel inserita. A sua volta, l'istanza MainPageViewModel ha ILoggingService e ISettingsService le istanze inserite:

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

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

Inoltre, in un'app basata su Shell, .NET MAUI inserirà le dipendenze nelle pagine di dettaglio registrate con il Routing.RegisterRoute metodo .

Risoluzione esplicita delle dipendenze

Un'app basata su Shell non può usare l'inserimento del costruttore quando un tipo espone solo un costruttore senza parametri. In alternativa, se l'app non usa Shell, dovrai usare la risoluzione esplicita delle dipendenze.

È possibile accedere in modo esplicito al contenitore di inserimento delle dipendenze da un oggetto Element tramite la relativa Handler.MauiContext.Service proprietà , che è di tipo IServiceProvider:

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

        HandlerChanged += OnHandlerChanged;
    }

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

Questo approccio può essere utile se è necessario risolvere una dipendenza da un Elementoggetto o dall'esterno del costruttore di un oggetto Element. In questo esempio, l'accesso al contenitore di inserimento delle dipendenze nel HandlerChanged gestore eventi garantisce che sia stato impostato un gestore per la pagina e quindi che la Handler proprietà non sia null.

Avviso

La Handler proprietà di Element potrebbe essere null, quindi tenere presente che potrebbe essere necessario tenere conto di questa situazione. Per altre informazioni, vedere Ciclo di vita del gestore.

In un modello di visualizzazione, è possibile accedere in modo esplicito al contenitore di inserimento delle dipendenze tramite la Handler.MauiContext.Service proprietà di 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>();
    }
}

In un modello di visualizzazione, è possibile accedere in modo esplicito al contenitore di inserimento delle dipendenze tramite la Handler.MauiContext.Service proprietà di Window.Page:

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

    public MainPageViewModel()
    {
        _loggingService = Application.Current.Windows[0].Page.Handler.MauiContext.Services.GetService<ILoggingService>();
        _settingsService = Application.Current.Windows[0].Page.Handler.MauiContext.Services.GetService<ISettingsService>();
    }
}

Uno svantaggio di questo approccio è che il modello di visualizzazione ha ora una dipendenza dal Application tipo . Tuttavia, questo svantaggio può essere eliminato passando un IServiceProvider argomento al costruttore view-model. L'oggetto IServiceProvider viene risolto tramite la risoluzione automatica delle dipendenze senza doverlo registrare con il contenitore di inserimento delle dipendenze. Con questo approccio un tipo e la relativa IServiceProvider dipendenza possono essere risolti automaticamente a condizione che il tipo sia registrato con il contenitore di inserimento delle dipendenze. Può IServiceProvider quindi essere usato per la risoluzione esplicita delle dipendenze:

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

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

Inoltre, è possibile accedere a un'istanza IServiceProvider in ogni piattaforma tramite la IPlatformApplication.Current.Services proprietà .

Limitazioni con le risorse XAML

Uno scenario comune consiste nel registrare una pagina con il contenitore di inserimento delle dipendenze e usare la risoluzione automatica delle dipendenze per inserirla nel App costruttore e impostarla come valore della MainPage proprietà:

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

Uno scenario comune consiste nel registrare una pagina con il contenitore di inserimento delle dipendenze e usare la risoluzione automatica delle dipendenze per inserirla nel App costruttore e impostarla come prima pagina da visualizzare nell'app:

MyFirstAppPage _firstPage;

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

protected override Window CreateWindow(IActivationState? activationState)
{
    return new Window(_firstPage);
}

In questo scenario, tuttavia, se MyFirstAppPage tenta di accedere a un StaticResource oggetto dichiarato in XAML nel App dizionario risorse, verrà generato un oggetto XamlParseException con un messaggio simile a Position {row}:{column}. StaticResource not found for key {key}. Ciò si verifica perché la pagina risolta tramite l'inserimento del costruttore è stata creata prima che le risorse XAML a livello di applicazione siano state inizializzate.

Una soluzione alternativa per questo problema consiste nell'inserire un oggetto IServiceProvider nella App classe e quindi usarlo per risolvere la pagina all'interno della App classe :

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

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

protected override Window CreateWindow(IActivationState? activationState)
{
    return new Window(_firstPage);
}

Questo approccio forza la creazione e l'inizializzazione dell'albero degli oggetti XAML prima che la pagina venga risolta.