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

← 前回記事【WPF Prism episode: 5 ~ TreeView の MVVM には ReactiveProperty が埋まっている ~】

前回は ReactiveProperty を使用して TreeView へアイコン付きの TreeViewItem を表示するまでを紹介したので、今回は WPF の イベントを Command へバインドする方法を紹介します。

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

WPF の Command とイベント

episode: 4.5 でも紹介しましたが、この連載で作成するサンプルアプリの MainWindow は下 fig. 1 のように TreeViewItem を選択すると対応した View を表示する画面です。

fig.1 サンプルアプリの完成予定画面

Windows Form で fig. 1 のように選択した TreeViewItem に対応した View に切り替えたい場合、TreeView の NodeMouseClick イベント等をハンドルして画面を切り替えると思いますが、WPF でも同じで TreeView のイベントを利用して View を切り替える方法を紹介します。

WPF の ICommand インタフェース

WPF にもイベントはあって、Windows Form と同じようにコードビハインドへ処理を書けばイベントをハンドルすることもできますが、MVVM パターンで作成する場合は基本、コードビハインドに処理を書かないので ICommand インタフェースをバインドしてイベントを処理することになります。
WPF の Command と Windows Form のイベントは似ているようで違う全くの別物なので、Windows Form での開発とは考え方を少し変える必要があります。

WPF 標準の Command については『++C++; // 未確認飛行 C』を主宰されている岩永 信之さんが書かれた『WPF入門:第6回 「コマンド」と「MVVMパターン」を理解する (2/3) – @IT』が詳しいと思いますが、Prism や ReactiveProperty を使う場合、完全に理解していなくてもとりあえずは大丈夫だと思います。

ですが、この ICommand インタフェースは、Windows Form のイベントと比べてなかなかの曲者で Command を送信できるのは ICommandSource を実装した Button 等のコントロールだけで、しかも特定のイベントにしか対応していないと言う縛り付きです。

では、TreeView のように Command が定義されていないコントロールのイベントをハンドルしたい場合はどうするかと言うと Blend SDK に含まれる System.Windows.Interactivity.dll を参照する必要があります。

System.Windows.Interactivity.dll とは

Visual Studio 2017 Community(以降 VS 2017) で System.Windows.Interactivity.dll を参照するには、『Visual Studio 2017 で System.Windows.Interactivity.dll が見つからない – Qiita』に書かれているように Visual Studio Installer から Blend for Visual Studio SDK for .NET を追加するようですが、Visual Studio 2019 Community(以降 VS 2019)ではインストール時に Blend のチェックを外さない場合は Blend for Visual Studio 2019 も同時にインストールされるため、個別コンポーネントとして追加する必要はなさそうです。(VS 2019 の個別コンポーネントに Blend for Visual Studio SDK for .NET はありませんでした)

但し、Prism でアプリを作成する場合は System.Windows.Interactivity.dll を別途参照する必要はありません

Prism Template Pack からアプリのテンプレートを作成した場合、Nuget から Prism を復元しますが、VS 2017 では fig. 2 のように Prism の参照は表示されて System.Windows.Interactivity.dll は表示されていません。

fig.2 プロジェクトの参照

参照に表示されていなくても以下のように XAML へ名前空間を追加すると IntelliSense の候補に出てくるので Prism に同梱されている System.Windows.Interactivity.dll が裏で自動的に参照へ追加されると思われます

fig.3 Interactivity 入力時の IntelliSense

青色の参照アイコンはパッケージリファレンスと言う形式らしく、管理人はあまり理解できていないので詳しくは以下のブログを参照してください。

ですが、VS 2019 ではパッケージリファレンスの仕組みも変わったのか、Nuget から Prism を復元すると下 fig. 4 のように Blend.Interactivity.Wpf が参照に表示されるようになっています。

fig.4 Visual Studio 2019 での参照

このように System.Windows.Interactivity.dll への参照が存在するプロジェクトでは任意のイベントを Command へバインドできるようになります

WPF でアプリを作る場合、こう言う所が非常に中途半端な印象を受けます。
Windows Form で散々イベントドリブンでの開発を推し進めてきたのに素の状態だと使えないイベントがありますよ!と言うのはイマイチ納得がいきません。
内部的にはどのような構成になっていても構わないと思いますが、どんなイベントも Command でバインドできるようになっていれば良いのに、そうなっていないのは中途半端に投げ出したような印象を受けます。

EventTrigger で任意のイベントをハンドルする

Prism と ReactiveProperty をインストールしている場合、任意のイベントを Command へバインドする方法は以下の 3 通りから選択できます。

  • System.Windows.Interactivity.dll の InvokeCommandAction を利用する
  • Prism に含まれる同名の InvokeCommandAction を利用する
  • ReactiveProperty の EventToReactiveCommand を利用する

