WPF Prism episode: 4 ~ DI だけど Unity さえあれば関係ないよねっ ~

← 前回記事【WPF Prism episode: 3 ~ Re: ゼロから始める Prism 生活 ~】

前回は Prism Module 内の View(UserControl)を Prism Shell へ動的に読み込むまでを紹介したので、今回は Prism のコンポーネント間でデータをやり取りするする際の仲介役となる DI コンテナ【Unity】の使い方を紹介します。
又、前回エントリの最後で「次回は TreeViewItem を TreeView へ表示する」と予告しましたが、エントリが予想以上に長くなってしまったため、「TreeViewItem の表示」は次回に延期します。

尚、この記事は Visual Studio 2017 Community Edition で .NET Framework 4.7.2 以上 と C# + Prism 7.1 を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。

Prism 7.1 (WPF) に対応しました!
日本時間 2018/10/16 の未明に Prism 7.1 がリリースされました。
このリリースは、影響が大きい修正が加えられたため、Ver.6.3 から変わった部分も併記しています。
注意 Prism for Xamarin.Form の情報はありません。

Prism DI コンテナ Unity 入門

このサンプルアプリの完成形は下 fig.1 のような画面になる予定で、全ての画面項目は単一のデータファイルから取得した内容を表示します。

fig.1 サンプルアプリ実行時イメージ

まず最初に MainWindow(Prism Shell)左側に配置した TreeView へ表示する内容を取得します。
TreeView を取り扱ったブログの記事等では、Windows のエクスプローラーを模した例をよく見かけますが、ここではもう少し実際的なサンプルとして、XML ファイルから取得したデータを TreeView へ表示する例を紹介します。

サンプルアプリのデータファイルは以下の概要通りとします。

  • データファイルは単一の XML ファイル
  • データファイルからオブジェクトをデシリアライズして取得する
  • データファイルの拡張子が作成するサンプルアプリに関連付けられている
  • アプリ単独で起動すると【新規データ作成モード】で起動し、デフォルト値を表示する

WPF アプリで起動時パラメータを取得する

