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

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

前回は、Prism Shell に Prism Module 内の View を動的に読み込むまでを作成したので、今回はサンプルアプリで使用するデータと、Prism Shell – Module 間のデータ連携に使う 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 の情報はありません。

TreeView を取り扱ったブログの記事等では、Windows のエクスプローラーを模した例をよく見かけますが、ここではもう少し実際的な例として、XML ファイルに保存したデータを TreeView へ表示します。

まず、データファイル周りの概要は以下とします。

  • データの保存先は単一の XML ファイル
  • データはオブジェクトをシリアライズして保存する
  • データファイルの拡張子を『.abc』等に変更して、作成するアプリに関連付けている
    (データファイルをダブルクリックするとアプリが起動するイメージ)
  • アプリ単体で起動した場合は、新規データ作成モードとし、デフォルト値をセットした空のデータを表示する

サンプルアプリで使用するデータ

サンプルアプリで使用するデータとして以下のクラスがあり、このクラスを XML ファイルからデシリアライズして、NavigationTree Module 内の TreeView へ表示します。
info『DataContract 属性』を付加する場合は、【System.Runtime.Serialization】への参照をプロジェクトへ追加する必要があります。

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

本来、【Bmi プロパティ】は読み取り専用プロパティとして定義するべきでしょうが、読み取り専用プロパティを View からバインドしても変更通知が来ないため、Height、Weight プロパティを更新しても View 側の値が更新されません。
Model が ViewModel の都合で構造を変えるのはおかしい気もしますが、『読取専用プロパティだが、他値の変更結果を反映して通知すること』的な仕様が指示された場合、この程度は許容範囲かと思っています。
実はもっと別の書き方もあるのかもしれませんが、管理人はこの書き方しか思い付きませんでした。

『試験日別得点データリスト』のメンバクラスです。
TestPointInformation.Average プロパティも上記、PhysicalInformation.Bmi プロパティと同様、読み取り専用ですが、変更が通知されるようにしています。

個人識別情報クラスです。(Sex プロパティを文字列型で定義しているのは手抜きです)

MVVM のデータバインディングと言えば一般的に、View – ViewModel 間を指しますが、このサンプルアプリでは Model – ViewModel 間もデータバインディングで接続するので、前述した『読取専用プロパティだけど変更を通知する』ようにしています。

又、Model と ViewModel をバインドするために、Model 側にも INotifyPropertyChanged インタフェースを実装する必要がありますが、WPF 標準だとプロパティの記述が冗長になる(面倒な)ため【Prism.Mvvm.BindableBase】クラス(INotifyPropertyChanged インタフェース実装済み)を継承することで代用しています。(それでも標準のプロパティと比べると長いですが…)

モデルを別プロジェクトに分割していて、モデルのプロジェクトで BindableBase を使用したい場合、Prism.Core のインストールが別途必要なため、NuGet で『prism.core』を検索してモデルのプロジェクトへインストールしてください。
注意 他のプロジェクトにインストールした Prism と同じバージョンをインストールしてください。

又、Prism Template Pack がインストール済みであれば Prism のコードスニペットも同時にインストールされているので、プロパティを記述する時に『propp』スニペットを利用すれば、更に楽ができます。
『propp』以外のコードスニペットは【Prism Template Packについてくるコードスニペット】等で紹介されています。

起動時パラメータ

データファイルをダブルクリックすると、起動時パラメータに対象ファイルのフルパスが設定される  Windows Form でも一般的な動作を前提に作成します。
そのために、データファイルからモデルを読み込むための DataLoader クラスをサービス層へ static なクラスとして作成します。

現時点では、新規データを作成すると『個人識別情報クラス』にデフォルト値を設定して、『測定日別身体検査データリスト』、『試験日別得点データリスト』に新規データ用のメンバーを 1 つずつ追加したオブジェクトを返すだけのクラスです。

