Prism の DI コンテナらは Ioc 上に歌う【step: 4 .NET Core WPF Prism MVVM 入門 2020】

前回記事「Prism はじめました【step: 3 .NET Core WPF Prism MVVM 入門 2020】」

 

前回は Prism Template Pack から WPF アプリプロジェクトを作成して、MainWindow を表示するまでの最低限の手順を紹介しました。今回は Prism に組み込まれる DI コンテナについて紹介します。

尚、この記事は Visual Studio 2019 Community Edition で .NET Core 3.1 以上 と C# + Prism + MahApps.Metro + Material Design In XAML Toolkit を使用して、WPF アプリケーションを MVVM パターンで作成するのが目的なので、C# の文法や基本的なコーディング知識を持っている人が対象です。

Prism と DI コンテナ

前回までにも何度か紹介した通り、Prism は画面を構成する UI 部品を Module と呼ばれる独立した複数のプロジェクトに分割して作成するための複合アプリケーション作成用フレームワークと言う側面も持ちます。

疎結合な関係で作成された個々の Module は実行時に Prism が動的に結合します。各 Module の結合にはデザインパターンの DI(Dependency injection:依存性の注入)が採用されていて、DI の実行には DI コンテナが利用されています。
つまり、DI とはデザインパターンを指し、DI コンテナとは DI を実行するためのライブラリを表します。

Prism アプリで使用する DI コンテナ

Prism Template Pack から作成したアプリ内で使用される DI コンテナは前回紹介した通り、fig. 1 の【Select Container ダイアログ】で選択した DI コンテナが使用されるようにプロジェクトが設定されます。

fig.1 Select Container ダイアログ

Select Container ダイアログで選択した DI コンテナ(例えば Unity)を後から別の DI コンテナ(例えば dryIoc)に変更することは基本的にできないと思ってください。方法が全く無いとは言いませんが、おそらくかなり面倒な作業になりそうなのは予想できるので、変更したい場合は Prism Template Pack から再作成し直すのが最も簡単だと思います。

Prism は Ver. 7.2 の破壊的変更で DI コンテナが抽象化されたため、アプリから DI コンテナを操作するには抽象化されたインタフェースを経由しないとアクセスできないように変更されました。そのため普通にアプリを作成するだけなら基本的にはどちらの DI コンテナを選択しても操作方法は変わりません。
各 DI コンテナ固有の機能にアクセスする事ができない訳ではありませんが、このエントリでは取り扱いません。

そして、現時点(2020/8)では最新の Prism Ver. 7.2 SR1 で選択可能な Unity と DryIoc それぞれの特徴を簡単に紹介します。

DI コンテナ Unity

Unity は某ゲームエンジンと名前は同じですが、全く関係ありません。

Unity も Prism と同じく Microsoft Patterns and Practices で開発された DI コンテナで、GitHub の ReadMe.md を要約すると以下のような特徴を持つようです。

Unity コンテナー(Unity)は、軽量で拡張可能な依存関係注入コンテナーです。疎結合アプリケーションの構築を容易にし、開発者に次の利点を提供します。unitycontainer

Prism Ver. 7.2 SR1 に含まれる Unity のバージョンは Ver. 5.11.1 です。



DI コンテナ DryIoc

DryIoc は GitHub で公開されている OSS の DIコンテナです。

管理人は最初『ドライロック』だと勘違い(I(アイ)を l(小文字のエル)だと思っていました)していましたが、実際は『ドライ・アイ・オー・シー』が正しい名称だと思います。DryIoc は GitHub の ReadMe.md を要約すると以下のような特徴を持つようです。

DryIoc は .NET Framewok、.NET Core で動作する高速で小型のフル機能を備えた .NET 用 IoC コンテナーです。DryIoc ReadMe.md

Prism Ver. 7.2 SR1 に含まれる DryIoc のバージョンは Ver. 4.0.7 です。

パフォーマンスの比較

Prism から DI コンテナを使用する場合、Unity と DryIoc で機能的な違いを感じることは無いので、パフォーマンスを比較します。DryIoc のブログで丁度 DI コンテナのベンチマーク比較 が公開されていたので、そこから DryIoc と Unity を抜粋しました。

このベンチマークは、各 DI コンテナから以下のタイプのクラスを 500,000 回取得(Resolve)した場合の時間をミリ秒単位で計測した結果で、上の数値はシングルスレッド、下の数値はマルチスレッドでの処理結果のようです。
※ 以下のタイプに付けた説明は Web 翻訳から類推した文章なので実際とは違う場合もあるかもしれません

  • Singleton:シングルトンオブジェクト
  • Transient:生存期間が一時的なオブジェクト
  • Combined:2 つの依存関係(Singleton と Transient)を持つオブジェクト。
  • Complex:いくつかのネストされた依存関係を持つオブジェクト
  • Generics:ジェネリックな依存関係を持つオブジェクト
  • IEnumerable:IEnumerable を実装したオブジェクト
  • Conditional:条件付きの依存関係を持つオブジェクト

※ 『No』は DI コンテナ無しを表します

ContainerSingletonTransientCombinedComplexGenericsIEnumerableConditional
No41
49
49
59
69
76
99
103
70
75
193
176
53
63
DryIoc 4.0.736
49
56
79
57
102
76
89
58
83
326
232
58
78
Unity 5.11.1231
160
1598
926
3599
1995
8365
4647
9191
5255
15421
8702
3319
1861

上記記事の公開日は 2011/8/30 ですが、随時更新されているようで上記のベンチマークは 2020/2/2 に更新した計測結果です。丁度 Prism Ver. 7.2 SR1 に同梱されている Unity と DryIoc と同じバージョンのベンチマークなので比較には最適です。

