episode: 3 ~ Re: ゼロから始める Prism 生活 ~

← 前回記事 【episode: 2 ~ WPFのフレームワーク決まってますか?迷ってますか?Prismを選択してもらっていいですか? ~】

前回の記事では、UI 部品の動的読込機能の実装を Prism 公式サンプルを使って紹介したので、今回は実際のアプリケーションへ実装する手順を紹介します。

Prism でアプリケーションを作成するには Prism Template Pack をインストールしていると非常に楽なので、未だインストールしていない場合は、episode: 1 を見てインストールしてください。
重要 本記事は Prism Template Pack がインストール済みの前提です。

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

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

(2018/10/26)
Prism 7.1 (WPF) の記述を数か所、追記・修正しました。

新しいプロジェクトの作成

Visual Studio で [新しいプロジェクトの作成] から【新しいプロジェクトダイアログ】を開き、[Visual C#] – [Prism] の順に開いて表示される【Prism Blank App (WPF)】を選択(プロジェクト名はお好みで。又、.NET Framework は Ver.4.7.2 を使用しています)すると、Prism の Shell となる Windows アプリケーションプロジェクトが作成されます。

保存場所とフレームワークのバージョンを選択して【OKボタン】をクリックすると、DI コンテナの選択画面が表示されます。

Prism 7.1 (WPF)
Prism 7.1 とほぼ同時に Prism Template Pack もバージョンアップされています。
しかも、バージョンアップしてしまうと以降は 7.1 用の Shell や Module しか作成できなくなるため、6.3 のまま使い続けたい場合は Prism Template Pack をバージョンアップしないよう気を付ける必要があります。

(2018/10/26 修正)
(一応、手作業で修正できそうな気はしますが、DI コンテナ関連の名前空間が根こそぎ変更されているため、かなり手間がかかりそうな雰囲気です)
(一応、手作業で修正できそうな気はしますが、DI コンテナ関連の名前空間が根こそぎ変更されているため、アプリ自体の作り次第では、かなり手間がかかる場合があるかもしれません)

Prism では DI コンテナ として、【Autofac】、【Dryloc】、【Unity】の 3 種類が選択できますが、ここでは【Unity】を選択して、[CREATE PROJECT ボタン] をクリックします。(.NET Framework Ver.4.6 等を選択すれば、【Ninject】も選択できるようですが、.NET Framework Ver.4.7.2 を選択した場合、Ninject は選択肢に表示されませんでした)

info DI とは Dependency Injection(依存性注入)のことで、Prism 内部では DI コンテナを使用して View、ViewModel の関連付け等を実装しているようです。

DI コンテナの【Unity】とは ゲーム開発用のエンジンと同じ名前ですが全くの別物です。DI コンテナの Unity は Prism と同じく Microsoft の patterns & practices で生まれたプロジェクトで、日本では最も知名度の高い DI コンテナだと思います。
管理人は Unity 以外の DI コンテナを知らないため、他の DI コンテナと比べたメリット、デメリット等は書けませんが、日本語での情報の取りやすさは 3 つの DI コンテナの中でトップだと言えるでしょう。

注意 Prism のテンプレートからプロジェクトを作成した直後は、Prism のコンポーネントが存在しないため、警告が表示されています。

警告を消すには、episode: 2 でサンプルを開いたときと同じく、ソリューションのコンテキストメニューから [NuGetパッケージの復元] を選択して、Prism のコンポーネントを復元してください。

パッケージの復元が完了すると、警告が消えてビルドできるようになります。

MainWindow の作成

ここで作成する MainWindow は以下のような外観で作成します。

  • ツールバーとステータスバーを配置
  • クライアント領域は左右 2 分割
  • クライアント領域の左側には TreeView を配置
  • 左側に配置した TreeView の TreeViewItem をクリックすると右側の View が切り替わる

まずは、Views.MainWindow.xaml を開いてください。

クライアント領域に、『Grid』と『ContentControl』がすでに配置されています。
管理人は今までそれなりの数の Windows Form アプリケーションを作成してきたので、この初期デザイン画面を見て、「Grid 要らなくね?」と思い、試しに両コントロールとも削除してみました。

削除したコントロールは非表示コントロールなので、プレビューの見た目は当然変わりません。
そして、MainWindow 内の適当な位置に『TextBox』をツールボックスから追加してみました。(コントロールの追加方法は Windows Form と同じです)

Windows Form の時と見た目はあまり変わりません。
そして、先ほど追加した TextBox の横に『TextBlock』(テキストブロックです!)を配置してみました。

先ほど追加したはずの TextBox が消えて、TextBlock に入れ替わってしまいました…
(XAML 側も書き換わっている!?)
知っている人からすれば、「何言ってんだこいつ?」と思われるかもしれませんが、管理人が始めてこの操作をした時は一瞬何が起こったのか理解できませんでした。

疑問に思って調べてみると、WPF の解説サイトでは第一人者(と管理人は思っています)のかずきさんが主宰する【かずきのBlog@hatena】にこんな説明が…

WPF は、コンテンツモデルという初見の人にとってはよくわからないものが採用されています。何か表示するものを 1 つだけ持つものは Content というプロパティで、それを指定します。この Content は object 型なのでなんでも入ったりするという、Windows Forms から見たら考えられない状態になっています。
Windows FormsからWPFへ乗り換えるときの最初の障壁

と言う事は、Content プロパティを持つコントロールに置けるコントロールは 1 つだけと言う事になり、Content プロパティを持つ『WPF の Window クラスはコンテナとしては機能しない』ことになります。
note 逆に言えば、Content プロパティを持つコントロールにコンテナを置けば、その中に複数のコントロールを配置することが可能と言うことになります。

まあ、Window クラスがコンテナとして機能しないことに何か問題があるかと言えば、すぐには思い付かないのも確かですが、このような Windows Form と根本的に違う部分などをまとめた情報が目立つ場所で目にすることがあまりない事が WPF が普及しない原因だと思うのは、管理人だけでしょうか…?

実際、XAML 構文やデータバインディングの方法等は重要な情報でしょうし、Microsoft 的には WPF における目玉機能なのでしょうが、開発者が新技術を目にした時、最も知りたい事は、自分がアプリケーションを作成する際に、どこに注意をするのか?、今までのやり方とどれだけ違うのか?という点ではないかと管理人は思います。
ただ、WPF にしても Prism にしても最近ようやく情報が熟れて来た感がありますし、Xamarin が WPF をサポートしたという追い風もあるので、管理人個人的にはそろそろ普及し始めて欲しいと思っています。
又、あくまで管理人の個人的な意見ですが、おそらく UWP は WPF 以上に普及せず廃れていくと思っているので、今から WPF を覚えても決して損はないとも思っています。

Grid コントロールでレイアウトを設定

Windows Form の画面デザインでもパネルと Dock プロパティを組合せることでリサイズに対応した画面を作成することができました。(管理人は主にパネルを多用してレイアウトしていましたが、TableLayoutPanel 等を使用していた人も多いかもしれません)

WPF でコントロールを配置する場合も基本の考え方は変わりません。
横方向のグループ・縦方向のグループに分けて、グループ単位でパネルに配置する考え方自体は特に変わらないと思います。(固定サイズ画面のみ使用する事が圧倒的に多い業務系の開発の経験しかない人には、馴染みが薄い考え方かもしれませんが…)

ただし、WPF では Windows Form にある Anchor や Dock プロパティ等は存在しないので、基本的にはコンテナ系のパネルを組み合わせて(又は入れ子にして)レイアウトを作っていくことになります。
例えると、CSS 登場以前の Web サイトデザインでよく見られた、Table タグを入れ子にして位置を調整するのと似ています。

多少横道に逸れましたが、本題に戻ってコントロールを配置していきます。
まずは、Prism Template が自動的に配置した ContentControl を削除して、ツールバー、ステータスバーの配置用に Grid を行方向に 3 分割します。
Grid を分割するには、[Grid のプロパティ] – [レイアウト(カテゴリ)] – [RowDefinitions] のボタンを Click して【RowDefinition コレクションエディタ】を開きます。

【RowDefinition コレクションエディタ】の追加ボタンを 3 回クリックすると行の定義が 3 つ分追加されます。

追加されたレイアウトの 1 行目へツールバーコントロールをツールボックスから追加します。
XAML を直接編集せず、ツールボックスからコントロールを追加した場合、下図のように、コントロールの周りはマージンとして設定されてしまうので、コントロールのコンテキストメニューから  [レイアウト] – [すべてリセット] を選択します。(XAML 側から『Margin』、『Height』等を削除しても同じです)

レイアウトを全てリセットすると、リセットされたコントロールは Grid の枠一杯に表示されるようになり、Grid 側に設定した比率に従って配置したコントロールが描画されるようになります。
Windows Form で言えば、Dock.Fill を設定したのと同じような見た目ですね。

レイアウトの 3 行目にステータスバーをツールバーと同様に配置して、両コントロールの Height プロパティを『25』に設定し、Grid レイアウトの 1 行目、3 行目の高さを『Auto』に設定します。
WPF では本来、コントロールを固定サイズに設定することは推奨されていませんが、ここではとりあえず画面の見た目を作ることを優先したいので、固定サイズとしました。
注意 ツールバーやステータスバーについては、配置するだけで使用方法等には触れない予定です。

レイアウトの 2 行目は列方向に 2 分割して、TreeView を配置する場所を確保したいので、レイアウトの 2 行目へ新規の Grid を追加します。
TreeView を含むクライアント領域は、Windows のエクスプローラーのように右側と左側の比率をユーザがマウス等で変更できるようにしたいので、TreeView、UI 部品動的読込用 ContentControl に加えて GridSplitter も配置します。
新規で追加した Grid を列方向に 3 分割して、以下の位置にコントロールを追加してください。

1 列目TreeView を読み込むための ContentControl
2 列目GridSplitter
3 列目動的に切り替える View を読み込むための ContentControl

各コントロール名や、MainWindow 自体のサイズや StartupPosition プロパティ等をお好みで設定して、最終的な XAML は以下のようになります。

2018/11/03 修正
22 行目 の GridSplitter で重要なプロパティ(HorizontalAlignment)が抜けていたため追加しました。このプロパティが抜けていると左右のペインが正常にリサイズできません。
詳しくは【[C#][WPF]WPF 垂直GridSplitterのポイント】を参照してください。

21、23 行目に配置した ContentControl.RegionManager.RegionName に設定するリージョン名は、お好みの名前で構いません。
この状態で 1 度実行してみます。

当たり前ですが、ツールバー、ステータスバー以外はのっぺらぼうの画面が表示されます。

Prism Module の作成

続いて、MainWindow に読み込む TreeView を含む Prism Module を作成します。

アプリの用途的に考えると TreeView は本来、動的に読み込む必要は無く、MainWindow へ固定的に配置するパターンが一般的だとは思いますが、ここではあえて TreeView を Prism Module へ配置して、アプリ起動時に動的に読み込む仕様で作成することにしました。
TreeView を貼るプロジェクトとして、ソリューションに【Prism Module プロジェクト】を追加します。(管理人はプロジェクト名を『NavigationTree』としました。)

設定するプロジェクト名は任意で構いませんが、『Module』という単語は避けて命名する方が良いと思います。(理由は後ほど)
Shell プロジェクトを作成したときと同様に DI コンテナの選択画面が出てくるので、Shell で選択したのと同じ DIコンテナ(ここでは Unity)を選択して [CREATE PROJECT ボタン]をクリックします。

Prism 7.1 (WPF)
Prism 7.1 (WPF) で管理人が最も影響が大きいと感じている変更は DI コンテナ関連です。
7.1 から DI コンテナが抽象化され、Unity や MEF 等のコンテナを直接指定して呼び出す必要がなくなっています。

そのため、Shell プロジェクト作成時は DI コンテナを選択しますが、Module 側では DI コンテナが隠蔽されているため、上記の DI コンテナ選択ダイアログは表示されません。

作成したプロジェクトの中身は以下のようになっていて、プロジェクトルートに Prism の IModule を継承した【NavigationTreeModule】が自動追加されています。
先ほど、『Module』という単語は避けたほうがいいと書いたのはこのためで、このクラスは『プロジェクト名 + Module』という名前で自動生成されるため、プロジェクト名を NaviModule にすると NaviModuleModule と言う名前で作成されます。(変更可能ですが)

最初に Shell のプロジェクトを作成した時と同様に、Prism のパッケージが読み込まれていないため、[ソリューションのパッケージを復元] を実行します。

又、作成したプロジェクトのプロパティから、『ルート名前空間』を Shell のルート名前空間に合わせます。
これは単に管理人の好みなので、デフォルトのままでも特に問題はありません。

Module と併せて【Views.ViewA】と言う UserControl が自動生成されています。
さすがに名前が微妙過ぎなので変更したいですが、このクラスを直接変更するより新規で追加した方が早いので、【Views】フォルダのコンテキストメニューから【Prism UserControl (WPF)】を追加します。(ファイル名はお好みで!)

管理人はプロジェクト名と同じ、『NavigationTree』として作成します。
View を追加すると、一緒に ViewModel も追加してくれます。
本来 View の名前を変更する場合、以下全ての名前を変更する必要があります。

  • View のファイル名
  • View のコードビハインド
  • View の Xaml
  • ViewModel のファイル名
  • ViewModel のコード

変更が必要な箇所が多いので変更するよりは新規で追加して、元のファイルを削除した方が楽ですね。

元々 Prism の テンプレートから自動追加されていた【ViewA】は不要なので、ViewModel と併せて削除します。

この Prism UserControl を追加する時もし、下のキャプチャのようにめっちゃ小さい UserControl が表示される

とか、Grid の行定義(例えば)に比率をちゃんと指定しているのに、見た目が崩れる
(下図は Grid の行を 50%、50% で指定しているが 2 行目が 50% に見えない)

等の状況にぶち当たって困っている場合は、XAML の UserControl の定義に以下でハイライトを付けた 4 行を追加することで解消できる場合があります。

以下の 4 行はデザイン時の UserControl のサイズを設定する項目で、実行時には無視されます。

コピペしやすいよう別枠で置いておきます。この 4 行を追加すると、土台となる UserControl が固定サイズで表示されるようになるため、コントロールを配置する時に実行時のイメージがつかみやすいと思います。
コントロールのレイアウトをリセットした後、UserControl に余白が見えなくなって困るなどの状況に当たった場合は、UserControl の XAML に上記の 4 行を追加してみてください。

これは、Prism Template Pack の問題で、プロジェクトの追加から自動で生成される View には上記 4 行を含んでソリューションへ追加されますが、新しい項目の追加から追加される View には含まれないことが原因です。
何故そのような状態になっているのか分かりませんが、次のリリースで治ることを期待しましょうw

Prism 7.1 (WPF)
Prism Template Pack もバージョンアップされましたが、対応されていませんでした…

本題に戻ります。
追加した Views.NavigationTree をデザイナで開いて、TreeView を追加し、Grid 一杯になるようレイアウトを全てリセットした後のデザイナ画面です。

とりあえずここではこれ以上何もせず、保存します。

NavigationTree を Shell のリージョンへ読み込む記述として、NavigationTreeModule に以下のハイライト行を追加してください。

この追加した IRegionManager.RegisterViewWithRegion が呼ばれると、第 1 パラメータで指定した Region へ第 2 パラメータで指定した型の View が読み込まれます。
Module 側への記述はこのハイライト行のみです。

Prism 7.1 で同様の処理をする場合、以下のように記述します。

Prism 7.1 (WPF)
DI コンテナを抽象化したため、IModule の構造も上記のように変更されています。
6.3 まではインジェクションしたい型を直接コンストラクタに追加していましたが、7.1 からは IModuleに【OnInitialized】メソッドと【RegisterTypes】メソッドが追加されたため、パラメータを追加してインジェクションできなくなっています。(IModule で定義されているメソッドのみ)
それに伴いそれぞれのメソッドのパラメータに【IContainerProvider】、【IContainerRegistry】が渡されるよう修正されています。

この変更で、指定した型がインジェクションされるのではなく、パラメータで渡された抽象化されたコンテナの Resolve メソッドを呼び出してオブジェクトを取り出す必要があります。

(2018/10/26 追記)
補足 Prism Module で DI コンテナへ型やインスタンスを登録したい場合、Prism 7.1 で追加された RegisterTypes メソッドの containerRegistry パラメータを使用して登録できます。

Bootstrapper

最後に Shell へ Module を読み込むための記述を Bootstrapper に追加します。
重要 以下を実装する前に、Shell へ Module の参照設定を追加する必要があります。

Prism 7.1 (WPF)
Prism 7.1 (WPF) 以降で Bootstrapper は廃止されました。
(ライブラリ内にはクラスが残っていますが、使用は推奨されません)

Bootstrapper の代わりに、【PrismApplication】が定義され、App.xaml が継承しています。
7.1 での App.xaml.cs は以下のように記述します。(記述内容は Bootstrapper とほぼ変わりません)

そして実行すると、TreeView(枠だけですが…)が読み込まれた画面が表示されます。

いかがでしょう?
結構少ない記述で UI 部品の動的読込機能が実装できる事が分かってもらえたと思います。
この機能を使用するだけでも Prism を組み込む価値がある!と管理人は声を大にして言います。

Prism 7.1 (WPF)
既存のプロジェクトの Prism を 7.1 にアップすると突然大量のコンパイルエラーが発生するので、一時はどうなるかと思いましたが、とりあえずプロジェクトを作り直して対応できました。
(ここに書いた対応が正しいかは未だ分かりませんが…)

(2018/10/26 追記)
後は、Module 内で DI コンテナをどのように使うかが見つかっていませんが、調査を続けていく予定です。

2018/10/26 時点でこの記事へのアクセスが増大してきている為、後で判明した点を追記しておきます。(次回記事は鋭意作成中でまだ公開できないので…)

Prism 7.1 で最も影響の大きな変更は DI コンテナの取り扱いで、『UnityContainer』等の直接的なクラス名を使用するのではなく、【IContainerRegistry】、【IContainerProvider】を使用して、型・インスタンスの登録・取り出しを行うようになった点です。
名前の通り、【IContainerRegistry】を使用して型・インスタンスの登録を行い、【IContainerProvider】を使用して型・インスタンスを取り出します。

Bootstrapper と Module の変更が強制されますが、DI コンテナ自体の動作は Prism 6.3 の頃から変わっていません。
例えば、ViewModel 等のコンストラクタに DI コンテナへ登録した型をパラメータとして追加すると、今までと変わらずインジェクションされます。(private や public や static に置いた DI コンテナをいろんな所からいじりまわしているような行儀の悪い作りだと困りそうですが…)

管理人的には、Module で IRegionManager に Regist する箇所、ViewModel でインスタンス登録用に IUnityContainer をインジェクションしている箇所(インジェクションする型を IContainerRegistry に変更する必要がある)は修正必須だと思いますが、それ以外は Prism 6.3 からの変更なしに動作すると思います。

とりあえず、今回はここまでとして次回は

  • TreeView に TreeViewItem を表示する
  • Prism で使う Unity

の2本立てでお送りする予定です。

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

この連載記事について(2018/11/26 追記)
WPF は .NET Framework 3.0 と共に 2006 年 11 月にリリースされて 10 年以上(2018 年 11 月現在)経つプラットフォームですが、今から WPF について勉強しようとしても、ネットで検索できる情報は Windows Form 関連の情報と比べて圧倒的に少なく(日本語の情報は特に)又、体系的に書かれた WPF 入門編的な情報もほとんど見当たりません。
しかも、MVVM パターンでフレームワークを使用して作成するとなると、更にヒットする情報は少なくなるため学習に困難な状況だと言えると思います。

加えて、MVVM パターンは WPF より遅れて普及したため、ネットで見つかる WPF の情報はコードビハインドを想定して書かれたものが多く、MVVM パターンでの開発に適した情報が少ないため、MVVMパターンでの開発に挫折することも多いような気がします。

管理人個人的には、MVVM で開発する場合、海外も含めて 2015 年より前に書かれた情報は MVVM パターンでの開発に適用できない情報が 8 割を超えると感じています。
その上、WPF への機能追加や、MVVM パターン内でも手法のトレンドが以前とは変わっているものもあり、ネットの情報はかなりカオスな状態で何から手を付けていいか途方に暮れそうになったことも 1 度や 2 度ではありませんでした。
そんな苦労した経験から、「自分が WPF アプリを作成するために調べた情報を、調べた順番に書けば役に立つ人も居るかもしれない!」と思ってこの記事を書き始めました。

この連載記事は、WPF 入門、Prism 入門として読んで欲しいと思って書いていますが、コメントやいいね等をもらったことがないので、実際、この記事が役に立っているのかは分かりません。
ただ、このサイトのアクセスログからは、この連載のいずれかの記事を見た人が他の連載ページも読んでいる場合が多い傾向が見える為、これから WPF や Prism を覚えたい人や、WPF や Prism で何ができるか知りたい人の何らかの助けになっていれば幸いです。

【WPF episode: 4 ~ DI だけど Unity さえあれば関係ないよね~】次回記事 →

あわせて読みたい

コメントを残す

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