WPF episode: 6 ~されどイベントは ViewModel と踊る~

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

前回は、ReactiveProperty を使用して TreeView へアイコン付きの TreeViewItem を表示するまでを紹介したので、今回は、選択した TreeView のノードに対応した Prism Module 内の編集 View への切り替え(画面遷移)時に編集 View と ReactiveProperty をバインドする方法を紹介します。

この連載を読んでくれている人がどの程度いるか分かりませんが、以前より増えているのはアクセスログから分かります。ただ、記事を書くために調べることが増えてきたので、今回は更新間隔が少し伸びてしまいました。今後も今まで通りの間隔で公開するのは難しい予感はしていますが、できるだけ間隔をあけないよう公開していきたいと思っていますので、気長に待ってやってください。
(こんな連載を待っている人が居るのかは疑問ですが…w)

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

IsExpanded をバインドして TreeView を全展開

現状、アプリを実行すると TreeViewItem が全て閉じた状態で起動していますが、全ての TreeViewItem が開いた状態で起動するよう変更します。

Windows Form であれば TreeView の ExpandAll() メソッドを呼ぶだけで TreeViewItem が全展開できますが、MVVM ではコントロールのメソッドを直接呼ばないので、TreeViewItem.IsExpanded プロパティをバインドすることで実現します。
TreeViewItem.IsExpanded プロパティをバインドするには、XAML へ TreeViewItem 用の Style を定義することで可能になるため、以下のハイライト行を追加します。

コントロールの子項目を設定するために Style を定義すると言うのは何となく違和感がありますが、『クラス型のコントロールプロパティを操作するには Style に定義する』と解釈しました。
又、Style の定義を Resources セクション内で行うのは以下のように XAML での決まりのようです。

スタイルを宣言する最も一般的な方法は、前の例で示したように XAML ファイルの Resources セクションのリソースとしてです。
スタイルとリソース - スタイルの基本 - スタイルとテンプレート【Microsoft Docs】

XAML の Resources セクションについてググると、「外観を共通化するために Resources へブラシを定義する」のような例が多く見つかると思いますが、上に書いた通り、クラス型のコントロールプロパティを設定する場合にも使用します。

又、上記コードでは IsExpanded プロパティと一緒に、IsSelected プロパティも追加していて、VM 側と値を双方向で同期するため、両方とも Mode = “TwoWay” を指定しています。

VM 側にも以下のハイライト箇所を追加します。

追加した ReactivePropertySlim 型のプロパティには XAML 側に設定した『Mode=”TwoWay”』と合わせてセッターも追加しています。
コンストラクタで IsExpanded プロパティの初期値を true に設定しているため、このまま実行するだけで下図のように TreeViewItem が全展開された状態で起動するようになります。

fig.1 TreeViewItem を全展開した MainWindow

続いて、アプリ起動時に MainWindow 右側へ表示する個人識別情報編集用の View を作成します。

編集画面 – PersonalDataEditView

このアプリで使用するデータオブジェクトには、episode: 4 で紹介した『個人識別情報』、『測定日別身体検査データリスト』、『試験日別得点データリスト』の 3 種類の編集対象データが含まれますが、このデータを編集する 3 種類の V と VM は全て 1 つの Prism Module(プロジェクト)内へ作成します。
(全てを別々の Prism Module として作成することは可能ですが、ここではそこまで分ける必要はないので 1 つの Module にまとめました)

新たに追加する Prism Module のプロジェクト名を『EditorViews』として、以下の 3 種類の View を新規で作成して追加します。

  • 『個人識別情報』編集用の PersonalEditor
  • 『測定日別身体検査データ』編集用の PhysicalEditor
  • 『試験日別得点データ』編集用の TestPointEditor

今回は、以下の『個人識別情報』編集用 PersonalEditor のみを紹介します。
(他の View はリポジトリを参照してください)

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

以下が作成した PersonalEditor の XAML です。
本来『Binding ~』を記述すべき部分は、とりあえずリテラル値を埋めています。

入力項目ラベルの幅として使用している Grid の列幅のみピクセル値で指定していますが、それ以外のコントロールはサイズを直接指定せず、土台となる UserControl の相対サイズで描画されるよう、Grid をネストしてレイアウトを作っています。
WPF での画面作成はまだ、あまり慣れてないので、単純な画面の割には結構ゴチャゴチャした XAML になっていて見にくいと思いますが、管理人もまだまだ試行錯誤中なので…

WPF でのレイアウトはこのようにデザインするのが正しいですよ的な意味ではなく、あくまで参考です。
上に貼ったキャプチャに近いレイアウトになっていれば、どんなパネル上にレイアウトしても問題ありません。
又、現時点で VM はテンプレートから作成したまま何も書かなくて OK です。