単純にベンチマークの結果だけを見ると DryIoc の方が圧倒的に高速なので、パフォーマンスを重視する場合は DryIoc を選択しても構わないと思いますが、上記の計測結果はあくまでも DI コンテナ単体のベンチマークなので Prism から使用する場合は必ずしも数値通りの結果が得られる訳ではありません。

このように Prism 組み込みの DI コンテナはパフォーマンスくらいしか違いが無いので、単純にパフォーマンスが良いと言う理由だけで選んでも構わないと思います。管理人が最近作成したサンプルのほとんどは DryIoc を選択しています。(連載記事の MVVM L@bo や WPF UI Gallery 等のサンプルは DryIoc を使用)

ですが、このサイトには DI、Unity 等の検索ワードで来られる方も多いようなので、この連載では DI コンテナに Unity を選択しますが、特に指定が無い場合、Unity、DryIoc のどちらを使用している場合でも記述は変わりません。

次章はデザインパターンの DI について紹介します。

デザインパターンの Dependency Injection(DI)とは

DI とは Dependency Injection の頭文字で日本語では『依存性の注入』と訳されることの多いソフトウェア開発パターンの 1 つです。正しい日本語訳は『依存オブジェクトの注入』だと言う意見も見かけますし、管理人的にも動作をイメージし易いのは『依存オブジェクトの注入』の方だと思います。

DI をざっくり言うと『あるクラス(クラス A)のメソッド内で別クラス(クラス B)のインスタンスを作成しているような場合は、外部からクラス B のインスタンスを注入する事で依存度を下げる』ためのデザインパターンです。
以降では DI のサンプルコードを紹介しますが、以下の前提で書いています。

  • サンプルコードに登場するクラスは特に指定が無い限り、全て MVVM パターンの Model に属するクラスです。
  • Model は単純な三層構造で設計していて、『アプリケーションロジック層』と『データアクセス層』があります。

Model に三層構造を適用する例は WPF MVVM L@bo #4 で詳しく紹介しています。

まず src. 1 は DI を使用しない場合のサンプルコードです。

src. 1 の 25 行目で PersonRepository を new しているため、DataAgent は PersonRepository に依存していることになり、加えて PersonRepository クラスを直接 new しているため、例えばデータの取得先を XML ファイルから DB に変更したい場合は容易でないとも言えます。

DI を適用

DataAgent クラスに DI を適用すると src. 2 のようになります。

src. 2 では DataAgent 内部で生成していた PersonRepository をインタフェースに変更して、外部からインスタンスを注入するように変更しています。このように依存度の高いオブジェクトを外部から注入することでクラス同士を疎結合にするパターンが DI です。ここでは依存クラスをコンストラクタから注入するコンストラクタインジェクションを適用していますが、他にもメソッドインジェクションやプロパティインジェクション等も選択できます。

このように DI 自体は大して難くないと思いますが、かなり致命的な問題が 1 つ出て来ます。問題なのは UI 部の VM 等から DataAgent を呼び出す部分で、通常は src. 3 のようにインスタンスを生成すると思います。

src. 3 の問題は 12 行目です。前提の通り Model は三層構造で作成するため、通常は fig. 2 のような参照関係で作成します。
※ NoDiCommand はボタンクリックを通知する ICommand ですが次回以降のエントリで詳しく紹介します

fig.2 三層構造の参照関係

つまり、プレゼンテーション層に属する VM にはデータアクセス層への参照が設定されないため、本来は src. 2 のように PersonRepository のインスタンスは作成できませんが、DI を適用すると fig. 2 の破線矢印のように VM でデータアクセス層のクラス生成が必要になる点です。

上記のような問題は DI を適用したら必ず出て来る問題と言う訳ではありませんし、歴史的には Factory パターン等を使用して回避していたと思われますが、管理人は当時の状況を追いかけていた訳ではないので詳しくは知りません。
実際の理由や流れは知りませんが、上記の問題やインスタンス作成時の煩雑さを解消したい等の要望から依存オブジェクトを一元的に管理するための仕組みとして DI コンテナと言う方式が考え出されたんだと思います。

次章では Prism から DI コンテナを使用する場合のサンプルを紹介します。



Prism の DI コンテナ

DI コンテナがいつ頃から出て来たライブラリなのかは分かりませんが、軽く調べただけでも 2005 年や 2006 年頃に書かれた Java の記事が見つかるので、おそらくそれ以前から存在したんだと思います。当時、管理人が仕事で使用する事の多かった Window Form や ASP.NET WebForm 等では DI を使用する事が無かったので、実際に DI コンテナの成り立ちを見て来た訳ではありません。そのため事実とは異なるかもしれませんが、DI する依存オブジェクトを一元管理したいと言う要望から DI コンテナと言う方式が考え出されたと予想しています。

DI コンテナは、極端に言うと以下 2 つの機能しか持たないと言えます。

  • DI コンテナへ依存クラスを登録する
  • DI コンテナで生成されたインスタンスを取得する(Resolve)

Prism の DI コンテナは fig. 3 のようなイメージで動作していると思ってください。

fig.3 DI コンテナ内部動作イメージ

fig. 3 のように、DI コンテナへの登録は実装者が個々のクラス単位で行いますが、インスタンスを取り出す(Resolveする)際は DI コンテナ内部で依存オブジェクトをインジェクションした結果のインスタンスが取得されます。つまり、DI コンテナにはインジェクションする側のクラスだけでなく、インジェクションされる側のクラスも登録しておく必要があります。

fig. 3 の動作イメージを踏まえて以降では Prism で DI コンテナを利用する方法を紹介します。

DataAccess プロジェクト(データアクセス層)

