Compartir a través de


Inserción de dependencia

.NET Multi-Platform App UI (.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 Inversión de control (IoC), en la que la preocupación que se invierte es el proceso de obtención de 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 existen otros tipos de inserción de dependencia, como la inserción de establecedor de propiedad y la inserción de llamada de método, pero su uso es menos frecuente.

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 dependencia

Si una clase no crea una instancia 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 constructor MainPageViewModel 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 constructor MainPage requiere un tipo concreto como argumento insertado por otra clase. La única dependencia de la clase MainPage está en el tipo MainPageViewModel. Por lo tanto, la clase MainPage 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 dependencia.

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 .NET MAUI que usa el patrón Modelo-Vista-Modelo de vista (MVVM), normalmente se usará un contenedor de inserción de dependencia para registrar y resolver vistas, registrar y resolver modelos de vista, y para registrar servicios e insertarlos en modelos de vista. Para obtener más información sobre el patrón MVVM, consulte Modelo-Vista-Modelo de vista (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 el runtime, el contenedor debe saber qué implementación de las dependencias se está solicitando para crear instancias de ellas para los objetos solicitados. En el ejemplo anterior, las interfaces ILoggingService y ISettingsService deben resolverse antes de que se pueda crear una instancia del objeto MainPageViewModel. 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, vea Resolución.

Finalmente, una aplicación terminará de usar el objeto MainPageViewModel 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. Registrar un tipo normalmente implica pasar al contenedor un tipo concreto, o una interfaz y un tipo concreto que implemente 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 .NET MAUI. La inserción de dependencias introduce 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 parte integral del tipo y nunca cambiarán, 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 método CreateMauiApp de la clase MauiProgram. La clase MauiProgram llama al método CreateMauiApp para crear un objeto MauiAppBuilder. El objeto MauiAppBuilder tiene una propiedad Services 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 propiedad Services se proporcionan al contenedor de inserción de dependencia cuando se llama a MauiAppBuilder.Build().

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 dependencia 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 método CreateMauiApp en la clase MauiProgram.

En una aplicación de Shell, no es necesario registrar las páginas con el contenedor de inserción de dependencias a menos que quiera influir en la duración de la página en relación con el contenedor con los AddSingletonmétodos , AddTransiento AddScoped . Para obtener más información, consulte Duración de dependencias.

Duración de la dependencia

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 duración 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, también lo 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 proporcionar su tipo concreto a los métodos AddSingleton<T>, AddTransient<T> o AddScoped<T>.

La clase MainPageViewModel se usa cerca de la raíz de la aplicación y siempre debe estar disponible, por lo que registrarla con AddSingleton<T> es beneficioso. Se puede navegar a otros modelos de vista de forma situacional o utilizarlos posteriormente en una aplicación. Si tiene un tipo que puede que no se utilice siempre, o si es desde el punto de vista de memoria o proceso intensivo o requiere datos Just-In-Time, puede ser un mejor candidato para el registro AddTransient<T>.

Otra manera común de registrar dependencias es usar los métodos AddSingleton<TService, TImplementation>, 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, se debe llamar a MauiAppBuilder.Build() para crear el objeto MauiApp y rellenar el contenedor de inserción de dependencia con todos los tipos registrados.

Importante

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

Registrar dependencias con un método de extensión

El método MauiApp.CreateBuilder crea un objeto MauiAppBuilder 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 instancia MauiAppBuilder para acceder a la propiedad Services 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 dependencia automática y explícita. La resolución automática de dependencias usa la inserción de constructor 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 .NET MAUI, siempre que haya registrado el tipo de dependencia y el tipo que usa la dependencia con el contenedor de inserción de dependencia.

Durante la navegación basada en el 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 constructor MainPage recibe una instancia MainPageViewModel que se inserta. A su vez, la instancia MainPageViewModel tiene instancias ILoggingService y ISettingsService 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 método Routing.RegisterRoute.

Resolución de dependencias explícita

Una aplicación basada en Shell no puede usar la inserción de constructor 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 dependencia desde Element a través de su propiedad Handler.MauiContext.Service, 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 Element o desde fuera del constructor de un Element. En este ejemplo, el acceso al contenedor de inserción de dependencia en el controlador de eventos HandlerChanged garantiza que se ha establecido un controlador para la página y, por tanto, que la propiedad Handler no será null.

Advertencia

La propiedad Handler de su Element podría ser null, por lo que debe tener en cuenta esta situación. Para 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 dependencia a través de la propiedad 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>();
    }
}

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

Un inconveniente de este enfoque es que el modelo de vista ahora tiene una dependencia del tipo Application. Sin embargo, este inconveniente se puede eliminar pasando un argumento IServiceProvider al constructor de modelo de vista. El IServiceProvider se resuelve a través de la resolución automática de dependencias sin necesidad de registrarlo en el contenedor de inserción de dependencia. Con este enfoque, un tipo y su dependencia IServiceProvider pueden resolverse automáticamente siempre que el tipo esté registrado en el contenedor de inserción de dependencia. A continuación, el IServiceProvider se puede usar para la resolución de la dependencia 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 instancia IServiceProvider en cada plataforma a través de la propiedad IPlatformApplication.Current.Services.

Limitaciones con recursos XAML

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

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

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 la primera página que se mostrará en la aplicación:

MyFirstAppPage _firstPage;

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

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

Sin embargo, en este escenario si MyFirstAppPage intenta acceder a un StaticResource que se ha declarado en XAML en el diccionario de recursos App, se producirá un XamlParseException 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 constructor se ha creado antes de inicializar los recursos XAML de nivel de aplicación.

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

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

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