Поделиться через


Внедрение зависимостей

Многоплатформенный пользовательский интерфейс приложения .NET (.NET MAUI) обеспечивает встроенную поддержку внедрения зависимостей. Внедрение зависимостей — это специализированная версия шаблона инверсии элемента управления (IoC), при которой превратимая проблема — это процесс получения требуемой зависимости. При внедрении зависимостей другой класс отвечает за внедрение зависимостей в объект во время выполнения.

Как правило, конструктор класса вызывается при создании экземпляра объекта и все значения, необходимые объекту, передаются в качестве аргументов конструктору. Это пример внедрения зависимостей, известный как внедрение конструктора. Зависимости, необходимые для объекта, внедряются в конструктор.

Примечание.

Существуют и другие типы внедрения зависимостей, такие как внедрение метода задания свойств и внедрение вызовов методов, но они реже используются.

При указании зависимостей в качестве типов интерфейса внедрение зависимостей позволяет развязывание конкретных типов от кода, зависящее от этих типов. Обычно он использует контейнер, содержащий список регистраций и сопоставлений между интерфейсами и абстрактными типами, а также конкретные типы, реализующие или расширяющие эти типы.

Контейнеры внедрения зависимостей

Если класс не создает экземпляры необходимых объектов, другой класс должен взять на себя эту ответственность. Рассмотрим следующий пример, в котором показан класс модели представления, для которого требуются аргументы конструктора:

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

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

В этом примере конструктору требуется два экземпляра объекта интерфейса в качестве аргументов, MainPageViewModel внедренных другим классом. Единственная зависимость в MainPageViewModel классе зависит от типов интерфейса. MainPageViewModel Поэтому у класса нет знаний о классе, ответственном за создание экземпляров объектов интерфейса.

Аналогичным образом рассмотрим следующий пример, показывающий класс страницы, требующий аргумент конструктора:

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

В этом примере MainPage конструктору требуется конкретный тип в качестве аргумента, внедренного другим классом. Единственная зависимость в MainPage классе зависит от MainPageViewModel типа. MainPage Поэтому у класса нет знаний о классе, ответственном за создание экземпляра конкретного типа.

В обоих случаях класс, отвечающий за создание экземпляров зависимостей и их вставку в зависимый класс, называется контейнером внедрения зависимостей.

Контейнеры внедрения зависимостей сокращают связь между объектами, предоставляя объект для создания экземпляров классов и управления их временем существования на основе конфигурации контейнера. Во время создания объекта контейнер внедряет все зависимости, необходимые объекту. Если эти зависимости не были созданы, контейнер сначала создает и разрешает их зависимости.

Существует несколько преимуществ использования контейнера внедрения зависимостей:

  • Контейнер удаляет необходимость в поиске зависимостей класса и управлении его временем существования.
  • Контейнер позволяет сопоставлять реализованные зависимости, не затрагивая класс.
  • Контейнер упрощает тестирование, позволяя издеваться над зависимостями.
  • Контейнер повышает удобство обслуживания, позволяя новым классам легко добавляться в приложение.

В контексте приложения .NET MAUI, использующего шаблон Model-View-ViewModel (MVVM), контейнер внедрения зависимостей обычно будет использоваться для регистрации и разрешения представлений, регистрации и разрешения моделей представлений, а также для регистрации служб и внедрения их в модели представления. Дополнительные сведения о шаблоне MVVM см. в разделе Model-View-ViewModel (MVVM).

Существует множество контейнеров внедрения зависимостей для .NET. .NET MAUI имеет встроенную поддержку для Microsoft.Extensions.DependencyInjection управления экземплярами представлений, моделей представлений и классов служб в приложении. Microsoft.Extensions.DependencyInjection упрощает создание слабо связанных приложений и предоставляет все функции, часто найденные в контейнерах внедрения зависимостей, включая методы для регистрации сопоставлений типов и экземпляров объектов, разрешения объектов, управления временем существования объектов и внедрения зависимых объектов в конструкторы объектов, которые он разрешает. Дополнительные сведения см Microsoft.Extensions.DependencyInjection. в статье об внедрении зависимостей в .NET.

Во время выполнения контейнер должен знать, какая реализация зависимостей запрашивается, чтобы создать экземпляры для запрошенных объектов. В приведенном выше ILoggingService примере необходимо разрешить интерфейсы и ISettingsService интерфейсы, прежде чем MainPageViewModel объект можно будет создать экземпляр. Это включает в себя контейнер, выполняющий следующие действия:

  • Решение о создании экземпляра объекта, реализующего интерфейс. Это называется регистрацией. Дополнительные сведения см. в разделе Регистрация.
  • Создание экземпляра объекта, реализующего необходимый интерфейс и MainPageViewModel объект. Это называется разрешением. Дополнительные сведения см. в разделе "Разрешение".

В конечном итоге приложение завершит использование MainPageViewModel объекта, и оно станет доступным для сборки мусора. На этом этапе сборщик мусора должен удалять любые кратковременные реализации интерфейса, если другие классы не используют одни и те же экземпляры.

Регистрация