MVVM の Model 部プロジェクトを作成する場合、通常は【クラスライブラリ (.NET Core)】から作成する場合が多いと思いますが、DI 対象のクラスを含むプロジェクトを作成する場合は、fig. 4 のように【Prism Module (.NET Core)】から作成します。

fig.4 新しいプロジェクトの追加

Prism Module テンプレートから作成したプロジェクトには fig. 5 のように Views と ViewModels フォルダが作成されますが、Model 部には不要なので削除します。(IModule を継承したクラスは残します

fig.5 DataAccess プロジェクト

PersonRepository クラス自体の実装は src. 2 と全く同じですが、IModule を継承した SampleDataAccessModule を src. 4 のように編集します。

Prism Module テンプレートから作成した場合に自動生成される IModule を継承したクラスの RegisterTypes メソッドは前回紹介した PrismApplication の RegisterTypes と同じ DI コンテナへクラスを登録するためのメソッドなので、IPersonRepository を指定して登録しています。プロジェクトを【Prism Module テンプレート】から作成したのはこのメソッドを呼び出す必要があったからです。

RegisterTypes メソッドのパラメータに渡される IContainerRegistry は DI コンテナの登録機能のみを抽象化したインタフェースで、以下のような 3 つのメソッドを持ちます。

メソッド名内容
Registerex.) containerRegistry.Register<IPersonRepository, PersonRepository>();
Register メソッドで登録した型は Resolve する度に新しいインスタンスが生成されます。
RegisterSingletonex.) containerRegistry.RegisterSingleton<IPersonRepository, PersonRepository>();
RegisterSingleton メソッドで登録した型は最初に Resolve した際に生成したインスタンスが常に返されます
RegisterInstanceex.) containerRegistry.RegisterInstance<IPersonRepository>(new PersonRepository());
RegisterInstance メソッドで登録した型は RegisterSingleton と同じく常に同一のインスタンスが返されます。RegisterSingleton の場合は DI コンテナ内部で作成したインスタンスが返されますが、RegisterInstance は登録時に設定したインスタンスが常に返されます
RegisterInstance で設定するインスタンスは外部で生成できるため、例えば XML ファイルからデシリアライズしたインスタンス等も設定できます。

IContainerRegistry には上記以外にも『RegisterForNavigation』等のメソッドがありますが、上記 3 メソッド以外は全て UI 系部品(部分 View 等)を登録する場合に使用する特別なメソッドなので、自分で作成した依存オブジェクトを登録する場合は上記 3 メソッドのみを使用します。
UI 系の部品(部分 View 等)を登録する場合に使用する特別なメソッドは別途紹介します。

そして、src. 4 の通り IPersonRepository は Register メソッドで登録しているため、Resolve(インジェクション)される度に新規のインスタンスが作成されます。



AppLogic プロジェクト(アプリケーションロジック層)

アプリケーションロジック層のプロジェクトもデータアクセス層と同じく【Prism Module (.NET Core) テンプレート】から作成して、DataAgent クラスを src. 5 のように作成します。

DataAgent の実装も src. 2 と全く同じで、コンストラクタに IPersonRepository をインジェクションするのも変わりません。DI コンテナを使用する場合でもコンストラクタのパラメータに依存クラスを並べるだけで、実行時はそれぞれにインスタンスが設定されます。つまり、依存クラスが 3 つあればパラメータを 3 つ、5 つあればパラメータを 5 つ並べるだけで実行時には各パラメータへインスタンスが設定されます。
インジェクションするクラスは全て DI コンテナへ登録しておく必要があります

そしてインジェクションされる側の DataAgent クラスも IPersonRepository と同じく src. 6 のように DI コンテナへ登録します。

通常、DI コンテナへの登録はインタフェースを指定しますが、src. 6 のようにクラスを直接登録することもでき、インタフェースを指定した場合と同様にインジェクションもされます。DataAgent も IPersonRepository と同様 Register メソッドで登録しているため、Resolve(インジェクション)される度に新規のインスタンスが生成されます。

ViewModel に Model 部のクラスをインジェクションする

前章で DataAgent のインスタンスを VM で作成する際に IPersonRepository のインスタンスが必要になる事が問題だと紹介しました。その問題は DataAgent を DI コンテナから VM にインジェクションすることで解決できますが、最初に紹介した通り、DI コンテナで DI を実行するには DI する側も、される側も DI コンテナへ登録する必要があります。

IContainerRegistry のメソッド紹介でも書きましたが、VM は UI 系部品なので src. 7 のように Model 系のクラスとは違うメソッドで登録します。

登録処理は他の Model 系のクラスと同様 IModule.RegisterTypes に書きますが、登録処理に【RegisterForNavigation】を使用するのが違う点です。【RegisterForNavigation】は Prism の動的 View 切り替え(ナビゲーション)用の UserControl を登録するためだけのメソッドと思われがちですが、Prism のナビゲーションは表示用の View を DI コンテナから取得しているため、結果的に部分 View も DI コンテナへ登録されます

部分 View も DI コンテナに登録されると言う事はインジェクションも、他の Model 系クラスと同様に行われます。DI コンテナからの部分 View 取り出し(Resolve)は Prism のナビゲーション機能を呼び出すと Prism 内部で実行されるので、実装者が Resolve を呼び出す必要はなく View の表示命令を呼び出すだけです。
※ Prism の部分 View 切り替えについては又、別エントリで紹介します

部分 View の VM を src. 7 のように編集します。

VM へインジェクションする場合も他の Model 系クラスと同じく src. 7 のようにコンストラクタのパラメータへインジェクションしたいクラスを追加します。src. 7 の 13 行目にブレークポイントを設定すると、DataAgent のインスタンスがセットされているのが確認でき、PersonRepository のインスタンスがセットされることも確認できます