ここでは System.Windows.Interactivity.dll の InvokeCommandAction を利用する方法は紹介しませんが、WPF 4.5(.NET Framework 4.5)以降ではマークアップ拡張という方法で EventArgs パラメータを受け取れるようになったようなので、詳しくは『WPF4.5の新機能~「イベントのマークアップ拡張」で、イベント発生時のコマンド呼び出しをスッキリ記述する~ – SourceChord』を参照してください。(管理人は未確認)

任意のイベントを Command へバインドには、XAML を src. 1 のように変更するとできるようになります。
(src. 1 は Prism の InvokeCommandAction を利用した例です)

Interaction.Triggers ~ EventTrigger までは XAML でイベントをハンドルするための決まり文句で、EventName へハンドルしたいイベント名を指定するとイベントを Command へバインドできるようになります。

EventName への入力は IntelliSense のサポートを受けられないため、オブジェクトブラウザや Microsoft の .NET API ブラウザー 等で調べた正式な名前を指定する必要があるのは少し面倒です。

Prism の InvokeCommandAction で TreeView の SelectedItemChanged をバインドする

src. 1 から EventTrigger の部分を抜粋しました。

Prism の InvokeCommandAction へ設定する値は以下の通りです。

Command
イベントのバインド先となる VM へ定義した Command 名を設定します。
TriggerParameterPath
ハンドルしたイベントパラメータメンバの内、任意のプロパティ名を 1 つだけ指定すると、VM 側で値が受け取れるようになります。
TriggerParameterPath は任意指定なのでイベントパラメータの値が不要な場合は省略可能です。

ここでは TreeView.SelectedItemChanged イベントをハンドルするので、TriggerParameterPath にイベントパラメータである RoutedPropertyChangedEventArgs<T> クラスの NewValue プロパティを指定すると VM で NewValue プロパティの値が受け取れるようになります。

TriggerParameterPath に指定する名前は EventTrigger.EventName と同じく正式な名前を指定する必要があるので、オブジェクトブラウザや Microsoft の .NET API ブラウザー 等で調べた名前を指定してください。

コマンドをバインドする VM は src. 3 のようになります。

Command を 1 つ追加しただけで結構な行数が増えますが、Prism Template Pack をインストールしている場合は『cmdgfull』コードスニペットが使えるので試してみてください。

ExecuteSelectedItemChanged がイベント(Command)を受け取った際の処理を記述する部分で、CanExecuteSelectedItemChanged が Command の実行可否を記述する部分です。
今回バインドした SelectedItemChanged は常に実行可能な Command なので CanExecuteSelectedItemChanged は省略可能です。(コードから削除しても OK)

『cmdgfull』コードスニペットは一見便利ですが、必要ないフィールドまで作成されるのは逆に不便かもしれません。
17、41 行目でコメントアウトしている記述に変えれば selectedItemChangedCmd フィールドは不要になります。

ここの Command は Delegate した先で実行するよう指定していますが、42 行目へ直接ラムダ式を記述することも可能です。又、ExecuteSelectedItemChanged へ記述する内容は次回のエントリで紹介します。

EventToReactiveCommand で TreeView の SelectedItemChanged をバインドする

ReactiveProperty に含まれる EventToReactiveCommand は非常に便利で強力なクラスですが、情報自体は非常に少なく、ググっても 77 件しかヒットしません…(2018/12/8 現在)

おそらく最も信頼のおける情報は、かずきさん主宰のブログエントリ『MVVMをリアクティブプログラミングで快適にReactivePropertyオーバービュー – かずきのBlog@hatena』の最後から 1/5 辺りにある『VからVMへのイベントの伝搬』だと思っていますが、詳細な使用法が書かれている訳ではなくどちらかと言うと応用的な使い方が書かれているだけです。

情報が少ないながらも MVVM Light Toolkit の EventToCommand を参考に試してみるとちゃんと動作したので Command を ReactiveCommand へバインドする方法を紹介します。

Prism の InvokeCommandAction の場合と同じく src. 4 のように EventTrigger へ EventToReactiveCommand を追加します。

XAML で EventToReactiveCommand を呼び出すために 4 行目 へ Reactive.Bindings.Interactivity 名前空間のエイリアスが必要なので追加しています

そして、EventToReactiveCommand の Command にも Prism の InvokeCommandAction と同じくイベントのバインド先の Command 名を指定しますが、TriggerParameterPath と同じようなプロパティは存在しません。