Перед внедрением зависимостей в объект типы зависимостей необходимо сначала зарегистрировать в контейнере. Регистрация типа обычно включает передачу контейнера конкретного типа или интерфейса и конкретного типа, реализующего интерфейс.

Существует два основных подхода к регистрации типов и объектов в контейнере:

  • Зарегистрируйте тип или сопоставление с контейнером. Это называется временной регистрацией. При необходимости контейнер создаст экземпляр указанного типа.
  • Зарегистрируйте существующий объект в контейнере в качестве одного. При необходимости контейнер вернет ссылку на существующий объект.

Внимание

Контейнеры внедрения зависимостей не всегда подходят для приложения .NET MAUI. Внедрение зависимостей представляет дополнительную сложность и требования, которые могут быть не подходящими или полезными для небольших приложений. Если класс не имеет зависимостей или не является зависимостью для других типов, он может не иметь смысла поместить его в контейнер. Кроме того, если у класса есть один набор зависимостей, которые являются неотъемлемой частью типа и никогда не изменятся, может не иметь смысла поместить их в контейнер.

Регистрация типов, требующих внедрения зависимостей, должна выполняться в одном методе в приложении. Этот метод следует вызвать в начале жизненного цикла приложения, чтобы убедиться, что он знает о зависимостях между его классами. Приложения обычно выполняют это в методе CreateMauiApp MauiProgram в классе. Класс MauiProgram вызывает CreateMauiApp метод для создания MauiAppBuilder объекта. Объект MauiAppBuilder имеет Services свойство типа IServiceCollection, которое предоставляет место для регистрации типов, таких как представления, модели представления и службы для внедрения зависимостей:

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

Типы, зарегистрированные в свойстве Services , предоставляются контейнеру внедрения зависимостей при MauiAppBuilder.Build() вызове.

При регистрации зависимостей необходимо зарегистрировать все зависимости, включая все типы, требующие зависимостей. Таким образом, если у вас есть модель представления, которая принимает зависимость в качестве параметра конструктора, необходимо зарегистрировать модель представления вместе со всеми его зависимостями. Аналогичным образом, если у вас есть представление, которое принимает зависимость модели представления в качестве параметра конструктора, необходимо зарегистрировать представление и модель представления вместе со всеми его зависимостями.

Совет

Контейнер внедрения зависимостей идеально подходит для создания экземпляров модели представления. Если модель представления имеет зависимости, она будет управлять созданием и внедрением всех необходимых служб. Просто убедитесь, что вы регистрируете модели представления и все зависимости, которые они могут иметь в методе CreateMauiApp MauiProgram в классе.

В приложении Оболочки не нужно регистрировать страницы с контейнером внедрения зависимостей, если вы не хотите влиять на время существования страницы относительно контейнера с AddSingletonпомощью методов AddTransientили AddScoped методов. Дополнительные сведения см. в разделе о времени существования зависимостей.

Время существования зависимостей

В зависимости от потребностей приложения может потребоваться зарегистрировать зависимости с различными временем существования. В следующей таблице перечислены основные методы, которые можно использовать для регистрации зависимостей и их времени существования регистрации:

Метод Description
AddSingleton<T> Создает один экземпляр объекта, который останется в течение всего времени существования приложения.
AddTransient<T> Создает новый экземпляр объекта при запросе во время разрешения. Временные объекты не имеют предопределенного времени существования, но обычно следуют времени существования их узла.
AddScoped<T> Создает экземпляр объекта, который использует время существования узла. Когда узел выходит из области, это делает его зависимость. Таким образом, разрешение одной зависимости несколько раз в пределах одной области дает один и тот же экземпляр, а разрешение одной зависимости в разных областях приведет к получению разных экземпляров.

Примечание.

Если объект не наследуется от интерфейса, например представления или модели представления, необходимо указать AddSingleton<T>AddTransient<T>только конкретный тип объекта или AddScoped<T> метода.

Класс MainPageViewModel используется рядом с корнем приложения и всегда должен быть доступен, поэтому регистрация в ней AddSingleton<T> полезна. Другие модели представления могут быть в ситуации переходы к приложению или использоваться позже. Если у вас есть тип, который может не всегда использоваться, или если это память или вычислительный ресурсоемкий или требуется JIT-данные, это может быть лучшим кандидатом на AddTransient<T> регистрацию.

Другой распространенный способ регистрации зависимостей — использование AddSingleton<TService, TImplementation>AddTransient<TService, TImplementation>методов или AddScoped<TService, TImplementation> методов. Эти методы принимают два типа — определение интерфейса и конкретную реализацию. Этот тип регистрации лучше всего подходит для случаев, когда вы реализуете службы на основе интерфейсов.

После регистрации MauiAppBuilder.Build() всех типов необходимо вызвать для создания MauiApp объекта и заполнения контейнера внедрения зависимостей всеми зарегистрированными типами.

Внимание

После MauiAppBuilder.Build() вызова типы, зарегистрированные в контейнере внедрения зависимостей, будут неизменяемыми и больше не могут быть обновлены или изменены.

Регистрация зависимостей с помощью метода расширения