src. 7 のように PersonRepository のインスタンスがセットされた DataAgent を受け取れるようになるので VM で DataAgent を使用する場合でも src. 3 のように PersonRepository を生成する必要が無くなる事が DI コンテナを利用した場合のメリットです。

後は実行すると fig. 6 のような画面が表示されます。

fig. 6 実行後の画面

赤枠で囲んだ部分が部分 View(ViewSample.xaml)で、MainWindow 起動時に表示しているので正常に動作していることが確認できます。

サンプルプロジェクトの構成

ここまで紹介してきたサンプルプロジェクトは fig. 7 のような構成イメージで作成しています。

fig.7 サンプルプロジェクト構成

fig. 7 のように Model のプロジェクトも Prism の Module として作成して各 Module から依存クラスを DI コンテナへ登録する場合、実は以下のような細々とした想定外の制限がいくつか出て来ました

  • Shell(Window・VM)へ Model 部の依存クラスはインジェクションできない
  • アプリ起動時に表示する部分 View へ DI すると例外が発生する場合がある

全てのパターンを試す事は不可能なので上記 2 点以外にも制限はあるかもしれませんが、GitHub リポジトリ に上げたプロジェクトでは一応、想定通りに動作しています。ですが、ちゃんと動作するまでかなりの試行錯誤が必要でした。

GitHub リポジトリ に上げたプロジェクトでも参照を変更したり、部分 View 表示メソッドの呼び出し場所を変更する等、多少手を加えただけで『ResolutionFailedException:No public constructor is available for type (クラス名)』例外が Throw される状況です。この例外はインジェクションする依存クラスが DI コンテナに見つからない場合に Throw される例外のようで、前回紹介した PrismApplication のオーバーライド可能なメソッドの呼び出し順序が原因でした。

Prism の起動プロセス実行順が原因とは言え Prism のバグではなく仕様なので、サンプルプロジェクトを全体的に見直す必要があると考えました。そのため本章の内容は本来不要だと思いますが、同じような問題が発生した人の参考になるかもしれないので残すことにしました。

尚、本章で紹介した方法はアンチパターンではない事は覚えておいてください。IModule.RegisterTypes は部分 View を登録する事が主な目的のようで、Model 系クラスの登録も可能ですが、Prism が想定しているシナリオではあまり推奨される方法ではないのかもしれません。

余談ですが、上で紹介した『ResolutionFailedException』ですが、Module を登録し忘れたり、インジェクションする依存クラスを登録し忘れたりした場合でも Throw されます。例外のエラーメッセージに『public なコンストラクタが無い』と表示されるため最初は原因に気付くまで数時間もかかってしまいました… この例外が Throw された場合は『DI コンテナへ未登録のクラスをインジェクションしようとしている』事が原因なのでメソッドの実行順やプロジェクトの参照関係等を見直す必要があります。

とりあえず本章で紹介したプロジェクト構成だと問題が起きる可能性もあるので、次章でサンプルプロジェクト全体を見直しますが、本章で紹介した内容はほとんど変わりません。



IoC(Inversion of Control:制御の反転)を有効活用するための構造

前章に書いた制限は VM へインジェクションする Model 系クラスを Module で登録している事が最大の原因でした。IModule の RegisterTypes メソッドは Prism 起動プロセスの最終段階で呼び出されるため Shell の Load 時や、Shell の Load 時に表示する View の生成時には依存クラスが未登録なので、前章で紹介した『ResolutionFailedException』が Throw されるようです。

依存クラスを DI コンテナへ登録するための RegisterTypes メソッドは IModule と PrismApplication に定義されているだけなので、Module から登録できない場合は PrismApplication で登録するしかありません。そして、PrismApplication で Model 系クラスを登録する場合、スタートアッププロジェクトへ Model 系プロジェクトの参照を追加しなければなりません。スタートアッププロジェクトには Shell となる Window が含まれるため UI 部に属すると考えて作成したのが前章までのプロジェクト構成でした。

そもそも Shell とは【Region(部分 View を表示する領域)で構成されたアプリケーションルートオブジェクト】と Prism では定義されているので、UI 部に属するオブジェクトではないと考える所から見直しを始めました。加えてスタートアッププロジェクトだけに含まれる PrismApplication はフレームワークの動作を設定するアプリケーション基盤と言う位置付けのため、最終的に fig. 8 のような構成で作成する方が Prism の想定するシナリオに合致しそうだと考えました。

fig.8 あるべきプロジェクトの構成

前章までの構成から最も変わった点はスタートアッププロジェクトを UI 部ではなく基盤部と考えた所です。スタートアッププロジェクトを基盤と見なす事で Model への参照を追加しても問題ないと管理人自身は納得しました。同時に、Model 部のプロジェクトは Module ではなく通常のクラスライブラリプロジェクトで作成できるようになります。
fig. 8 の構成に変更することで Shell や 起動時表示 View へのインジェクションも問題なく動作するようになります。

つまり、Prism でアプリを作成する場合、最低限実行可能プロジェクト 1 つと、部分 View を格納した Module 1 つ、計 2 つのプロジェクトが最低限の構成だと考えました。前章で紹介した構成を変更しますが、基本的には考え方を変えただけなのでソースコード等は前章から大きく変わりません。

この連載では fig. 8 の構成を前提で進めて行くつもりですが、『それは違うだろ』とか『思想的にやってはダメだろ』等の反論なりある場合はコメントで教えてもらえると助かります。管理人的にも一応 Prism の構造等を踏まえて考えた結果ですが、管理人の知らない方法等があれば知りたいです。

スタートアッププロジェクトをアプリケーション基盤に変更

