相依性插入
.NET 多平台應用程式 UI (.NET MAUI) 提供使用相依性插入的內建支援。 相依性插入是控制反轉模式的特殊版本,其中反轉的關注是取得所需相依性的程式。 使用相依性插入時,另一個類別負責在執行階段將相依性插入物件。
一般而言,具現化物件時會叫用類別建構函式,並將物件需要的任何值作為引數傳遞至建構函式。 此為相依性插入範例,稱為建構函式插入。 物件所需的相依性會插入建構函式。
注意
也有其他類型的相依性插入,例如 屬性 setter 插入 和 方法呼叫插入,但較不常使用。
相依性插入會將相依性指定為介面類型,而可使具體類型及其相依的程式碼分開。 這項技術通常會使用容器來保存介面和抽象類型之間的登錄和對應清單,以及實作或擴充這些類型的具體類型。
相依性插入容器
如果類別未直接具現化它所需的物件,另一個類別必須承擔此責任。 請考慮下列範例,其中顯示需要建構函式自變數的檢視模型類別:
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
對負責具現化具體類型的類別沒有任何知識。
在這兩種情況下,負責具現化相依性的類別,並將其插入相依類別,稱為 相依性插入容器。
相依性插入容器可提供一項功能來具現化類別執行個體,並根據容器組態管理其存留期,以便減少物件間的結合程度。 在物件建立期間,容器會插入物件所需的任何相依性。 如果尚未建立這些相依性,容器會先建立並解析其相依性。
使用相依性插入容器有幾項優點:
- 容器不需要類別即可找出相依性,並管理其存留期。
- 容器允許對應實作的相依性,而不影響類別。
- 容器允許模擬相依性,有助於提升測試性。
- 容器允許應用程式輕鬆新增類別,以增加可維護性。
在使用 Model-View-ViewModel (MVVM) 模式的 .NET MAUI 應用程式內容中,相依性插入容器通常用於註冊和解析檢視、註冊和解析檢視模型,以及註冊服務,並將其插入檢視模型。 如需MVVM模式的詳細資訊,請參閱 Model-View-ViewModel (MVVM) 。
有許多適用於 .NET 的相依性插入容器。 .NET MAUI 有內建支援,可用來 Microsoft.Extensions.DependencyInjection 管理應用程式中檢視、檢視模型和服務類別的具現化。 Microsoft.Extensions.DependencyInjection 有助於建置以鬆散方式結合的應用程式,並提供相依性插入容器中的所有常見功能,包含登錄類型對應和物件執行個體、解析物件、管理物件存留期,以及在建構函式中將相依物件插入解析的物件等多種方法。 如需 Microsoft.Extensions.DependencyInjection 的詳細資訊,請參閱 .NET 中的相依性插入。
在運行時間,容器必須知道要求相依性的實作,才能為要求的物件具現化它們。 在上述範例中, ILoggingService
必須先解析 和 ISettingsService
介面, MainPageViewModel
才能具現化物件。 這牽涉到執行下列動作的容器:
最後,應用程式會使用 MainPageViewModel
物件完成,而且會變成可供垃圾收集使用。 此時,如果其他類別不共用相同的實例,垃圾收集行程應該處置任何短期介面實作。
註冊
必須先向容器註冊相依性,才能將相依性插入物件中。 註冊類型通常牽涉到傳遞容器具象類型,或是實作介面的介面和具象類型。
使用容器註冊類型和物件有兩個主要方法:
- 登錄容器的類型或對應。 這稱為暫時性登錄。 容器會視需要建置指定類型的執行個體。
- 將容器中的現有物件登錄為單一項目。 容器會視需要傳回現有物件的參考。
警告
相依性插入容器不一定適用於 .NET MAUI 應用程式。 相依性插入引進其他複雜度和需求,這些需求可能不適合或對較小的應用程式有用。 如果類別沒有任何相依性,或不是其他類型的相依性,將它放在容器中可能沒有意義。 此外,如果類別有單一相依性集合,這些相依性是類型不可或缺的一組,而且永遠不會變更,則將相依性放在容器中可能沒有意義。
需要相依性插入的類型註冊應該在應用程式中的單一方法中執行。 這個方法應該在應用程式的生命週期早期叫用,以確保它知道其類別之間的相依性。 應用程式通常會在類別的 MauiProgram
方法中CreateMauiApp
執行此動作。 類別 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();
}
}
呼叫 時MauiAppBuilder.Build(),向屬性註冊的類型Services會提供給相依性插入容器。
註冊相依性時,您必須註冊所有相依性,包括包含任何需要相依性的類型。 因此,如果您有採用相依性作為建構函式參數的檢視模型,您必須註冊檢視模型及其所有相依性。 同樣地,如果您有檢視以檢視模型相依性作為建構函式參數的檢視,則需要註冊檢視,以及檢視模型及其所有相依性。
提示
相依性插入容器很適合用來建立檢視模型實例。 如果檢視模型具有相依性,它會管理任何必要服務的建立和插入。 只要確定您註冊檢視模型,以及類別中 方法中CreateMauiApp
MauiProgram
可能具有的任何相依性即可。
在Shell應用程式中,除非您想要使用、 AddTransient
或 AddScoped
方法影響頁面相對於容器AddSingleton
的存留期,否則您不需要向相依性插入容器註冊頁面。 如需詳細資訊,請參閱 相依性存留期。
相依性存留期
視應用程式的需求而定,您可能需要向不同的存留期註冊相依性。 下表列出可用來註冊相依性的主要方法,以及其註冊存留期:
方法 | 描述 |
---|---|
AddSingleton<T> |
建立物件的單一實例,此實例將保留應用程式存留期。 |
AddTransient<T> |
在解析期間要求時,建立 物件的新實例。 暫時性物件沒有預先定義的存留期,但通常遵循其主控項目的存留期。 |
AddScoped<T> |
建立共用其主機存留期之 對象的實例。 當主機超出範圍時,其相依性也是如此。 因此,在相同範圍內解析相同相依性多次會產生相同的實例,而在不同的範圍中解析相同的相依性會產生不同的實例。 |
注意
如果物件未繼承自介面,例如檢視或檢視模型,則只需要將其具體類型提供給 AddSingleton<T>
、 AddTransient<T>
或 AddScoped<T>
方法。
類別 MainPageViewModel
會在應用程式的根目錄附近使用,而且應該一律可供使用,因此向 註冊類別 AddSingleton<T>
會很有説明。 其他檢視模型可能會在應用程式中流覽至或稍後使用。 如果您有可能不一定會使用的型別,或是記憶體或需要大量計算,或需要 Just-In-Time 數據,可能是較適合註冊的候選專案 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 屬性來註冊相依性。
解決方法
當類型登錄後,則可解析或插入為相依性。 當類型正在進行解析,且容器必須建立新執行個體時,便會在執行個體中插入所有相依性。
一般而言,當類型解析時,會發生下列三種案例之一:
- 若類型尚未登錄,容器會擲回例外狀況。
- 若類型已登錄為單一項目,容器會傳回單一執行個體。 若這是第一次呼叫類型,容器會視需要建立該類型,並維護其參考。
- 若類型已登錄為暫時性,容器則會傳回新的執行個體,且不會維護其參考。
.NET MAUI 支援 自動 和 明確的 相依性解析。 自動相依性解析會使用建構函式插入,而不需要從容器明確要求相依性。 明確要求容器的相依性時,會視需要進行明確相依性解析。
自動相依性解析
自動相依性解析會在使用 .NET MAUI Shell 的應用程式中發生,前提是您已註冊相依性的類型,以及搭配相依性插入容器使用相依性的類型。
在殼層型導覽期間,.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;
}
}
此外,在Shell型應用程式中,.NET MAUI 會將相依性插入至向 Routing.RegisterRoute 方法註冊的詳細數據頁面。
明確相依性解析
當類型只公開無參數建構函式時,殼層型應用程式無法使用建構函式插入。 或者,如果您的應用程式未使用Shell,則必須使用明確的相依性解析。
您可以透過其 Handler.MauiContext.Service
屬性,從明確存取Element相依性插入容器,其類型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
,因此請注意,您可能需要考慮這種情況。 如需詳細資訊,請參閱 處理程式生命週期。
在檢視模型中,可以透過的Application.Current.MainPage
屬性明確存取Handler.MauiContext.Service
相依性插入容器:
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>();
}
}
在檢視模型中,可以透過的Window.Page
屬性明確存取Handler.MauiContext.Service
相依性插入容器:
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 物件樹狀結構。