Метод MauiApp.CreateBuilder создает MauiAppBuilder объект, который можно использовать для регистрации зависимостей. Если приложению нужно зарегистрировать множество зависимостей, можно создать методы расширения, чтобы обеспечить упорядоченный и поддерживаемый рабочий процесс регистрации:

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

В этом примере три метода расширения регистрации используют MauiAppBuilder экземпляр для доступа к Services свойству для регистрации зависимостей.

Разрешение

После регистрации типа его можно разрешить или внедрить как зависимость. При разрешении типа и контейнеру необходимо создать новый экземпляр, он внедряет все зависимости в экземпляр.

Как правило, при разрешении типа происходит один из трех сценариев:

  1. Если тип не зарегистрирован, контейнер создает исключение.
  2. Если тип зарегистрирован в качестве одного, контейнер возвращает одноэлементный экземпляр. Если этот тип вызывается при первом вызове, контейнер создает его при необходимости и сохраняет ссылку на него.
  3. Если тип зарегистрирован как временный, контейнер возвращает новый экземпляр и не сохраняет ссылку на него.

.NET MAUI поддерживает автоматическое и явное разрешение зависимостей. Автоматическое разрешение зависимостей использует внедрение конструктора без явного запроса зависимости от контейнера. Явное разрешение зависимостей происходит по запросу путем явного запроса зависимости от контейнера.

Автоматическое разрешение зависимостей

Автоматическое разрешение зависимостей происходит в приложениях, использующих оболочку .NET MAUI, если вы зарегистрировали тип зависимости и тип, который использует зависимость с контейнером внедрения зависимостей.

Во время навигации на основе оболочки .NET MAUI будет искать регистрации страниц, и если они найдены, она создаст такую страницу и введет все зависимости в его конструктор:

public MainPage(MainPageViewModel viewModel)
{
    InitializeComponent();

    BindingContext = viewModel;
}

В этом примере MainPage конструктор получает внедренный MainPageViewModel экземпляр. В свою очередь экземпляр MainPageViewModel содержит ILoggingService и ISettingsService внедряется экземпляр:

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

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

Кроме того, в приложении на основе оболочки .NET MAUI внедряет зависимости в подробные страницы, зарегистрированные в методе Routing.RegisterRoute .

Явное разрешение зависимостей

Приложение на основе оболочки не может использовать внедрение конструктора, если тип предоставляет только конструктор без параметров. Кроме того, если приложение не использует оболочку, необходимо использовать явное разрешение зависимостей.

Контейнер внедрения зависимостей можно явно получить из Element свойства Handler.MauiContext.Service , которое имеет тип IServiceProvider:

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

        HandlerChanged += OnHandlerChanged;
    }

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

Этот подход может быть полезным, если необходимо устранить зависимость от Elementконструктора или вне конструктора Element. В этом примере доступ к контейнеру внедрения зависимостей в HandlerChanged обработчике событий гарантирует, что для страницы задан обработчик, поэтому Handler свойство не будет null.

Предупреждение

Свойство Handler вашего Element может быть null, поэтому помните, что вам может потребоваться учитывать эту ситуацию. Дополнительные сведения см. в разделе "Жизненный цикл обработчика".

В модели представления контейнер внедрения зависимостей можно явно получить через Handler.MauiContext.Service свойство 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>();
    }
}

В модели представления контейнер внедрения зависимостей можно явным образом получить через Handler.MauiContext.Service свойство :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>();
    }
}

Недостатком этого подхода является то, что модель представления теперь имеет зависимость от Application типа. Однако этот недостаток можно устранить, передав IServiceProvider аргумент конструктору модели представления. Решение IServiceProvider разрешается с помощью автоматического разрешения зависимостей без необходимости зарегистрировать его в контейнере внедрения зависимостей. При таком подходе тип и его IServiceProvider зависимость можно автоматически разрешить, если тип зарегистрирован в контейнере внедрения зависимостей. Затем IServiceProvider его можно использовать для явного разрешения зависимостей:

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

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

Кроме того, экземпляр можно получить на каждой IServiceProvider IPlatformApplication.Current.Services платформе через свойство.

Ограничения с ресурсами XAML

Распространенный сценарий заключается в регистрации страницы в контейнере внедрения зависимостей и автоматическом разрешении зависимостей для внедрения его в App конструктор и задания его в качестве значения MainPage свойства:

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

Распространенный сценарий заключается в регистрации страницы в контейнере внедрения зависимостей и автоматическом разрешении зависимостей для внедрения его в App конструктор и задания его в качестве первой страницы, отображаемой в приложении:

MyFirstAppPage _firstPage;

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

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

Однако в этом сценарии при MyFirstAppPage попытке получить доступ StaticResource к объекту, объявленному в XAML в App словаре ресурсов, XamlParseException создается сообщение, аналогичное Position {row}:{column}. StaticResource not found for key {key}сообщению. Это происходит из-за того, что страница, разрешенная путем внедрения конструктора, была создана до инициализации ресурсов XAML на уровне приложения.

Решение этой проблемы заключается в внедрении IServiceProvider App в класс, а затем использовать его для разрешения страницы внутри 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);
}

Этот подход заставляет дерево объектов XAML создаваться и инициализироваться перед разрешением страницы.