WPF では V → VM 間の通知に ICommand インタフェースが標準で用意されていますが、View から Command(イベント)を送信できるのは ICommandSource を実装しているコントロール(Button等)のみで、しかも特定のイベントにしか対応していません。

任意のイベントを送信するための代表的な方法として、Blend SDK に含まれる System.Windows.Interactivity.dll の InvokeCommandAction があり又、Prism にも Prism 独自の InvokeCommandAction と、ICommand インタフェースを実装した DelegateCommand クラスが含まれていますが、ここでは ReactiveProperty に同梱されている超強力な【EventToReactiveCommand】と【ReactiveCommand】を使用して TreeView.SelectedItemChanged  イベントをハンドルします。

EventToReactiveCommand は非常に強力で便利なクラスですが、情報自体は非常に少なく、ググっても 77 件しかヒットしません…(2018/12/8 現在)
最も信頼のおける情報としては、前回記事でも紹介した ReactiveProperty のメンテナーである、かずきさん自身のブログ『MVVMをリアクティブプログラミングで快適にReactivePropertyオーバービュー – かずきのBlog@hatena』の最後から 1/5 辺りにある『VからVMへのイベントの伝搬』へひっそりと、しかも応用的な使い方が書かれているだけなので、他にどのような使い方ができるかが分かり辛いですが、管理人的には『MVVM パターンで作成した View のイベントを EventArgs パラメータと併せて VM へ送信できる』つまり、『View のイベントをコードビハインドではなく VM に書くことができて、更に VM 側で EventArgs も受け取れる』と解釈しています。

EventToReactiveCommand の詳細は後述しますが、EventToReactiveCommand を使用するために、Blend SDK 付属の System.Windows.Interactivity.dll が必要なので準備します。

System.Windows.Interactivity.dll の準備

System.Windows.Interactivity.dll は一般的に Blend SDK に含まれていますが、Prism がインストール済みの環境であれば Prism の NuGet パッケージに含まれている System.Windows.Interactivity.dll が自動で参照に追加される場合があるようです

具体的には、プロジェクト配下の【参照】を開いて、Prism のアイコンが下のキャプチャのような青色のアイコンになっていれば、Prism に同梱されている System.Windows.Interactivity.dll が内部的に参照されている可能性が高いと思います。

fig.3 パッケージリファレンスの参照

参照アイコンが青色のものはパッケージリファレンスと言う形式らしく、Visual Studio 2017 で作成したプロジェクトで使用されるようです。
Visual Studio 2017 からプロジェクトファイルの形式が変更されたことに併せて、NuGet のパッケージ管理方法も変更された事で導入されたようですが、管理人は未だ変更内容を理解できていないので、詳しくは以下のページを参照してください。

参照形式がこのパッケージリファレンスの場合、対象のパッケージをクリックしてもプロパティウィンドウには何も表示されませんし、上記のように自動で参照に追加されるライブラリがあっても分からないので、管理人個人的にはあまり便利だとは思えませんが…

System.Windows.Interactivity.dll が参照済かを確かめるには、Prism.Wpf パッケージがインストールされている(例えば、NavigationTree プロジェクト)の XAML(ここでは NavigationTree.xaml)へ以下のように名前空間を追加する方法があります。

上記 5 行目のように、System.Windows.Interactivity.dll の名前空間を追加(名前空間を書いている最中に IntelliSense が働けば参照に追加されています)してもコンパイルエラーが出ない場合は、見えない所で参照に追加されていると考えて間違いないと思います。
(試しにビルドすると出力先フォルダに System.Windows.Interactivity.dll もコピーされます)

TreeView の SelectedItemChanged イベントをハンドル

System.Windows.Interactivity.dll への参照が存在することが確認できたら、NavigationTree の XAML へ以下のハイライト箇所を追加します。

EventToReactiveCommand を XAML で使用するために 7 行目へ『ReactiveProperty パッケージ』名前空間をへ追加しています。
そして、イベントをハンドルしたいコントロール(ここでは TreeView)へ Interaction.Triggers と EventTrigger クラスを追加して、EventTrigger.EventName プロパティへハンドルしたいイベント名をセットします。
又、EventTrigger 内に【EventToReactiveCommand】を追加して、VM 側のバインド先である  ReactiveCommand プロパティ名を指定すれば XAML 側の変更は完了です。

次に、イベントの受信側となる VM(NavigationTreeViewModel)へ以下のハイライト箇所を追加します。

14 行目で 宣言している ReactiveCommand の型パラメータに TreeView.SelectedItemChanged イベントの第 2 引数と同じ型の RoutedPropertyChangedEventArgs<object> を指定することで、VM 側で EventArges パラメータを受け取る事が出来るようになります。