この DataLoader をアプリ起動時に呼び出す場合、Windows Form では Main メソッドから呼び出すのがよくあるパターンだと思いますが、ここでは単に起動時パラメータを取得したいだけなので、App.OnStartup メソッドをオーバーライドして取得します。
参考本来、WPF の Main メソッドは自動生成されるため隠蔽されていますが、自動生成をやめて Main メソッドを自作する方法もあるようです。(例:C#のWPFでMainメソッドを編集する [管理人は未確認])

info ソリューションエクスプローラーの『App.xaml』のコンテキストメニューから [コードの表示] を選択すると App のソースが表示できます

Windows Form アプリの場合ではスタートアップフォームに追加したプロパティ等を経由してデータを受け渡すようなパターンも多いでしょうが、このサンプルアプリの場合、例え MainWindow にデータを渡したとしても、実際に使用したいのは TreeView を含む Prism Module 側です。
今回は Shell から NavigationTree Module を直接参照設定しているので、Bootstrapper 経由で渡す等の方法(Prism 7.1 以降では Bootstrapper ではなく App 経由)が無いわけではありませんが、あまりスマートなやり方とは思えません。
それに Prism では Module を参照しなくても使用できる方法が用意されているため、Module を参照設定しない場合ではこの方法も採れません。

他にもよくあるパターンとしては、DataHolder のような名前の Singleton クラスを作成して Prism Shell、Moduleの両方から参照してデータを受け渡すと言う方法もあると思いますが、NavigationTree Module が DataHolder に依存する作りになってしまいそうです。

DI コンテナ Unity

ここで出てくるのが DI コンテナの【Unity】です。
UnityContainer はインスタンスを自由に出し入れできるグローバルなオブジェクト保管庫のように使うことができると考えてください。

泣き言 実は管理人、現時点でも DI 及び DI コンテナについてちゃんと理解できているとは思えないので、思い込みや間違い等も多いかもしれません。間違い等あれば指摘して頂けると有難いです。

つまり、UnityContainer にインスタンスを登録しておけばソリューション内のプロジェクト間で同一のインスタンスを引き回して使うことが出来ます。
そのため、まずは Prism の入り口である、Bootstrapper クラスへデータ受け渡し用のプロパティを追加して読み込んだデータを App クラスからセットします。
以下が、データ受け渡し用のプロパティを追加して、UnityContainer へデータを登録する処理を追加した Bootstrapper です。

26 ~ 31 行目のオーバーライドした ConfigureContainer メソッドでデータオブジェクトを UnityContainer へ登録しています。これだけで登録したオブジェクトはいつでもどこでも取り出すことができるようになります。

Prism 7.1 (WPF)
Prism 7.1 以降では Bootstrapper が非推奨になったため、Bootstrapper の役割が Appクラス(App.xmal.cs)へ移動されました。
そのため Prism 7.1 以降では App クラスへ上記 Bootstrapper と App クラスを結合したようなコードを記述することになります。以下が、Prism 7.1 以降の App クラスです。
Prism 7.1 (WPF)
Prism 7.1 以降の Template Packで作成したプロジェクトには、Bootstrapper が存在せず、App クラスにも Container プロパティが存在しません。
そのため、アプリ起動時に DI コンテナへオブジェクトを登録したい場合は、App クラスのオーバーライドした【RegisterTypesメソッド】のパラメータに渡される IContainerRegistry へデータを登録する形に変更されています。
上記コードの 20 行目が Prism 6 Bootstrapper 26 ~ 31 行目と同様の処理になります。

又、Prism 6系・7.1 以降に関わらず、App クラスでの起動を中断してアプリを終了したい場合、途中で Return するだけでは終了しないので、CreateShell メソッドで App.Shutdown メソッドを呼び出す必要があります。
14 行目のコメントアウトしている位置にアプリ終了用コードを追加します。
注意 CreateShell メソッドではなく OnStartup メソッド内等で App.Shutdown を呼び出した場合、MainWindow が一瞬表示されてからアプリが終了するため、CreateShell メソッドで呼び出す方が良いと思います。

Prism Shell でオブジェクトを登録した UnityContainer を Prism Module の ViewModel 等で使用したい場合、対象クラス(ここでは NavigationTreeViewModel)のコンストラクタに IUnityContainer 型のパラメータをを追加するだけです。
ブレークポイントを張って実行すれば一目瞭然ですが、追加したコンストラクタのパラメータへ UnityContainer のインスタンスが自動的にインジェクションされているのが確認できます。

IUnityContainer 型のオブジェクトは Prism が内部で自動的に登録してくれているため、プログラマ側で直接インスタンスを登録する必要はなく、コンストラクタにパラメータを追加するだけで注入されます。
当然、IRegionManager や IEventAggregator 等も同様に Prism が内部で自動的に登録してくれているため、コンストラクタへのパラメータ追加やプロパティに指定するだけで、自動的に注入してくれます。
ex. 上記のコンストラクタを public Constructor(IRegionManager rm,IUnityContainer uc, IEventAggregator ea) のように変更するだけで、3 つのパラメータ全てにインスタンスが注入されます。

DI コンテナをちゃんと理解している人たちからすれば、「それが何か?」かもしれませんが、管理人のように使い方を知らなかった人間からすれば、『そんな書き方が出来ること自体が驚き』でしたw
DI コンテナってすごいですねw どんな原理で動作しているか未だによく分かっていませんw

どんな原理で動いているか分からなくても使い方がわかれば先に進めます。
つまり、上記のようにコンストラクタで UnityContainer が取得できるので、後は Container に登録されているオブジェクトを引っ張り出せば OK と言うことになります。

独り言 管理人の探し方が悪いのでしょうが、DI コンテナをどのように使うかというようなドンピシャの情報は見つかりませんでした。(Prism、Unity のような複合キーワードで検索してたからかもしれませんが…)ここを見ると分かりやすく書かれてるよ!って所があれば教えて欲しいです

DI コンテナ を使って、Prism Shell と Module 間でデータを共有するには上記のように書くことで実現できますが、UnityContainer にはどんなオブジェクトでも登録できるため、UnityContainer 自体をインジェクションしてしまうと、ユニットテスト的には問題があると言えます。
(Prism 6系 を使用していて Module 内で型やオブジェクトを登録したい場合は DI コンテナ自体をインジェクションする必要はあります)

DI コンテナは Prism 内部で自動的に登録されたオブジェクトのみインジェクションされるのではなく、コンテナへ登録したオブジェクトは何でもインジェクションしてくれるので、上記のコードを以下のように変更するだけで…

データオブジェクト自体もインジェクションしてくれます。
ユニットテスト等を考えると、WpfTestAppData 用に IWpfTestAppData インタフェースのようなものを定義して、インジェクションする型に指定する方が良いのかもしれませんとが、サンプルなので実際のオブジェクトを指定しています。
このように、関係が疎に保たれた Prism 内部をシームレスに繋ぐ仕組みとして DI コンテナの Unity が利用できます。

Prism 7.1 (WPF)
Prism 7.1 以降でもインジェクションの動作自体は上記で紹介した方法から変わっていません。
【UnityContainer】等そのものズバリの名称が見えなくなっただけで、プロジェクト作成時に Unity を選択すれば、Prism 6 系で使用していた時と同じ(上記で紹介したコンストラクタインジェクションも同様)ようにオブジェクトがインジェクションされます。

つまり Prism 7.1 からは、Prism Shell 起動時の Bootstrapper、Module 初期化時の IRegionManager の取得方法等が変わっただけと言うことになります。

このように DI コンテナ【Unity】を使用すると Prism Shell – Module 間でデータを簡単に受け渡すことができるようになります。

DI コンテナは上で紹介した通り、かなり強力で便利なライブラリですが、強力過ぎるので使い方を誤れば諸刃の剣になる可能性も高いと思います。特に、大人数が関わるようなプロジェクトではよく注意しないと、構造が簡単に破綻するような状況になるかもしれません。(自分 1 人だったり、2 ~ 3人程度ならどうにかなる場合も多いでしょうが…)

実際に Prism で DI コンテナの Unity を使うにはこの記事で紹介した通り簡単なので、大人数のプロジェクト等に導入を検討する場合は、検討前に『Dependency Injection』デザインパターンを理解してから使う方がトラブルは少ないかと思います。
大人数のプロジェクトで使用したい場合は、DI コンテナの派生クラス等を作って、決まったオブジェクトや型しか登録できないよう制限をかけた方が良い場合もあるでしょう。

アプリ起動時に Prism Shell で読み込んだデータを Module に渡すことができたので、後は受け取ったデータを元に 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 が連携して… 的な発表があった場合には乗り換えを再検討してしまうかもw かずきさんのブログには新機能への対応はしないような記載があるので、そんな状況は起こらなさそうですが…)

次回記事【WPF episode: 5 ~ご注文は TreeView ですか?~】→

あわせて読みたい

コメントを残す

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