相依性插入

提示

本內容節錄自《Enterprise Application Patterns Using .NET MAUI》電子書,可以從 .NET Docs 取得,也可以免費下載 PDF 離線閱讀。

Enterprise Application Patterns Using .NET MAUI eBook cover thumbnail.

一般而言,具現化物件時會叫用類別建構函式,並將物件需要的任何值作為引數傳遞至建構函式。 此為相依性插入範例,稱為建構函式插入。 物件所需的相依性會插入建構函式。

相依性插入會將相依性指定為介面類型,而可使具體類型及其相依的程式碼分開。 這項技術通常會使用容器來保存介面和抽象類型之間的登錄和對應清單,以及實作或擴充這些類型的具體類型。

另外還有其他類型的相依性插入,例如屬性 setter 插入方法呼叫插入,但較不常見。 因此,本章只著重於使用相依性插入容器來執行建構函式插入。

相依性插入簡介

相依性插入是控制反轉 (IoC) 模式的特殊版本,而反轉的考量是取得所需相依性的處理序。 使用相依性插入時,另一個類別負責在執行階段將相依性插入物件。 下列程式碼範例示範如何使用相依性插入來建構 ProfileViewModel 類別:

private readonly ISettingsService _settingsService;
private readonly IAppEnvironmentService _appEnvironmentService;

public ProfileViewModel(
    IAppEnvironmentService appEnvironmentService,
    IDialogService dialogService, 
    INavigationService navigationService, 
    ISettingsService settingsService)
    : base(dialogService, navigationService, settingsService)
{
    _appEnvironmentService = appEnvironmentService;
    _settingsService = settingsService;

    // Omitted for brevity
}

ProfileViewModel 建構函式會接收多個介面物件執行個體,作為另一個類別所插入的引數。 ProfileViewModel 類別中唯一的相依性是介面類型。 因此,對於負責具現化介面物件的類別,ProfileViewModel 類別完全不具任何知識。 負責具現化介面物件並插入 ProfileViewModel 類別的類別,稱為相依性插入容器

相依性插入容器可提供一項功能來具現化類別執行個體,並根據容器組態管理其存留期,以便減少物件間的結合程度。 在物件建立期間,該容器會插入物件所需的任何相依性。 若尚未建立這些相依性,容器則會先建立並解析其相依性。

使用相依性插入容器有幾項優點:

  • 容器不需要類別即可找出相依性,並管理其存留期。
  • 容器允許對應實作的相依性,而不影響類別。
  • 容器允許模擬相依性,有助於提升測試性。
  • 容器允許應用程式輕鬆新增類別,以增加可維護性。

在使用 MVVM 的 .NET MAUI 應用程式情境中,相依性插入容器通常用於登錄和解析檢視、登錄和解析檢視模型,以及登錄服務和插入檢視模型。

.NET 中有許多可用的相依性插入容器;eShopOnContainers 多平台應用程式使用 Microsoft.Extensions.DependencyInjection 來管理應用程式中檢視、檢視模型和服務類別的具現化。 Microsoft.Extensions.DependencyInjection 有助於建置以鬆散方式結合的應用程式,並提供相依性插入容器中的所有常見功能,包含登錄類型對應和物件執行個體、解析物件、管理物件存留期,以及在建構函式中將相依物件插入解析的物件等多種方法。 如需 Microsoft.Extensions.DependencyInjection 的詳細資訊,請參閱 .NET 中的相依性插入

在 .NET MAUI 中,MauiProgram 類別會呼叫 CreateMauiApp 方法以建立 MauiAppBuilder 物件。 MauiAppBuilder 物件具有 IServiceCollection 類型的 Services 屬性,可提供登錄元件的位置,例如相依性插入的檢視、檢視模型和服務。 呼叫 MauiAppBuilder.Build 方法時,系統會提供已登錄 Services 屬性的所有元件給相依性插入容器。

