Inserción de dependencia

La interfaz de usuario de aplicaciones multiplataforma de .NET (.NET MAUI) proporciona compatibilidad integrada para usar la inserción de dependencias. La inserción de dependencias es una versión especializada del patrón Inversion of Control (IoC), donde la preocupación que se invierte es el proceso de obtener la dependencia necesaria. Con la inserción de dependencias, otra clase es responsable de insertar dependencias en un objeto en tiempo de ejecución.

Normalmente, se invoca un constructor de clase al crear una instancia de un objeto, y los valores que el objeto necesita se pasan como argumentos al constructor. Este es un ejemplo de inserción de dependencias conocida como inserción de constructores. Las dependencias que necesita el objeto se insertan en el constructor.

Nota:

También hay otros tipos de inserción de dependencias, como la inserción de establecedores de propiedades y la inserción de llamadas de método, pero se usan con menos frecuencia.

Al especificar dependencias como tipos de interfaz, la inserción de dependencias permite desacoplar los tipos concretos del código que depende de estos tipos. Por lo general, usa un contenedor que contiene una lista de registros y asignaciones entre interfaces y tipos abstractos, y los tipos concretos que implementan o extienden estos tipos.

Contenedores de inserción de dependencias

Si una clase no crea instancias directamente de los objetos que necesita, otra clase debe asumir esta responsabilidad. Considere el ejemplo siguiente, que muestra una clase de modelo de vista que requiere argumentos de constructor:

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

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

En este ejemplo, el MainPageViewModel constructor requiere dos instancias de objeto de interfaz como argumentos insertados por otra clase. La única dependencia de la clase MainPageViewModel es de los tipos de interfaz. Por lo tanto, la clase MainPageViewModel no tiene ningún conocimiento de la clase responsable de crear instancias de los objetos de interfaz.

Del mismo modo, considere el ejemplo siguiente que muestra una clase de página que requiere un argumento de constructor:

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

En este ejemplo, el MainPage constructor requiere un tipo concreto como argumento insertado por otra clase. La única dependencia de la MainPage clase está en el MainPageViewModel tipo . Por lo tanto, la MainPage clase no tiene ningún conocimiento de la clase responsable de crear instancias del tipo concreto.

En ambos casos, la clase responsable de crear instancias de las dependencias e insertarlas en la clase dependiente se conoce como contenedor de inserción de dependencias.

Los contenedores de inserción de dependencias reducen el acoplamiento entre objetos proporcionando un recurso para crear instancias de clase y administrar su duración en función de la configuración del contenedor. Durante la creación de objetos, el contenedor inserta todas las dependencias que requiera el objeto. Si no se han creado esas dependencias, el contenedor crea y resuelve primero sus dependencias.

El uso de un contenedor de inserción de dependencias ofrece varias ventajas:

  • Un contenedor elimina la necesidad de que una clase localice sus dependencias y administre sus duraciones.
  • Un contenedor permite la asignación de dependencias implementadas sin afectar a la clase.
  • Un contenedor facilita la comprobación, ya que permite simular las dependencias.
  • Un contenedor aumenta la capacidad de mantenimiento al permitir que las nuevas clases se agreguen fácilmente a la aplicación.

En el contexto de una aplicación MAUI de .NET que usa el patrón Model-View-View-ViewModel (MVVM), normalmente se usará un contenedor de inserción de dependencias para registrar y resolver vistas, registrar y resolver modelos de vista, así como para registrar servicios e insertarlos en modelos de vista. Para obtener más información sobre el patrón MVVM, consulte Model-View-ViewModel (MVVM).

Hay muchos contenedores de inserción de dependencias disponibles para .NET. .NET MAUI tiene compatibilidad integrada para usar Microsoft.Extensions.DependencyInjection para administrar la creación de instancias de vistas, modelos de vista y clases de servicio en una aplicación. Microsoft.Extensions.DependencyInjection facilita la creación de aplicaciones de acoplamiento flexible y proporciona todas las características que se encuentran normalmente en contenedores de inserción de dependencias, incluidos métodos para registrar asignaciones de tipos e instancias de objeto, resolver objetos, administrar duraciones de objetos e insertar objetos dependientes en constructores de objetos que resuelve. Para más información sobre Microsoft.Extensions.DependencyInjection, vea Inserción de dependencias en .NET.

En tiempo de ejecución, el contenedor debe saber qué implementación de las dependencias se solicitan para crear instancias de ellos para los objetos solicitados. En el ejemplo anterior, las ILoggingService interfaces y ISettingsService deben resolverse antes de que se pueda crear una instancia del MainPageViewModel objeto. Esto implica que el contenedor realice las siguientes acciones:

  • Decidir cómo crear una instancia de un objeto que implementa la interfaz. Esto se conoce como registro. Para más información, consulte Registro.
  • Crear instancias del objeto que implementa la interfaz necesaria y el objeto MainPageViewModel. Esto se conoce como resolución. Para obtener más información, consulte Resolución.

