依存関係の挿入
.NET Multi-Platform App UI (.NET MAUI) では、依存関係の挿入の使用が組み込みでサポートされています。 依存関係の挿入は制御の反転 (IoC) パターンの特殊なバージョンであり、反転されるものは必要な依存関係を取得するプロセスです。 依存関係の挿入を使用すると、実行時に依存関係をオブジェクトに挿入する役割を別のクラスが担います。
通常、クラス コンストラクターは、オブジェクトをインスタンス化するときに呼び出され、オブジェクトに必要な値はすべて引数としてコンストラクターに渡されます。 これは、"コンストラクター挿入" と呼ばれる依存関係の挿入の一例です。 オブジェクトに必要な依存関係は、コンストラクターに挿入されます。
Note
依存関係の挿入には "プロパティ セッターの挿入" や "メソッド呼び出しの挿入" などの他の種類もありますが、あまり一般的ではありません。
依存関係をインターフェイス型として指定すると、依存関係の挿入により、具象型を、これらの型に依存するコードから分離できます。 通常、インターフェイスと抽象型の登録とそれらの間のマッピングの一覧、およびこれらの型を実装または拡張する具象型を保持するコンテナーが使用されます。
依存関係挿入コンテナー
あるクラスが必要なオブジェクトを直接インスタンス化しない場合、別のクラスがこの責任を負う必要があります。 コンストラクター引数を必要とするビュー モデル クラスを示す次の例を考えてみます。
public class MainPageViewModel
{
readonly ILoggingService _loggingService;
readonly ISettingsService _settingsService;
public MainPageViewModel(ILoggingService loggingService, ISettingsService settingsService)
{
_loggingService = loggingService;
_settingsService = settingsService;
}
}
この例の MainPageViewModel
コンストラクターには、別のクラスによって挿入される引数として 2 つのインターフェイス オブジェクト インスタンスが必要です。 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 での依存関係の挿入」を参照してください。
コンテナーは、実行時に、要求されたオブジェクトに対する依存関係をインスタンス化するため、依存関係のどの実装が要求されているかを知る必要があります。 上の例では、MainPageViewModel
オブジェクトをインスタンス化する前に、ILoggingService
と ISettingsService
インターフェイスを解決する必要があります。 このためには、次のアクションを実行するコンテナーが必要です。
- インターフェイスを実装するオブジェクトのインスタンス化方法の決定。 これは "登録" と呼ばれます。 詳細については、「登録」を参照してください。
- 必要なインターフェイスと
MainPageViewModel
オブジェクトを実装するオブジェクトのインスタンス化。 これは "解決" と呼ばれます。 詳しくは、「解決」をご覧ください。
最終的に、アプリは MainPageViewModel
オブジェクトを使い終わり、ガベージ コレクションで使用できるようになります。 この時点で、有効期間の短いインターフェイスの実装は、他のクラスが同じインスタンスを共有していない場合、ガベージ コレクターによって破棄される必要があります。
登録
依存関係をオブジェクトに挿入するには、その前にまず、依存関係の型をコンテナーに登録する必要があります。 型を登録するには、通常、具象型、またはインターフェイスとインターフェイスを実装する具象型を、コンテナーに渡す必要があります。
型とオブジェクトをコンテナーに登録するには、主に 2 つの方法があります。
- 型またはマッピングをコンテナーに登録します。 これは、一時的な登録と呼ばれます。 必要に応じて、コンテナーにより、指定された型のインスタンスが構築されます。
- コンテナー内の既存のオブジェクトをシングルトンとして登録します。 必要に応じて、コンテナーにより、既存のオブジェクトへの参照が返されます。
注意事項
依存関係挿入コンテナーは、.NET MAUI アプリに常に適しているわけではありません。 依存関係の挿入は、複雑さと要件が増し、小さなアプリには適さない場合や役に立たない可能性があります。 クラスに依存関係がない場合、またはクラスが他の型の依存関係でない場合は、コンテナーに配置しても意味がない可能性があります。 さらに、型にとって不可欠で変化しない依存関係の単一のセットがクラスにある場合は、それらをコンテナーに配置しても意味がない可能性があります。
依存関係の挿入を必要とする型の登録は、アプリ内の 1 つのメソッドで実行する必要があります。 アプリのライフサイクルの早い段階でこのメソッドを呼び出して、クラス間の依存関係が確実に認識されるようにする必要があります。 アプリでは、通常、MauiProgram
クラスの CreateMauiApp
メソッドでこれを実行する必要があります。 MauiProgram
クラスは、CreateMauiApp
メソッドを呼び出して MauiAppBuilder オブジェクトを作成します。 MauiAppBuilder オブジェクトには IServiceCollection 型の Services プロパティがあり、依存関係の挿入に対するビュー、ビュー モデル、サービスなどの型を登録する場所が提供されます。
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() が呼び出されると依存関係挿入コンテナーに提供されます。
依存関係を登録するときは、依存関係を必要とする型を含むすべての依存関係を登録する必要があります。 したがって、依存関係をコンストラクター パラメーターとして受け取るビュー モデルがある場合は、ビュー モデルとそのすべての依存関係を登録する必要があります。 同様に、ビュー モデルの依存関係をコンストラクター パラメーターとして受け取るビューがある場合は、ビューおよびビュー モデルとそのすべての依存関係を登録する必要があります。
ヒント
依存関係挿入コンテナーは、ビュー モデル インスタンスの作成に最適です。 ビュー モデルは、依存関係がある場合、必要なすべてのサービスの作成と挿入を管理します。 ユーザーは、MauiProgram
クラスの CreateMauiApp
メソッドで、ビュー モデルとそれが持つ可能性のある依存関係を登録することだけが必要です。
シェル アプリでは、 AddSingleton
、 AddTransient
、または AddScoped
メソッドを使用して、コンテナーに対するページの有効期間に影響を与える必要がない限り、ページを依存関係挿入コンテナーに登録する必要はありません。 詳細については、「 Dependency lifetime」を参照してください。
依存関係の有効期間
アプリのニーズによっては、異なる有効期間で依存関係を登録することが必要になる場合があります。 次の表は、依存関係の登録に使用できる主な方法と、その登録の有効期間を示したものです。
Method | 説明 |
---|---|
AddSingleton<T> |
アプリの有効期間を通して維持される、オブジェクトの 1 つのインスタンスを作成します。 |
AddTransient<T> |
解決の間に要求された時点で、オブジェクトの新しいインスタンスを作成します。 一時的なオブジェクトの有効期間は事前に定義されませんが、通常、ホストの有効期間に従います。 |
AddScoped<T> |
ホストの有効期間を共有する、オブジェクトのインスタンスを作成します。 ホストがスコープ外になると、その依存関係もそうなります。 そのため、同じスコープ内で同じ依存関係を複数回解決すると、同じインスタンスが生成されますが、異なるスコープ内で同じ依存関係を解決すると、異なるインスタンスが生成されます。 |
Note
オブジェクトがビューやビュー モデルなどのインターフェイスを継承していない場合、AddSingleton<T>
、AddTransient<T>
、または AddScoped<T>
メソッドにはその具象型のみを提供する必要があります。
MainPageViewModel
クラスは、アプリのルートの近くで使われ、常に使用可能である必要があるため、AddSingleton<T>
で登録すると便利です。 他のビュー モデルは、状況に応じて、アプリで後の方に移動したり、後で使ったりできます。 型が、常に使われるとは限らない場合、メモリや計算を多量に使う場合、または Just-In-Time データを必要とする場合は、AddTransient<T>
登録のよい候補である可能性があります。
依存関係を登録するもう 1 つの一般的な方法は、AddSingleton<TService, TImplementation>
、AddTransient<TService, TImplementation>
、または AddScoped<TService, TImplementation>
メソッドを使うことです。 これらのメソッドは、インターフェイス定義と具象実装の 2 つの型を受け取ります。 この種類の登録は、インターフェイスに基づいてサービスを実装する場合に最適です。
すべての型が登録されたら、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;
}
}
この例の 3 つの登録拡張メソッドは、MauiAppBuilder インスタンスを使って Services プロパティにアクセスして、依存関係を登録します。
解決方法
型は、登録された後、依存関係として解決または挿入できます。 型が解決され、コンテナーで新しいインスタンスを作成する必要がある場合、そのインスタンスに依存関係が挿入されます。
一般に、型が解決されるときは、次の 3 つのシナリオのいずれかが発生します。
- 型が登録されていない場合、コンテナーによって例外がスローされます。
- 型がシングルトンとして登録されていない場合、コンテナーによってシングルトン インスタンスが返されます。 型が呼び出されるのがこれが初めてである場合、コンテナーでは必要に応じてそれを作成し、それへの参照を保持します。
- 型が一時的として登録されている場合、コンテナーによって新しいインスタンスが返され、それに対する参照は保持されません。
.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;
}
}
さらに、シェル ベースのアプリでは、.NET MAUI は Routing.RegisterRoute メソッドに登録されている詳細ページに依存関係を挿入します。
明示的な依存関係の解決
型でパラメーターのないコンストラクターだけが公開されている場合、シェル ベースのアプリではコンストラクターの挿入を使用できません。 または、アプリでシェルを使っていない場合は、明示的な依存関係の解決を使う必要があります。
依存関係挿入コンテナーには、IServiceProvider 型の Handler.MauiContext.Service
プロパティを使って、Element から明示的にアクセスできます。
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
にならないことが保証されます。
警告
Element
の Handler
プロパティは 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>();
}
}
さらに、各プラットフォームで IPlatformApplication.Current.Services
プロパティを使って IServiceProvider インスタンスにアクセスできます。
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);
}
ただし、このシナリオでは、App
リソース ディクショナリの XAML で宣言されている StaticResource
に MyFirstAppPage
がアクセスしようとすると、Position {row}:{column}. StaticResource not found for key {key}
のようなメッセージを含む XamlParseException がスローされます。 これが発生するのは、アプリケーション レベルの XAML リソースが初期化される前に、コンストラクターの挿入によって解決されたページが作成されるためです。
この問題を回避するには、App
クラスに IServiceProvider を挿入してから、それを使って 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 オブジェクト ツリーが強制的に作成および初期化されます。
.NET MAUI