プロパティが存在しないと言ってもイベントパラメータが受け取れない訳ではなく EventToReactiveCommand の場合は src. 5 のように VM 側で指定します

EventToReactiveCommand でハンドルしたイベントは読んで字のごとく ReactiveCommand へ変換されるので、14 行目へ ReactiveCommand(ここでは SelectedItemChanged)を宣言しています。

Prism の InvokeCommandAction はイベントパラメータのメンバプロパティを 1 つだけ受け取ることができましたが、EventToReactiveCommand はイベントパラメータ自体を丸ごと渡すことができます

ReactiveCommand でイベントパラメータを受け取るには src. 5 の 14 行目のように型パラメータへイベントパラメータと同じ型を指定すると受け取ることができるようになります
イベントパラメータが必要ない場合は型パラメータ無しで宣言します。

前項でも紹介した通り TreeView.SelectedItemChanged イベントのパラメータは RoutedPropertyChangedEventArgs<T> で、型パラメータへ実際に設定されるのは TreeViewItemViewModel ですが、EventToReactiveCommand で RoutedPropertyChangedEventArgs<object> に変換されます

管理人が動作テストをした時は、型パラメータへ TreeViewItemViewModel と書いて試しましたが、Invalid Cast Exception が Throw されました。EventToReactiveCommand の内部処理的に何が指定されるか分からないのであれば object で宣言するのは自然の流れだと思います。
管理人の場合、例外が出た時に型を確認して object になっていたので気付いたんですが…

ReactiveCommand も ReactiveProperty と同様に初期化が必要で、宣言と同時の初期化も可能ですが、Reactive Extension で色々できるコンストラクタで初期化するのがお勧めです。

ReactiveCommand では IObservable<bool> から生成することで実行可否を制御することも可能ですが、Prism の InvokeCommandAction の場合と同じく SelectedItemChanged イベントは常に実行可能な Command として定義するため単純に new しています。
IObservable<bool> から生成する例は次回以降のエントリで紹介したいと思います。

そして、初期化時に呼び出している AddTo メソッドは ReactiveProperty の場合と同じく一括で Dispose するために呼び出しています。
最後に Command の実行先は Subscribe メソッドで Delegate 先を指定するか、ラムダ式で処理内容を記述するのも Prism の InvokeCommandAction の場合と同じです。

src. 5 では ReactiveCommand の初期化とコマンドの購読先を指定する Subscribe メソッドを 2 行に分けて書いていますが、ReactiveProperty Ver. 4.0.0 で WithSubscribe 拡張メソッドが追加されているので src. 6 のようにまとめて書くことができるようになっています。(つい最近まで知りませんでした)

追記: 2019/8/10

又、Command の実行先となる nodeChanged 内の記述についてはエントリを改めて紹介します。
nodeChanged メソッドの中身は空のままでも構わないので、適当にブレークポイントを張って TreeView の選択ノードを切り替えると、選択した TreeViewItem の VM がイベントパラメータにセットされるのが確認できるはずです。

src. 5 のように Command の実行先に Delegate を指定した場合、fig. 5 のような『ラムダ式 はデリゲート型ではないため、型 ~ に変換できません。』と言うようなコンパイルエラーが出る場合があります。

fig.5 Delegate 先を指定すると発生するコンパイルエラー

毎回忘れて調べまわってしまいますが、実は簡単に解決できます。

コンパイルエラーを消すには【using System; を追加する】ことです。

新しく追加したコードファイル、例えば Prism の ViewModel テンプレートから新規追加したコードファイル等へ書いているとたまに発生するので、そんな場合は【using System;】するだけであっけなく解決する場合があります。

追記: 2019/6/15

Prism と ReactiveProperty の棲み分け

ここまで Prism と ReactiveProperty の両方でコントロールの任意のイベントをハンドルする方法を紹介してきましたが、機能が重複する部分も多くあります。
この連載での棲み分けとしては、アプリ全体のインフラに関わる部分は PrismVM ⇔ View 間のバインドに関わる部分は ReactiveProperty。という形で棲み分けします。

但し、VM ⇔ View 間のバインドに関わる部分でも ReactiveProperty をインストールしない(できない)場合もあると思うので、補足が必要そうな場面では Prism での方法も紹介する予定です。

 

WPF のイベントをハンドルする方法はここまでとして、次回は Prism で View を動的に切り替える方法を紹介する予定です。
又、今回作成したソースも GitHub リポジトリにアップしておきます。

 

 

【WPF Prism episode: 6.5 ~ いつだって Prism の画面遷移は RequestNavigation だった。 ~】次回記事 →

 

あわせて読みたい

コメントを残す

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

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

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