Finalmente, una aplicación terminará de usar el MainPageViewModel objeto y estará disponible para la recolección de elementos no utilizados. En este momento, el recolector de elementos no utilizados debe eliminar cualquier implementación de interfaz de corta duración si otras clases no comparten las mismas instancias.

Registro

Antes de que las dependencias se puedan insertar en un objeto, los tipos de las dependencias deben registrarse primero en el contenedor. El registro de un tipo normalmente implica pasar el contenedor un tipo concreto, o una interfaz y un tipo concreto que implementa la interfaz.

Hay dos enfoques principales para registrar tipos y objetos con el contenedor:

  • Registre un tipo o una asignación en el contenedor. Esto se conoce como registro transitorio. Cuando sea necesario, el contenedor compilará una instancia del tipo especificado.
  • Registre un objeto existente en el contenedor como singleton. Cuando sea necesario, el contenedor devolverá una referencia al objeto existente.

Precaución

Los contenedores de inserción de dependencias no siempre son adecuados para una aplicación MAUI de .NET. La inserción de dependencias presenta complejidad y requisitos adicionales que podrían no ser adecuados o útiles para aplicaciones más pequeñas. Si una clase no tiene ninguna dependencia o no es una dependencia para otros tipos, es posible que no tenga sentido colocarla en el contenedor. Además, si una clase tiene un único conjunto de dependencias que son integrales al tipo y nunca cambiará, es posible que no tenga sentido colocarlas en el contenedor.

El registro de tipos que requieren inserción de dependencias debe realizarse en un único método de la aplicación. Este método debe invocarse al principio del ciclo de vida de la aplicación para asegurarse de que conoce las dependencias entre sus clases. Las aplicaciones normalmente deben realizar esto en el CreateMauiApp método de la MauiProgram clase . La MauiProgram clase llama al CreateMauiApp método para crear un MauiAppBuilder objeto . El MauiAppBuilder objeto tiene una Services propiedad de tipo IServiceCollection, que proporciona un lugar para registrar los tipos, como vistas, modelos de vista y servicios para la inserción de dependencias:

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

Los tipos registrados con la Services propiedad se proporcionan al contenedor de inserción de dependencias cuando MauiAppBuilder.Build() se llama a .

Al registrar dependencias, debe registrar todas las dependencias, incluidos los tipos que requieran las dependencias. Por lo tanto, si tiene un modelo de vista que toma una dependencia como parámetro de constructor, debe registrar el modelo de vista junto con todas sus dependencias. Del mismo modo, si tiene una vista que toma una dependencia de modelo de vista como parámetro de constructor, debe registrar la vista y el modelo de vista junto con todas sus dependencias.

Sugerencia

Un contenedor de inserción de dependencias es ideal para crear instancias de modelo de vista. Si un modelo de vista tiene dependencias, administrará la creación e inserción de los servicios necesarios. Solo tiene que asegurarse de registrar los modelos de vista y las dependencias que puedan tener en el CreateMauiApp método de la MauiProgram clase .

Duración de dependencias

En función de las necesidades de la aplicación, es posible que deba registrar dependencias con distintas duraciones. En la tabla siguiente se enumeran los métodos principales que puede usar para registrar dependencias y sus duraciones de registro:

Method Descripción
AddSingleton<T> Crea una única instancia del objeto que permanecerá durante la vigencia de la aplicación.
AddTransient<T> Crea una nueva instancia del objeto cuando se solicita durante la resolución. Los objetos transitorios no tienen una duración predefinida, pero normalmente durarán lo mismo que su host.
AddScoped<T> Crea una instancia del objeto que comparte la duración de su host. Cuando el host sale del ámbito, así que hace su dependencia. Por lo tanto, resolver la misma dependencia varias veces dentro del mismo ámbito produce la misma instancia, mientras que la resolución de la misma dependencia en ámbitos diferentes producirá instancias diferentes.

Nota:

Si un objeto no hereda de una interfaz, como una vista o un modelo de vista, solo debe proporcionarse su tipo concreto al AddSingleton<T>método , AddTransient<T>o AddScoped<T> .

La MainPageViewModel clase se usa cerca de la raíz de la aplicación y siempre debe estar disponible, por lo que registrarla con AddSingleton<T> es beneficiosa. Es posible que otros modelos de vista se naveguen o se usen más adelante en una aplicación. Si tiene un tipo que podría no usarse siempre, o si es memoria o un uso intensivo de cálculo o requiere datos Just-In-Time, puede ser un mejor candidato para AddTransient<T> el registro.

Otra manera común de registrar dependencias es usar los AddSingleton<TService, TImplementation>métodos , AddTransient<TService, TImplementation>o AddScoped<TService, TImplementation> . Estos métodos toman dos tipos: la definición de interfaz y la implementación concreta. Este tipo de registro es más conveniente para los casos en los que se implementan servicios basados en interfaces.

Una vez registrados todos los tipos, MauiAppBuilder.Build() se debe llamar a para crear el MauiApp objeto y rellenar el contenedor de inserción de dependencias con todos los tipos registrados.

Importante