ダブルクリックしたデータファイルの Path を起動時パラメータから受け取るのは Windows Form でもよくあるパターンで、Windows Form では Main メソッドで取得するのが一般的だと思いますが、WPF では Main メソッドが隠蔽されているため、App クラスの OnStartup メソッドをオーバーライドして取得します
参考 WPF の Main メソッドを自動生成せず、Main メソッドを自作する方法もあるようです。
(参考先:C#のWPFでMainメソッドを編集する [管理人は未確認])

まず、ソリューションエクスプローラーの『App.xaml』のコンテキストメニューから [コードの表示] で App クラスをコードエディタで開き、以下のように OnStartup メソッド内で起動時パラメータを取得します。

起動時パラメータの取得自体はどうと言うことはありませんが、取得したデータファイルの Path をアプリへ引き渡す必要があります。

Windows Form の場合であれば、App クラスへデータファイルの Path プロパティを追加するとか、スタートアップフォームへプロパティを追加してデータを渡す等のパターンが考えられますが、episode: 3 で紹介した通りこのサンプルアプリは MainWindow と TreeView が別々のプロジェクトに分かれていて、しかも実際にデータが必要なのは TreeView を含む NavigationTree Module 内の VM です

今回は Shell が NavigationTree Module を参照しているので、Bootstrapper 経由で渡す方法もありそうですが、あまりスマートな方法とは思えません。
それに Prism では Shell に Module の参照を追加しなくても使用できる方法が用意されているため、その場合は Bootstrapper 経由で渡す方法も採れません。
他にもよくあるパターンとしては、DataHolder のような名前の Singleton (static)クラスを作成して Prism Shell、Module の両方から DataHolder を参照してデータを受け渡すと言う方法も考えられますが、DataHolder に依存する作りになってしまいそうです。

Prism では各コンポーネント間が疎結合なので、Windows Form での開発とは少し考え方を変えないと static なクラスを山のように作ってしまうことにもなりかねませんが、Prism にはコンポーネント間でデータをやり取りする仕組みが当然用意されています

アプリ内で使用するクラスのインスタンスをアプリ実行時に DI コンテナへ登録する

その仕組みとは episode: 3プロジェクト作成時に指定した DI コンテナ(ここでは Unity)ですが、DI コンテナの使い方を紹介する前に、まず起動時パラメータを Prism へ渡さないと始まらないので、Bootstrapper に追加したプロパティを経由して起動時パラメータ(データファイルの Path)を渡します。

データファイルの Path を受け取る Bootstrapper です。

後述しますが Bootstrapper の ConfigureContainer をオーバーライドして、ロードしたデータ(WpfTestApp オブジェクト)を DI コンテナへセットするとサンプルアプリ内で WpfTestApp オブジェクトを受け渡しする準備が整ったことになります。

Prism 7.1 (WPF)

Prism 7.1 以降では Bootstrapper に Obsolete 属性が追加されて非推奨になったため、Bootstrapper の役割が Appクラス(App.xmal.cs)へ移動されました。
そのため Prism 7.1 からは App.xmal.cs へ以下のように実装します。

OnStartup メソッドをオーバーライドして起動時パラメータを取得するのは Prism 6.3 の場合と同じですし、取得したデータファイルの Path から WpfTestAppData を取得して IContainerRegistry.RegisterInstance メソッドで DI コンテナへ登録する方法も Prism 6.3 の場合と変わりません。
Prism 7.1 (WPF)

Prism 6.3 では App.xaml.cs と Bootstrapper の 2 ファイルに記述場所が分かれていましたが、Prism 7.1 からは Bootstrapper が PrismApplication に統合されたため、App.xaml.cs のみに記述すればよくなりました。

又、Prism 6系・7.1 以降の両方ともデータファイルの読み込み失敗等の理由で App クラスの起動を中断してアプリを終了したい場合、途中で Return するだけでは終了しないので、CreateShell メソッドで App.Shutdown メソッドを呼び出す必要があります

アプリの起動を中止したい場合は 14 行目のコメントアウトしている位置で Shutdown メソッドを呼び出すとアプリが終了します。
注意 App.Shutdown を CreateShell メソッドではなく OnStartup メソッドで呼び出した場合、アプリが終了する前に MainWindow が一瞬表示されてしまうのでCreateShell メソッドで呼び出す方が良いと思います。

DI コンテナに登録した型やインスタンスを取り出す方法を紹介する前に、今までにも何度か登場しているサンプルアプリデータクラスや、サンプルアプリデータクラスを Load するための Agent クラスを紹介します。

サンプルアプリのデータの読み込み・生成を行うクラス

以下が新規データを生成したり、データファイルからモデルを読み込んだりするためのサービス層に位置する static な DataLoader クラスです。

現時点ではデータ保存済みのファイルが存在しないので、Load メソッドの dataFilePath パラメータは常に空文字が渡されるため、新規データを作成して返すだけのクラスです。
DataLoader クラスはソリューションに新規追加した WpfTestAppServices プロジェクト内に作成しています。

サンプルアプリデータ本体

以下のクラスがサンプルアプリで内で使用するデータ本体で、XML データファイルからデシリアライズさるクラスです。
info『DataContract 属性』を付加する場合は、【System.Runtime.Serialization】の参照追加が必要です。

このクラスもソリューションへ新規追加した WpfTestAppModels プロジェクト内に作成しています。

WpfTestAppData クラスは『個人識別情報』と『測定日別身体検査データリスト』、『試験日別得点データリスト』を含んでいます。本来、このようなデータを単一のデータファイルに保存することは一般的ではないでしょうが、そのような要求があったと仮定してください。
又、2 つのリストで使用している【System.Collections.ObjectModel.ObservableCollection<T>】はメンバを追加・削除すると通知してくれるコレクションクラスです。

WpfTestAppData クラス内に定義している『個人識別情報』と『測定日別身体検査データ』、『試験日別得点データ』は、次回以降で必要になった時に改めて紹介しますが、GitHub リポジトリで見ることもできます。

DI コンテナの Unity を介して Prism コンポーネント間でデータをやり取りする

前回のエントリから『DI コンテナ』と言うワードを何度も使っていますが詳しい説明はしていません。
泣き言 実は管理人、現時点でも DI 及び DI コンテナについてちゃんと理解できているとは思えないので、思い込みや間違い等も多いかもしれません。間違い等あれば指摘して頂けると有難いです。

まず、DI(Dependeny Injection)と DI コンテナ は全く別物です。
詳しくは『DI・DIコンテナ、ちゃんと理解出来てる・・? – Qiita』を参照してください。

一言で言うと、DI(Dependeny Injection)は『依存性注入』と言うデザインパターンの 1 つで、DI コンテナは DI を実行するためのライブラリを表します。

【Unity】は DI コンテナなので DI を実行するために Prism に内蔵された機能です。
もっと簡単に言うと、DI コンテナ の【Unity】は型やインスタンスを自由に出し入れできるグローバルなオブジェクト保管庫のようなもので、DI コンテナに登録した型やインスタンスはいつでも・どこでも取り出すことができる便利な保管庫だと考えれば分かりやすいかもしれません。

Prism で使用できる DI コンテナは何種類かありますが、ここではプロジェクト作成時に Unity を選択した場合のみの方法を紹介しています。(管理人は Unity 以外使ったことがありません)
ただ、DI コンテナの考え方自体は変わらないはずなので、DI コンテナ自体の使用方法が少し違う(属性を付加する必要がある等)だけで、ここで書いた情報が応用できるかもしれません。

DI コンテナの RegisterInstance メソッドでインスタンスを登録

前章で既に紹介していますが、Prism 内の DI コンテナへ型やインスタンスを登録するには PrismApplication.RegisterTypes メソッドや、IModule.RegisterTypes メソッドのパラメータに渡される IContainerRegistry を利用します

ここで紹介した RegisterInstance メソッドは読んで字のごとく DI コンテナへインスタンスを登録するためのメソッドで、登録したインスタンスは Singleton のように振舞うので、ソリューション内のプロジェクト間で同一のインスタンスを引き回して使う場合に適しています。

DI コンテナにはインスタンスを登録する以外に【型(Type)】も登録できますが、それは又別の機会に紹介します。

DI コンテナからインスタンスを取り出す

続いて、DI コンテナへ登録した型やオブジェクトの取り出し方ですが、ググってもドンピシャな内容が見つからず気が付くまでかなりの時間がかかった気がします。

Prism 7.1 の IModule には OnInitialized メソッドのパラメータに IContainerProvider が渡されるようになったので、そのパラメータを通じて DI コンテナへアクセスできますし、IContainerProvider.Resolve メソッドで登録された型やインスタンスを引き出すことが可能です。

ですが、IModule 内で登録したインスタンスを引き出したとしても、実際に引き出した型を使いたいのは UserControl の VM である場合が多く、IModule から VM へ引き渡す方法を別途考える必要があるのかと悩みましたが、VM で DI コンテナに登録したインスタンスを引き出すには、【VM のコンストラクタに引き出したい型のパラメータを追加する】だけです。

Prism 6.3 の VM で Unity コンテナを受け取る

例えば NavigationTree(Prism Module 内の UserControl)の VM で IUnityContainer のインスタンスが欲しい場合は、以下のようにコンストラクタへ IUnityContainer 型のパラメータを追加します。
Prism 7.1 では本項目の内容は実行できませんが、飛ばさず読んでもらえるとありがたいです。

ブレークポイントを設定して実行すれば一目瞭然ですが、追加したコンストラクタのパラメータへ IUnityContainer のインスタンスが渡されているのが確認できるはずです。

IUnityContainer 型のオブジェクトは Prism 6.3 内部で自動的に登録されているため、開発者が DI コンテナへ登録しなくてもコンストラクタにパラメータを追加するだけで注入されます。
これをコンストラクタインジェクションと言います

DI コンテナの使い方を知っている人たちからすれば、「それが何か?」かもしれませんが、管理人のように使い方を知らなかった人間からすれば、『そんな書き方が出来ること自体が驚き』でしたw
DI コンテナってすごいですねw どんな原理で動作しているか未だによく分かっていませんw
独り言 管理人の探し方が悪いのでしょうが、DI コンテナをどのように使うかというようなドンピシャの情報は見つかりませんでした。(Prism、Unity のような複合キーワードで検索してたからかもしれませんが…)ここを見ると分かりやすく書かれてるよ!って所があれば教えて欲しいです。

どんな原理で動いているか分からなくても使い方がわかれば先に進めます。
つまり、上記のようにコンストラクタにパラメータを追加するだけで DI コンテナのインスタンスが取得できるので、後は DI コンテナから Resolve メソッドで登録している型やインスタンスを取り出すことができます。
ですが、VM 内部で Resolve<WpfTestAppData>() すると言うことは、WpfTestAppData に依存していることになり DI パターンとは言えません。

DI コンテナの Unity へ登録済みのインスタンスを VM のコンストラクタへインジェクション

DI コンテナでインジェクションできるのは Prism 内部で自動的に登録されたオブジェクトだけでなく、コンテナへ登録した型やインスタンスであれば何でもインジェクションできます

IRegionManager や IEventAggregator (何をするためのクラスなのかは別の機会に紹介します)等も同様に Prism(6.3、7.1共に)が内部で自動的に登録してくれているため、コンストラクタへパラメータを追加するだけで勝手にインジェクションされます。
ex. 上記のコンストラクタを public Constructor(IRegionManager rm, IEventAggregator ea) に変更するだけで、2 つのパラメータにインスタンスがインジェクションされるので試してみてください。

登録済みのオブジェクトは何でもインジェクションしてくれると言うことは、上記のコードを以下に変更するだけで…

App.RegisterTypes 等で開発者が登録したデータオブジェクトもインジェクションされます
文章だけだと伝わりにくいかもしれないので、実際に書いて動かすのが 1 番手っ取り早いです。

ユニットテスト等を考えると、WpfTestAppData 用に IWpfTestAppData インタフェースを別途定義して、DI コンテナへは型を登録する方が良いのかもしれませんが、面倒なので 今後の説明の都合上インスタンスを登録する方を選択しました。

これで、どの VM でもコンストラクタへパラメータを追加するだけで WpfTestAppData のインスタンスが受け取れるので、疎結合で組み立てた Prism アプリケーション内のどこでもデータの受け渡しが可能になります。

Prism 7.1 (WPF)

Prism 7.1 以降でもインジェクションの動作自体は上記で紹介した方法から変わっていません。
【UnityContainer】のようなライブラリ自体の名称が見えなくなっただけで、Prism 6 系で使用していた時と変わらず DI コンテナへの登録やコンストラクタインジェクションも使用できます

今まで紹介した内容をまとめると、Prism 7.1 以降で Prism Shell ⇔ Prism Module 間、VM ⇔ VM 間 でクラスのインスタンスをやり取りしたい場合、PrismApplication.RegisterTypes メソッドか、IModule.RegisterTypes(登録はどこか 1 か所で行えば良い)でインスタンスを登録して、VM 等のコンストラクタへ必要な型のパラメータを追加すれば自動的にインジェクションされる。これを Unity のコンストラクタインジェクションと言うことになります。
DI コンテナは【型やインスタンスを自由に出し入れできるグローバルなオブジェクト保管庫】だと紹介したのは上記のようなことができるからです。

これで DI コンテナ(Unity)の使用方法を理解してもらえたかどうかは分かりませんが、何を書いているのか分からないような所があればコメントでも頂ければ回答します。(管理人が理解している範囲内ですが…)

アプリ起動時に取得したデータを Module 内の VM へ渡すことができたので、後は受け取ったデータを元に TreeViewItem を作成するだけですが、これ以上は長くなり過ぎるので今回はここまでとします。
次回こそは、TreeView へ TreeViewItem を表示する方法を紹介します!

又、ここまで作成したソリューションをリポジトリに上げておきます。

Livet Ver. 2.0

この記事の公開直前に気が付いたんですが、Livet 2.0 がリリースされていました!
Livet v2.0.0 をリリースしました – かずきのBlog@hatena

しかも ReactiveProperty のメンテナーであるかずきさんが、Livet のメンテナーも引き受けられたようなので、かなり心動かされて Livet への鞍替えを真剣に考えてしまいました。
やはり国産で MVVM への対応がきめ細かいと言う評判をよく見かけるフレームワークなので(現時点で詳細は未調査)悩みましたが、ここまで Prism を調べてきたのに今更 Livet へ鞍替えするのはさすがに躊躇しました。

Livet の対応について、あまり詳しい内容は調べていませんが、Xamarin へは未対応(? 未調査です)のような気がするので、やはり当面は Prism 推しで行こうかとは考えています。
Livet が Xamarin に対応!とか Livet と ReactiveProperty が連携して… 的な発表があった場合には乗り換えを再検討してしまうかもしれませんが、かずきさんのブログに新機能への対応はしないような記載があるので、そんな状況は起こらなさそうですが…

 

次回記事【WPF Prism episode: 4.5 ~ ReactiveProperty からはじまる MVVM 狂想曲 ~】→

 

あわせて読みたい

コメントを残す

メールアドレスが公開されることはありません。

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

%d人のブロガーが「いいね」をつけました。