在執行階段,容器必須知道所要求的服務實作,才能為要求的物件具現化這些實作。 在 eShopOnContainers 多平台應用程式中,必須先解析 IAppEnvironmentServiceIDialogServiceINavigationServiceISettingsService 介面,應用程式才能具現化 ProfileViewModel 物件。 這牽涉到執行下列動作的容器:

  • 決定實作介面的物件具現化方式。 這稱為登錄
  • 將實作必要介面及 ProfileViewModel 物件的物件具現化。 這稱為解析

最後完成時,應用程式會使用 ProfileViewModel 物件,並可供記憶體回收。 此時,若其他類別未共用相同的執行個體,記憶體回收行程應處置所有短期的介面實作。

註冊

必須先登錄容器的相依性類型,才能將相依性插入物件。 類型的登錄動作涉及將介面傳送給容器,以及實作該介面的具體類型。

在容器中透過程式碼登錄類型和物件有兩種方式:

  • 登錄容器的類型或對應。 這稱為暫時性登錄。 容器會視需要建置指定類型的執行個體。
  • 將容器中的現有物件登錄為單一項目。 容器會視需要傳回現有物件的參考。

注意

相依性插入容器不一定適合。 相依性插入加入的額外的複雜度和需求,對小型應用程式而言可能不適合或實用。 若某類別沒有任何相依性,或不是其他類型的相依性,則放置於容器可能不合理。 此外,若類別具有單一個相依性集合,而這些相依性對該類型不可或缺、且永不變更,則放置於容器可能不合理。

需要相依性插入的類型,應在應用程式中的單一方法中執行登錄。 此方法應在應用程式的生命週期早期叫用,以確保該應用程式得知類別間的相依性。 eShopOnContainers 多平台應用程式會執行此方法 MauiProgram.CreateMauiApp。 下列程式碼範例示範 eShopOnContainers 多平台應用程式如何在 MauiProgram 類別中宣告 CreateMauiApp

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
        => MauiApp.CreateBuilder()
            .UseMauiApp<App>()
            // Omitted for brevity            
            .RegisterAppServices()
            .RegisterViewModels()
            .RegisterViews()
            .Build();
}

MauiApp.CreateBuilder 方法會建立 MauiAppBuilder 物件,可用於登錄相依性。 eShopOnContainers 多平台應用程式中的許多相依性必須進行登錄,因此已建立擴充方法 RegisterAppServicesRegisterViewModelsRegisterViews,以便提供結構清晰、可供維護的登錄工作流程。 下列程式碼顯示 RegisterViewModels 方法:

public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder)
{
    mauiAppBuilder.Services.AddSingleton<ViewModels.MainViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.LoginViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.BasketViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.CatalogViewModel>();
    mauiAppBuilder.Services.AddSingleton<ViewModels.ProfileViewModel>();

    mauiAppBuilder.Services.AddTransient<ViewModels.CheckoutViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.OrderDetailViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.SettingsViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.CampaignViewModel>();
    mauiAppBuilder.Services.AddTransient<ViewModels.CampaignDetailsViewModel>();

    return mauiAppBuilder;
}

此方法會接收 MauiAppBuilder 的執行個體,我們可使用 Services 屬性來登錄檢視模型。 視應用程式的需求而定,您可能需要新增不同存留期的服務。 針對這些不同登錄存留期的選擇時機,下表提供相關資訊:

方法 描述
AddSingleton<T> 將會建立該物件的單一執行個體,並保留在應用程式存留期內。
AddTransient<T> 在解析期間進行要求時,將會建立物件的新執行個體。 暫時性物件沒有預先定義的存留期,但通常遵循其主控項目的存留期。

注意

檢視模型不會從介面繼承,因此只需要提供給 AddSingleton<T>AddTransient<T> 方法的具體類型。