サンプルアプリを fig. 8 の構成に変更してもソースコードはほとんどそのまま使用できます。各プロジェクトの参照設定を fig. 8 のように変更して Model 部のプロジェクトから Module クラスを削除すれば前準備は完了です。後は App.xaml.cs を src. 9 のように変更します。

前章までは IModule に書いていた Register メソッドを App.xaml.cs で呼び出すように変更しただけです。一応、前章で紹介していたプロジェクトは全て【RegistAtModule】フォルダ に移動して残しています。

念のためもう 1 度書きますが、依存クラスを DI コンテナへ登録するのは PrismApplication でも IModule でも可能なので【RegistAtModule】フォルダに移動したサンプルコードを使用することもできますが、Shell や アプリ起動時に表示する View へインジェクションしたいクラスは PrismApplication で登録する必要があると言う事だけは覚えておいてください。つまり、fig. 7、8 の構成は併用可能であり共存可能です。

DI コンテナから取得できる Prism オブジェクト

ここまでは自分で作成した依存クラスを DI する方法を紹介して来ましたが、DI コンテナには Prism の機能を利用するためのクラスも Prism から登録されます。Prism は PrismApplicationBase(PrismApplication の継承元)の初期化プロセスで RegisterRequiredTypes を呼び出して Prism 内の各種インタフェースを登録しています。

そのため、以下のクラスをコンストラクタのパラメータへ追加すれば DI コンテナからインジェクションされます

インタフェース名内容Use
IModuleCatalogモジュールの一覧を表すインタフェース。×
ILoggerFacadeLogger の Facade インタフェース。
※ Prism Ver. 8 で変更されそうなので利用はお勧めしません。
IDialogServiceダイアログウィンドウ等を表示するためのサービスを表すインタフェース。
IRegionManagerRegion(部分 View を表示するエリア)の管理部分 View と Region をアタッチするためのインタフェース。
IEventAggregator疎結合なコンポーネント間で通信するためのインタフェース。
IRegionNavigationJournalRegion 内で戻る、進む等の部分 View 遷移履歴を表すインタフェース。
IRegionNavigationServiceRegion 上で View を切り替えるためのインタフェース。
IDialogWindowIDialogService で表示する Window を表すインタフェース。

『Use 列』は以下のような意味です。

  • × → ほとんど使用しない
  • △ → あまり使用しない
  • 〇 → よく使用する
  • ◎ → ほぼ必ず使用する

実際の RegisterRequiredTypes では上記以外のインタフェース類も登録されていますが、Prism が内部で使用するために用意している public なメンバを持たないインタフェース等は除いています。又『Use 列』のマークは管理人の独断と偏見なので参考程度に考えてください。

上記に加えて、Shell である Window も VM も DI コンテナへ登録済みですし、src. 8 で紹介した RegisterForNavigation で登録した UserControl と VM も DI コンテへ登録されるので、VM だけでなく Window や UserControl のコードビハインドのコンストラクタにもインジェクション可能です。

ServiceLocator パターン

Prism が使用する DI コンテナは Ver. 7.2 から抽象化されて登録専用の IContainerRegistry インタフェースと取得専用の IContainerProvider インタフェースのみ提供されるよう変わりました。ですが、上で紹介した DI コンテナへ登録されるインタフェースの一覧にはどちらも含まれていません

そのため IContainerRegistry、IContainerProvider のどちらもインジェクションすることはできませんIContainerRegistry は PrismApplication と IModule の RegisterTypes のパラメータから取得できるだけですし、IContainerProvider は IModule の OnInitialized のパラメータから取得できるだけです。

そのため、DI コンテナへの登録は PrismApplication と IModule の RegisterTypes から実行するしかありませんし、DI コンテナからの取得はコンストラクタインジェクションから取得するしかありませんが、登録はともかく取得は任意のタイミングで行いたい場合もあると思います。

例えばサンプルで紹介した IPersonRepository がインスタンス作成と同時に DB へのコネクションを張り、Dispose のタイミングでコネクションを切断するようなクラスだった場合、DataAgent と同じ生存期間になるように作成すれば問題ないと思いますが、DataAgent を使用する VM では困る場合もあると思います。

上の src. 7 では DataAgent をコンストラクタへインジェクションしていました。

src. 7 の場合、DataAgent 内部の IPersonRepository は ViewSampleViewModel のインスタンス生成と同時に作成されます。本来はボタンクリック等のユーザ操作が通知されたタイミングで DB へのコネクションを確立したいのに画面表示時点から VM が破棄されるまで DB への接続を保持したままになってしまいます。

そのような場合、コンストラクタインジェクションではなく src. 10 のように ServiceLocator パターンでインスタンスを取得することもできます。

16 行目の Resolve の Generic 版を使用する場合【using Prism.Ioc】を追加しないと呼び出せないので気を付けてください。【using Prism.Ioc】しなくても Resolve メソッドは呼び出せますがキャストが必要になります。

src. 10 のように PrismApplication の Container プロパティを経由して任意の型を Resolve する事ができます。
※ Prism の DelegateCommand 等の使用法は又、別のエントリで紹介します

PrismApplication.Container から取得した場合でも fig. 9 のように DataAgent 内部には IPersonRepository のインスタンスが設定された状態で取得されます。

fig.9 PrismApplication.Container から取得した DataAgent

src. 10 のように渡したコンテナから任意のクラスを取得するデザインパターンを ServiceLocator パターンと言います。ServiceLocator パターン は DI コンテナを使用している場合には基本的に非推奨ですが、元から使用していたクラスを変更せずに使用したいような場合でも対応できるように用意されていると思われます。

ServiceLocator パターンでインスタンスを取得できるとしても View、ViewModel のみの使用に止めるべきで、Model 内は全て DI パターンで作成すべきだと管理人個人的には思っています。