又、37 行目の Subscribe() メソッドでイベントの購読先(Delegate先)を指定しています
ここでは Delegate 先に、VM の nodeChanged メソッドを指定していますが、Reactive Extensions や ラムダ式を使用して、Subscribe() メソッド内へ直接処理を記述することも可能です。
nodeChanged メソッドの中身は次章で紹介するので現時点では空のままにしています。

VM にイベントハンドラを書けるようになるのは助かりますが、XAML 側へ設定するイベント名や VM に指定するイベントパラメータの型名(ここでは RoutedPropertyChangedEventArgs)等は IntelliSense が利用できず、オブジェクトブラウザや Microsoft の .NET API ブラウザー等でわざわざ調べる必要があるので、結構面倒です。

又、.NET API ブラウザーの TreeView.SelectedItemChanged にはイベントパラメータの型は『RoutedPropertyChangedEventArgs<T>』と書かれていますが、ソースコードには『RoutedPropertyChangedEventArgs<object>』と書いています。
これは最初、管理人が動作テストしていた時は型パラメータを、RoutedPropertyChangedEventArgs<TreeViewItemViewModel> と書いて試しましたが、Invalid Cast Exception が出て止まってしまい、その時の型が『RoutedPropertyChangedEventArgs<object>』になっていたので合わせました。

これで VM 側でイベントハンドラがバリバリ書ける!とは思ったものの、実際イベントハンドラで EventArgs パラメータが無いと処理が書けない場面はあまり無い気がしてきました…
今回の場合でも、InvokeCommandAction でイベントの発生だけを受信して、別途 TreeView.SelectedItem にバインドしたプロパティを参照すれば同じことができるな…と今更ながら思い付きましたw

やはり MVVM ではイベントの発生だけを受信して、バインドしたプロパティを操作する方が王道なのかもしれませんが、やはり Windows Form に慣れ切った身としては、そう言う発想になかなか辿り着けず、ついイベントで考えてしまいます…

とは言っても、せっかくイベントがハンドル出来るようになったので、このままイベントで行きます。
nodeChanged メソッドは空のままでも構わないので、適当にブレークポイントを張って TreeView の選択ノードを切り替えると、選択した TreeViewItem の VM がイベントパラメータにセットされて取得できるのが確認できるはずです。

WPF + MVVM で EventArgs パラメータを VM で受け取れるのは EventToReactiveCommand だけと言う訳ではなく、他には以下のような方法があるようです。

[WPF 標準] マークアップ拡張.NET Framework 4.5 から対応。MarkupExtension から派生したクラスを作成する必要がありそう?【参考:SourceChord
[Prism] InvokeCommandActionEventArgs のメンバプロパティの内 1 つのみ渡すことは可能。
EventArgs を丸ごとは渡せない。
[MVVM Light Toolkit] EventToCommandEventToReactiveCommand とほぼ同じで、EventArgs を丸ごと渡すことが可能。

『Prism の InvokeCommandAction』のパターンのみ、管理人自身でコードを書いて取得できることは確認しましたが、他の 2 つは未確認です。
調べた内、EventToReactiveCommand を使って実装するのが 1 番楽そうだったので、今回は EventToReactiveCommand を採用することにしました。

続いて、選択したノードに対応した View へ画面遷移する方法を紹介します。

Prism での画面遷移

Prism では IRegionManager.RequestNavigate() を呼び出すと指定した View へ画面遷移(View の動的切替)することができるので、まずは以下のように、コンストラクタを修正します。

IRegionManager のインスタンスを取得するために、コンストラクタへ第 2 パラメータを追加して、Unity からインジェクションしてもらいます。

イベントの Delegate 先に指定した nodeChanged メソッドでは、受け取った EventArgs から TreeViewItem の VM を取得して、遷移先の View を特定しています。
そして、IRegionManager.RequestNavigate() メソッドのパラメータへ特定した View 名を指定して呼び出すと、MainWindow の右側に配置した『EditorArea リージョン』上の View が自動で切り替わるようになります。

以下は、RequestNavigate メソッドのパラメータに指定する内容です。

  • 第 1 パラメータには View 表示先のリージョン名を指定します。
  • 第 2 パラメータにはリージョンに表示する View 名を指定します。
    View 名は XAML ファイル名から拡張子以降(.xaml)を除いた文字列(つまり、クラス名)

これで画面の切り替えが出来そうなので、さっそく実行してみます。

fig.4 画面遷移失敗例

作成した個人識別情報編集 View が表示されません… orz