CatalogViewModel 的使用位置靠近應用程式根目錄,且應一律可供使用,因此使用 AddSingleton<T> 進行登錄會有所助益。 CheckoutViewModelOrderDetailViewModel 等其他檢視模型,則會視情況瀏覽或稍後用於應用程式。 假設您知道有某個元件可能並非一律會使用。 在此情況下,若需要大量記憶體或計算、或需要 Just-In-Time 資料,則可能是較適合 AddTransient<T> 登錄的選擇。

另一個常見的新增服務方式是使用 AddSingleton<TService, TImplementation>AddTransient<TService, TImplementation> 方法。 這些方法採用兩個輸入類型:介面定義和具體實作。 此登錄類型最適合用於根據介面實作服務的情況。 在下列程式碼範例中,我們會使用 SettingsService 實作來登錄 ISettingsService 介面:

public static MauiAppBuilder RegisterAppServices(this MauiAppBuilder mauiAppBuilder)
{
    mauiAppBuilder.Services.AddSingleton<ISettingsService, SettingsService>();
    // Omitted for brevity...
}

一旦登錄所有服務後,應呼叫MauiAppBuilder.Build 方法來建立 MauiApp 並在相依性插入容器中填入所有已登錄的服務。

重要

呼叫 Build 方法後,登錄相依性插入容器的服務則為不可變,無法再更新或修改。

解決方法

當類型登錄後,則可解析或插入為相依性。 當類型正在進行解析,且容器必須建立新執行個體時,便會在執行個體中插入所有相依性。

已解析類型時,通常會發生下列三個狀況之一:

  1. 若類型尚未登錄,容器會擲回例外狀況。
  2. 若類型已登錄為單一項目,容器會傳回單一執行個體。 若這是第一次呼叫類型,容器會視需要建立該類型,並維護其參考。
  3. 若類型已登錄為暫時性,容器則會傳回新的執行個體,且不會維護其參考。

.NET MAUI 提供數種方式,根據您的需求解析登錄的元件。 最直接的方式,即是使用 Handler.MauiContext.ServicesElement 取得相依性插入容器的存取權。 範例如下所示:

var settingsService = this.Handler.MauiContext.Services.GetServices<ISettingsService>();

若您必須Element 內部或 Element 的建構函式外部解析服務,此方式可能相當實用。

警告

您的 Element 屬性 Handler 可能為 Null,因此請留意有可能必須處理這些情況。 如需詳細資訊,請參閱 Microsoft 文件中心的處理常式生命週期

若使用 .NET MAUI 的 Shell 控制項,將會以隱含方式呼叫相依性插入容器,以便在瀏覽期間建立物件。 設定我們的 Shell 控制項時,Routing.RegisterRoute 方法會將路由路徑繫結至 View,如下列範例所示:

Routing.RegisterRoute("Filter", typeof(FiltersView));

Shell 瀏覽期間,其會尋找 FiltersView 的登錄,若找到任何登錄,便會建立該檢視,並將任何相依性插入建構函式。 如下列程式碼範例所示,CatalogViewModel 將會插入 FiltersView

namespace eShopOnContainers.Views;

public partial class FiltersView : ContentPage
{
    public FiltersView(CatalogViewModel viewModel)
    {
        BindingContext = viewModel;

        InitializeComponent();
    }
}

提示

相依性插入容器適合用於建立檢視模型執行個體。 若檢視模型具有相依性,該模型便會負責建立和插入任何所需服務。 只要確定您登錄檢視模型,以及模型在 MauiProgram 類別中使用 CreateMauiApp 方法的任何可能相依性即可。

摘要

相依性插入可使具體類型及其相依的程式碼分開。 這項技術通常會使用容器來保存介面和抽象類型之間的登錄和對應清單,以及實作或擴充這些類型的具體類型。

Microsoft.Extensions.DependencyInjection 有助於建置以鬆散方式結合的應用程式,並提供相依性插入容器中的所有常見功能,包含登錄類型對應和物件執行個體、解析物件、管理物件存留期,以及在建構函式中將相依物件插入解析的物件等多種方法。