又、ServiceLocator パターンを使用する場合、PrismApplication の参照が必要なため、fig. 10 のように別途 DI コンテナ別のパッケージを Nuget からインストールする必要があります

fig.10 Prism.Unity パッケージのインストール

fig. 10 は【Ctrl + .】で表示されるクイックアクションから Prism.Unity パッケージをインストールしています。
※ DryIoc を選択した場合は Prism.DryIoc パッケージをインストールします

ServiceLocator パターンの注意点(2020/9/15 追記)

前項では ServiceLocator パターンを利用して DI コンテナから任意のタイミングでインスタンスを取得する方法を紹介しましたが、注意しなければならないのは PrismApplication.Container を使用する VM は単体テストができないと言う点です。

少なくとも IContainerProvider がインジェクションできるようにならないと ServiceLocator パターンは使うべきではないのかと思って試してみたら IContainerProvider をインジェクションする方法を見つけてしまいました

StackOverflow や Prism の Issue に書かれている方法ではないので邪道っぽいですが『それだけの事⁉』と思うような方法でもあります。まず、src. 11 のように DI コンテナへ IContainerProvider を登録できます。

正に『それだけの事⁉』と言う程度かもしれませんが、App.xaml の RegisterTypes で this.Container を指定すれば登録できます。気を付けるのは【containerRegistry.RegisterInstance】で登録する事です。登録する IContainerProvider は常に同一のインスタンスをインジェクションして欲しいので RegisterInstance で登録して Singleton のように扱う事を指示します。

IContainerProvider がインジェクションできるようになれば src. 10 の ViewSampleViewModel は src. 12 のような実装にできます。

コンストラクタへのインジェクションは今まで紹介してきた方法と何も変わりません。src. 11、12 のように修正して実行しても前項までと同じように動作します。加えて、src. 11、12 のように修正すると前項でインストールした『Prism.Unity パッケージ』が不要になります。アンインストールしても変わらず動作します

IContainerProvider がインジェクションできるようになっても単体テスト時に DataAgent が登録できる IContainerProvider を用意しなければならないのは変わりませんが、単体テストは実行できるようになります。やはり本来はインジェクションしても問題ない構造の DataAgent を作成すべきだと思います。

あくまでもここでは、Model 系クラスの構造が変更できないため止む無く任意のタイミングで DI コンテナからインスタンスを取得する方法を紹介しているだけで、本来はコンストラクタへインジェクションしても問題無い構造で作成する必要がある事は覚えておいてください。
尚、IContainerRegistry も同じ方法でインジェクション可能にはなりますが、有用なサンプルは思い付きません。管理人個人的には避けるべきだと思っています。

IoC フレームワークとしての Prism

※ 2021/4/18 匿名さんのコメントをきっかけに本章をリライトしました。

最後になりましたが、ここまでほとんど紹介もせずサラッと使ってきた IoC(制御の反転)について簡単に紹介します。DI について調べていると IoC(と言う単語)も見かける事が多いので DI する場合は IoC になるよう気を付けないと… と思ってしまうかもしれません(管理人は最初そう思っていました)が、IoC とは主に【DI コンテナを使用するフレームワークを作成する際の設計原理】なので、アプリを作成する場合にはあまり気にしなくても良いと管理人は思っています。

IoC 自体は管理人自身も言語化できる程理解できているのか怪しいので、申し訳ありませんが、別途調べてください。尚『ioc』で検索するとトップには「国際オリンピック委員会」関連のページしか表示されないので『制御の反転』で検索するか『C#』等を追加した方が良いと思います。

つまり、IoC は Prism(と言うフレームワーク)には既に組み込み済みの仕組みなので、自分で作成するアプリは、ここまで紹介した通り、各部品を疎結合になるよう作成して、実行時は DI コンテナで動的に結合するように作成すれば実現できます

但し、Prism で IoC を実現する場合に 1 つだけ注意があり、それは【Prism の標準動作等を無闇やたらに変えない】事で、最重要は【ViewModelLocator.AutoWireViewModel = True】です。これまでも ViewModelLocator.AutoWireViewModel = False にしてコードビハインドから DataContext を設定する方法等を紹介しましたが、その方法を採った場合、IoC は利用できないので注意してください。

Prism は【ViewModelLocator.AutoWireViewModel = True】に設定することを前提に IoC を実現する設計になっているので、False に設定してしまうと VM が DI コンテナに登録されない = VM へインジェクションされません。アプリ作成者自身で DI コンテナへの VM 登録や DI コンテナへ登録した VM を取得して Window や部分 View の DataContext へ設定することもできそうな気はしますが、Prism が用意してくれている仕組みに乗っかる方が楽だと思いますし、乗らないなら Prism を使用する意味がありません

Prism が IoC を行う仕組みを理解した上で False に設定するのであれば構わないとも思いますが、DI コンテナや Region ナビゲーションさえも捨てる事になるのは忘れないでください。

クリックして開くとリライト前の文章が読めます

ここまで DI コンテナを利用して疎結合な関係にある各部品を動的に結合する方法を紹介して来ました。ここで 1 度思い返す(読み返す)と実装者がクラスのインスタンスを作成(new)している箇所がほとんどない事に気付くと思います。DI コンテナ無しでアプリを作成する場合、通常は Window を new してその内部でサービスを new して… と言うように実装者自身がクラスのインスタンスを作成するのが当たり前だったと思いますが、このエントリで紹介したような方法で作成したアプリは実装者がインスタンスを作成しなくても DI コンテナから自動的に依存オブジェクトがセットされるようになります。