Prism では認識できないクラス(フレームワーク内に存在しないクラス)を指定した場合でも、例外が Throw されるのではなく、上図のようにとりあえず new した object を表示して例外の Throw を回避している場合も多いように感じます。(Prism 自体の実装は未確認)
管理人の経験的に、上図のような表示になった場合、新しく追加した部分が間違っていると言うより、必要な何かの設定が抜けていることが原因である場合が多いと感じています。

ここまで書くと分かる人も居るかもしれませんが、この記事の先頭でソリューションに追加した編集 View を含んだ Prism Module はどこからも参照されていません。
表示対象の View が Prism Shell 内に存在しない為、上記キャプチャのような表示になっています。
そのため、episode: 2 で NavigationTree Module を追加した際に記述したコードを、この EditorViews Module にも同様に追加する必要があります。

WpfTestApp プロジェクトに EditorViews Module の参照を追加して、App.xaml のコードビハインドへ以下のハイライト行を追加します。

実は足りないのはモジュールへの参照だけではなく、IRegionManager.RequestNavigate メソッドを使用するための必須設定も抜けています。
EditorViews.EditorViewsModule へ以下のハイライト箇所を追加します。

上記のように、IRegionManager.RequestNavigate メソッドに渡す全ての View を Navigation 用ビューとして Unity コンテナに登録する必要があり、忘れると上のキャプチャのように『System.Object』等と表示されてしまう結果になるので、忘れないようにしてください。
ここまで記述して実行すると、指定した編集 View が MainWindow の右側リージョンへちゃんと表示されるようになるはずです。
(編集 View の V – VM はまだ何もバインドしていないので下図は View の TextBox.Text プロパティへセットした値が表示されています)

fig.5 画面遷移成功例

ここまで記述すると、TreeView で選択したノードに対応した View に切り替れるようになります。

Prism で画面切替(画面遷移)するまでの手順を紹介するのに、連載記事 6 本(実質的には 3 本)を費やしてやっと辿り着きました!
episode: 2 を公開して 2 か月強くらいでしょうか… 結構長かったです。

管理人自身が Prism を使って動かしてみようと思って初めて情報を探した時、Prism について体系的に書かれた情報をあまり見かけなかった事がきっかけで、Prism 入門的な記事を書けば需要があるかと思い、この連載を書き始めました。
Prism だけではなく WPF 自体もあまり知らなかったので、その時調べた内容も Prism を説明する前提として記事の内容に含めたため、結局記事 3 本分も使うことになってしまいました。

ここまで書いてきた印象ですが、Prism の紹介で作成するサンプルアプリは、シンプルな 1 画面だけのダイアログのようなアプリでは Prism で出来る事の 1/10 も伝えられないんじゃないかとは思いますが、ネットで見かけるのはシンプルなサンプルで紹介しているような記事が大半で、Prism の公式サンプルに出会うまでは結構苦労した覚えがあります。
この連載内で作成しているようなサンプルアプリがまさに最適だ!等とは思っていませんが、Prism の紹介にはちょうど良い程度の規模だったと若干自負していますw

Prism 自体は想像するよりも楽に使えるライブラリであるにも拘らず、体系的に学習したり、「Prism をプロジェクトで使用するために要件を満たすのか?」を調べるには、あまりに情報が足りない現状(日本語での情報は特に)だと思っていますが、この連載を読んだ後に「Prism って高機能だけど結構簡単に使えそう!」的なことを感じてもらえれば、記事を書いた甲斐があります。

Prism とは関係ない情報や、管理人の無駄口的な文章も多かったと思いますが、実質的に Prism を動かすためのコード量は決して多くない事は分かってもらえたのではないでしょうか?

実は管理人的に、2019年からは WPF が若干盛り上がるんではないかと予想しています。
2017 年末の Xamarin WPF サポートに始まり、Windows Community Toolkit version 5.0 で WPF で UWP コントロールを動作させるための XAML Islands API 上に構築された WindowsXamlHost の発表。加えて、.NET Framework 4.8 からは WPF も WinRT API にフルアクセスできるという情報も見かけるので、来年以降は WPF 界隈がかなり賑やかになるような(なってくれれば良い)予感がしています。

何だか、「この連載はこれで終わります。」的な文章になってしまいましたが、まだ何回かは書きたいと思っていますし、一応ネタも考えています。
今回はここまでとして、次回は編集画面に編集データを表示するために、TreeView から Module へデータを受け渡す辺りを紹介する予定です。

又、今回作成したソースも GitHub リポジトリにアップしておきます。

【WPF episode: 7 ~ ReactiveProperty がバインドできないのはどう考えても Navigation が悪い!~】次回記事→

あわせて読みたい

コメントを残す

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