Una vez MauiAppBuilder.Build() que se haya llamado a , los tipos registrados con el contenedor de inserción de dependencias serán inmutables y ya no se pueden actualizar ni modificar.

Registro de dependencias con un método de extensión

El MauiApp.CreateBuilder método crea un MauiAppBuilder objeto que se puede usar para registrar dependencias. Si la aplicación necesita registrar muchas dependencias, puede crear métodos de extensión para ayudar a proporcionar un flujo de trabajo de registro organizado y fácil de mantener:

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

En este ejemplo, los tres métodos de extensión de registro usan la MauiAppBuilder instancia para acceder a la Services propiedad para registrar dependencias.

Solución

Una vez registrado un tipo, se puede resolver o insertar como una dependencia. Cuando se resuelve un tipo y el contenedor debe crear una instancia, inserta todas las dependencias en la instancia.

Por lo general, cuando se resuelve un tipo, se produce uno de los tres escenarios:

  1. Si el tipo no se ha registrado, el contenedor produce una excepción.
  2. Si el tipo se ha registrado como singleton, el contenedor devuelve la instancia singleton. Si es la primera vez que se llama al tipo, el contenedor lo crea si es necesario y mantiene una referencia a él.
  3. Si el tipo se ha registrado como transitorio, el contenedor devuelve una nueva instancia y no mantiene una referencia a él.

.NET MAUI admite la resolución de dependencias automática y explícita . La resolución automática de dependencias usa la inserción de constructores sin solicitar explícitamente la dependencia del contenedor. La resolución de dependencias explícita se produce a petición solicitando explícitamente una dependencia del contenedor.

Resolución automática de dependencias

La resolución automática de dependencias se produce en las aplicaciones que usan el shell de MAUI de .NET, siempre que haya registrado el tipo de dependencia y el tipo que usa la dependencia con el contenedor de inserción de dependencias.

Durante la navegación basada en Shell, .NET MAUI buscará registros de página y, si se encuentra alguno, creará esa página e insertará dependencias en su constructor:

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

En este ejemplo, el MainPage constructor recibe una MainPageViewModel instancia que se inserta. A su vez, la MainPageViewModel instancia tiene ILoggingService y ISettingsService las instancias insertadas:

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

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

Además, en una aplicación basada en Shell, .NET MAUI insertará dependencias en páginas de detalles registradas con el Routing.RegisterRoute método .

Resolución de dependencias explícita

Una aplicación basada en Shell no puede usar la inserción de constructores cuando un tipo solo expone un constructor sin parámetros. Como alternativa, si la aplicación no usa Shell, deberá usar la resolución de dependencias explícita.

Se puede acceder explícitamente al contenedor de inserción de dependencias desde a Element través de su Handler.MauiContext.Service propiedad , que es de tipo IServiceProvider:

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

        HandlerChanged += OnHandlerChanged;
    }

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

Este enfoque puede ser útil si necesita resolver una dependencia desde o Elementdesde fuera del constructor de .Element En este ejemplo, el acceso al contenedor de inserción de dependencias en el HandlerChanged controlador de eventos garantiza que se ha establecido un controlador para la página y, por tanto, que la Handler propiedad no será null.

Advertencia

La Handler propiedad de puede Element ser null, por lo que debe tener en cuenta esta situación. Para obtener más información, consulte Ciclo de vida del controlador.

En un modelo de vista, se puede acceder explícitamente al contenedor de inserción de dependencias a través de la Handler.MauiContext.Service propiedad 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>();
    }
}

Un inconveniente de este enfoque es que el modelo de vista ahora tiene una dependencia del Application tipo. Sin embargo, este inconveniente se puede eliminar pasando un IServiceProvider argumento al constructor de modelo de vista. IServiceProvider se resuelve a través de la resolución automática de dependencias sin tener que registrarla en el contenedor de inserción de dependencias. Con este enfoque se puede resolver automáticamente un tipo y su IServiceProvider dependencia siempre que el tipo se registre en el contenedor de inserción de dependencias. IServiceProvider Después, se puede usar para la resolución de dependencias explícita:

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

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

Además, se puede acceder a una IServiceProvider instancia a través de las siguientes propiedades nativas:

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

Limitaciones con recursos XAML

Un escenario común es registrar una página con el contenedor de inserción de dependencias y usar la resolución automática de dependencias para insertarla en el App constructor y establecerla como el valor de la MainPage propiedad :

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

Sin embargo, en este escenario si MyFirstAppPage intenta tener acceso a un StaticResource que se ha declarado en XAML en el App diccionario de recursos, se producirá un XamlParseException elemento con un mensaje similar a Position {row}:{column}. StaticResource not found for key {key}. Esto ocurre porque la página resuelta a través de la inserción de constructores se ha creado antes de inicializar los recursos XAML de nivel de aplicación.

Una solución alternativa para este problema es insertar un elemento IServiceProvider en la App clase y, a continuación, usarlo para resolver la página dentro de la App clase :

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

Este enfoque obliga a crear e inicializar el árbol de objetos XAML antes de que se resuelva la página.