このようにクラスの生成を実装者ではなく Prism のようなフレームワーク側で自動的に行われる仕組みを IoC(制御の反転)と呼びます。IoC を意識して作成するアプリはプログラムのモジュール化が進んで拡張性も高くなります。作成するアプリに IoC を取り入れる場合、Prism のようなフレームワークの使用が前提になります。

IoC を取り入れると言ってもデザインパターンのように指針なりクラス図なりが存在している訳ではなく『DI コンテナを組み込んだフレームワークを使用して作成したアプリは IoC』と言える概念のようなものと管理人は理解しています。厳密な取り決めなどが定義されている訳ではなさそうなので、極端に言えば Prism を使用しているだけで IoC を取り入れていると言えます。

確かに Prism のナビゲーション等の機能はフレームワークで生成されたインスタンスがセットされるため IoC と言えますが、それだけでなくこのエントリで紹介したように自作のクラスも DI コンテナへ登録して IoC されるように作成する方がプログラムのモジュール化を促進して拡張性を高める事になると思います。

但し、Prism で作成したアプリへ積極的に IoC を取り入れたい場合は 1 つだけ注意があって【Prism が提供している標準設定等を無闇やたらに自分好みの方法に変えない】事です。

例えば前回の記事で保留にした Window や UserControl の DataContext の設定方法等です。

前回、DataContext を手動で設定する場合はコードビハインドで設定する… と言うような事を書きましたが、Prism で作成する場合、DataContext は標準のまま ViewModelLocator.AutoWireViewModel = true を使用すべきです。Prism は ViewModelLocator に従って Window や UserControl、その VM を DI コンテナへ自動的に登録するため、この辺りの仕組みに手を入れると今回紹介したような Model 系クラスのインジェクションが行われなくなるため無闇に変更すべきではないと思います。

Prism が IoC を行う仕組みを理解した上で変更するのであれば構わないとも思いますが、基本的には非推奨な方法だと思ってください。

又、本章のタイトルを『IoC(Inversion of Control:制御の反転)を有効活用するための構造』にしていますが、前章で紹介した Module から依存クラスを登録する方法を採っても IoC なのは変わりません。展開の都合上このタイトルにしただけだと思ってください

まとめと終わりに

この連載を始めてから毎回書いている気がしますが、今回もかなり長いエントリになりました。ですが、Prism の DI コンテナを有効利用するための情報としてはある程度網羅できたような気がします。各 DI コンテナ固有の使い方にまで踏み込むにはさすがにスペースが足らないので今回は紹介しませんが、機会があれば記事にしたいと思います。

今回紹介した IoC を取り入れるために DI コンテナを活用すると、副産物としてユニットテストし易くなる等のメリットも発生するのでお勧めです。

以前 WPF Prism episode: 4 で DI コンテナの使い方を紹介した時には、何のために DI コンテナを使うのかさえ分からない状態で書きましたが、今回は一応、IoC と言う Prism が目指していると思われる概念的な所まで踏み込んで書けたと思っています。

Prism と DI コンテナは別々の記事で紹介されている事も多く、Prism から DI コンテナを使う方法等はほとんど見たことが無いので、Prism の DI コンテナはイマイチ活用されていないと思っています。今回の記事で DI コンテナがもう少し活用されるようになってくれれば良いと思っています。

そして次回は Prism の Region と部分 View について書こうと考えています。又、いつものように今回紹介したサンプルコードも GitHub リポジトリ へ上げています。

 

 

次回記事「Prism の Region に部分 View がいます。【step: 5 .NET Core WPF Prism MVVM 入門 2020】」

 

 

おすすめ

14件のフィードバック

  1. noriokun4649 より:

    素敵な記事ありがとうございます。
    Prism + MVVMで構築されたアプリを弄っているため、非常に助かってます。

  2. Ala より:

    Prismについて知りたくて今この連載を読みつつ勉強させてもらってます。
    ひとつ気になる点がfig.8なのですが、
    ModuleSample–>SampleDataAccess–>SampleAppLogicsという参照の記載になっていますが、
    プロジェクト参照を見る感じだとModuleSample–>SampleAppLogics–>SampleDataAccessではないでしょうか・・・?
    勘違いならすみません。

    • 沖田玲朗 より:

      返信遅くなって本当にすいません!
      読んで頂き & コメントまで頂きありがとうございます!

      なかなか時間が取れず放置してすいませんでした…
      ご指摘の通り fig. 8 の参照は SampleAppLogics –> SampleDataAccess が正しいので、図を修正しました。

      間違いになかなか気が付かないので指摘いただけるのは本当にありがたいです。
      今後ともよろしくお願いします!

  3. 匿名 より:

    「クラスの生成を実装者ではなく Prism のようなフレームワーク側で自動的に行われる仕組みを Ioc(制御の反転)と呼びます。」という部分は訂正した方が良いと思います。フレームワーク経由でDIやインスタンスの解決をすることとIocとは全くの別物で直接的には関係ありません。該当セクションで何を伝えたいのかは分かりませんが、IocとPrismの関係性を説明したいという意味では、Iocを実現するためにフレームワーク(DIコンテナ)を利用することが一般的、という表現が妥当ではないでしょうか。セクションごと削除もしくは取り消し線送りにすることをおすすめします。

    書いた当時は管理者さんの理解がなく、現時点では習得しているかどうかなど状況が分かりませんので、一応内容について補足しておきます。まず、Iocを極めて省略して表現するなら、操作の流れと参照の向きが逆になること、です。操作の流れとは、主にUIなどを起点に実行される関数の順序や呼ばれるクラスの順序のことを意味します。実現するためには依存はインターフェイスに向けておく必要があります。一方、フレームワークは具象クラス(インターフェイスを実装するクラス)のインスタンスを良い感じにハンドリングしてくれます。手動で作成することなく良い感じに生成し良い感じに引渡してくれるなど、あくまで具象クラスの解決がラクになるだけです。(補:手動で作成・登録も可能) その機構を用いることでIocが行いやすくなる、というだけで、実際にIocになっているかどうかは実装次第です。おそらく、インスタンスの解決を目的にフレームワークだけを使用することはあると思いますが、Iocは実装するけどインスタンスはフレームワークを頼らず手動で解決する、ということはないでしょう。Iocを実装するときはまず間違いなくフレームワーク(DIコンテナ)はセットで使用されます。(補:本文にも似たニュアンスの事は書いてあったような・・) なので、全く関連性がないということはありませんが、それぞれの関心事は全く別のところにあるので、ご注意ください。結論、あえて関連性に触れるならIocを実現するためにフレームワークを利用するのが一般的、が適切だと思います。

    • 沖田玲朗 より:

      読んで & コメントも頂いてありがとうございます。

      > フレームワーク経由でDIやインスタンスの解決をすることとIocとは全くの別物で直接的には関係ありません。
      > Iocを実現するためにフレームワーク(DIコンテナ)を利用することが一般的、という表現が妥当ではないでしょうか。
      > 該当セクションで何を伝えたいのかは分かりませんが
      【Ioc とは主にフレームワークに組み込んだ DI コンテナをユーザが利用する場合等に採用される設計原理である】と言うのが管理人の認識です。
      Ioc = 設計原理と言う性質上、実際のアプリ作成に Ioc を取り入れる方法を紹介しているような記事は少ないので、Ioc を組み込んだフレームワーク(Prism)を使用して Ioc の恩恵を受けるにはどうするかと言う方向で書いたのがこのエントリです。

      つまり、『Ioc とは何ぞや』を紹介するのではなく【Ioc を組み込んだ Prism を利用するには】に焦点を当てて書いたエントリなので、該当セクションも削除しなければならない程的外れだとは思えません。

      該当セクションで伝えたかったのは、DI コンテナをガッツリ利用しなくても Prism 自体は Ioc を組み込んだフレームワークであり、View と VM の自動関連付けのように Prism のお作法通りに作成していれば勝手に Ioc が動作するフレームワークですが、それには Prism が標準で用意している【ViewModelLocator.AutoWireViewModel = true】を指定しないと DI コンテナへの登録や、View の DataContext へセットする VM を DI コンテナから取得するのも自分で書く事になるから自己流は避けた方が良いと言う点です。

      Prism を使用する前提で、DI コンテナの利用法を紹介してしまうと、Ioc については書く事があまり思い付かなかったと言うのもありますが、自分の文章力の低さを痛感しています。
      まあ、該当セクションを書いていた頃は何度も書き直して飽きていた頃だったので、Ioc についての紹介を端折り過ぎたな…と、匿名さんのコメントを見て反省しています。

      そのため、該当セクションは上で書いたような意図があるので、
      > セクションごと削除もしくは取り消し線送りにすることをおすすめします。
      上記のような措置は取らないと思います。

      頂いたコメントを参考に推敲して、端折った事を追記したり修正はするかもしれませんが、セクション自体を削除することは恐らく無いと思います。
      管理人は自分の理解度が大して高くないと思っているので、コメントで指摘いただくのは非常にありがたいです。
      どのようにするかはまだ決めていませんが、週末中には該当セクションを修正すると思うので、それ以降(おそらく4/19以降)に間違いなどあれば又、指摘いただけるとありがたいです。

      • 匿名 より:

        すみません、私の言っていることは検討違いでした。制御の反転ではなく依存性逆転についてで、ちょっとまた違いましたね。改めて確認しましたがIocとPrismの関連性は、コメント内の「View と VM の自動関連付けのように Prism のお作法通りに作成していれば勝手に Ioc が動作するフレームワーク」が適切だと思います。どの道、文章が長くまどろっこしいのでそれくらい端的に表現した方が分かりやすいと思います。余計なコメントすみませんでした。

        • 沖田玲朗 より:

          いえいえ、こちらこそありがとうございました。
          公開した時は何を書いているか分かりにくい文章だと言う自覚はあったんですが、エントリの一部だけを見直すことはほとんどないので、指摘していただいて助かりました。

          自分 1 人で文章を考えていると煮詰まって新しい考えを思い付きにくいですが、指摘いただいた内容を踏まえて文章を読み返すと驚く程頭の中で整理ができました。一応、週末中には書き直そうと思っています。

          今後ともよろしくお願いします。
          #依存性逆転は初めて?聞いたので又詳しく調べようと思います!ありがとうございました。

  4. 匿名 より:

    重箱の隅をつつくようで申し訳無いですがsrc.3はMainWindow.xamlではなくMainWindowViewModel.csではないでしょうか

  5. SakaToshi より:

    WPF+prismについての理解がとても深まります
    概念はぼんやり理解しているが人に説明が出来ない(つまり自分でも詳細がわかってない)状態だったので非常に助かります

    • 沖田玲朗 より:

      SakaToshi さん>
      コメントありがとうございます!
      管理人も Prism 使い始めて 1年以上経ってやっと何となく分かった感じなのでそう言って頂けると記事に書いた甲斐があります。

  6. kasa883 より:

    このサイトを最初の頃からよく読んでおかげで今の業務でPrism + MVVM で構築することができています。
    今回はとても良い記事でした。楽しみにしています。

    • 沖田玲朗 より:

      kasa883さん >
      読んで頂き & コメントも頂きありがとうございます。

      > 今の業務でPrism + MVVM で構築することができています。
      そうなんですね!こんな記事を書いていますが管理人はWPFで作成するプロジェクトに入ったことが無いのでそういう情報を聞くと嬉しくなります。
      今後ともよろしくお願いします。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください