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

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

 

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

2020/11/11 追記
WPF コントロールのイベントと ReactiveCommand については、現在新規連載中の .NET Core WPF Prism MVVM 入門 2020 step: 8 でも詳しく紹介しているので、良ければそちらも見てください。

2020/8/20 追記
PrismでDispose – 雲の亀日記』から来られた方へ
このエントリでは Shell や部分 View を Dispose する方法は紹介していません。

Shell や部分 View の表示・破棄は現在連載中の .NET Core WPF Prism MVVM 入門 2020 step: 5 で紹介しているのでそちらを見てください。

尚、この記事は 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 リポジトリにアップしておきます。

 

 

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

 

 

おすすめ

8件のフィードバック

  1. てすと より:

    スタートアッププロジェクトを WpfTestApp に変える試してみました
    最終的には14_episode16以降は動くけど他は上にあるエラーの
    System.IO.FileLoadException: ‘ファイルまたはアセンブリ ‘Unity.Abstractions, Version=3.3.1.0, Culture=neutral, PublicKeyToken=6d32ff45e0ccc69f’、またはその依存関係の 1 つ
    ~~のエラーが発生したのでUnity.Abstractions, Version=3.3.1を入れるとに

    System.IO.FileLoadException: ‘ファイルまたはアセンブリ ‘Unity.Abstractions, Version=4.1.3.0, Culture=neutral, PublicKeyToken=489b6accfaf20ef0’、またはその依存関係の 1 つ
    ~~エラーが発生したのでUnity.Abstractions, Version=4.1.3を入れると

    System.IO.FileLoadException: ‘ファイルまたはアセンブリ ‘Unity.Abstractions, Version=3.3.1.0, Culture=neutral, PublicKeyToken=6d32ff45e0ccc69f’、またはその依存関係の 1 つ
    ~~ループとなり駄目でした

    >最新パッケージに更新することも検討したいと思います。
    是非ともお願いします。
    個人的には.NET Core 3.0 以上 と C# + Prism 7.2 以降の環境で作りたいのですが
    最新の記事をみると最初のほうの内容はPrism 7.2では廃止されていたりより簡潔に書ける等episodeを最初から追いかけていくと混乱しそうだったので
    Prism 7.2 以降で使うなら参考にできるサンプルだけでも更新されているとありがたいです。

    • 沖田玲朗 より:

      てすとさん
      返信遅くなりました。

      GitHub リポジトリの master ブランチ 02_episode03_Prism7.1 ~ 13_episode15 を全て以下の環境に更新しました。
      ※ 01_episode03 のみ Prism 6.3

      • Visual Studio 2019
      • .NET Framework 4.8
      • Prism 7.2

      参照を Prism 7.2 に単純に変更するだけではビルドが通らなかったのでプロジェクトを作り直したりしているため足らないファイルなどがあるかもしれませんが、その場合はお手数ですがコメントお願いします。
      一応、ビルドが通ることと、実行して画面が表示されるまでは確認しているので今度は大丈夫だと思います。

      • てすと より:

        ~13_episode15でビルドが通ることを確認しました。
        最初の記事から参考にしたいと思います。
        環境更新ありがとうございます。本当に助かりました。

        • 沖田玲朗 より:

          てすとさん
          対応が必要なプロジェクトが思っていた以上に多くて手間取ってしまいました。
          遅くなってすいませんでした。
          今後ともよろしくお願いします。

  2. てすと より:

    vs2019でサンプルをビルドしてみたところNavigationTreeでエラーがでるのですがこれは2019だからなのでしょうか?

    System.IO.FileLoadException: ‘ファイルまたはアセンブリ ‘Unity.Abstractions, Version=3.3.1.0, Culture=neutral, PublicKeyToken=6d32ff45e0ccc69f’、
    またはその依存関係の 1 つが読み込めませんでした。見つかったアセンブリのマニフェスト定義はアセンブリ参照に一致しません。 (HRESULT からの例外:0x80131040)’

    • 沖田玲朗 より:

      念のため確認ですが、エラーが出ているのは『04_episode06\WpfTestApp.sln』で間違いないでしょうか?
      Prism がちゃんとインストールされていないことが原因だと考えられます。
      ソリューションの Nuget パッケージの管理を開いて以下の赤枠で囲んだバージョンの Prism はインストールされていますでしょうか?

      又は Visual Studio のメニュー [ツール] – [オプション] – [Nuget パッケージマネージャー] を開いて赤枠で囲んだ項目にチェックした後でビルドするとどうでしょうか?

      • てすと より:

        返信ありがとうございます一度全部ビルド試してみたところ
        14_episode16~18_episode20、QA_MvvmSampleApp、QA_VmLoadTest
        はビルドできました他のは以下のエラーがでてるので古いのが駄目そうな感じ?
        一度動くのを参考にして試してみようと思います

        13_episode15
        12_episode14
        11_episode13 は
        重大度レベル コード 説明 プロジェクト ファイル 行 抑制状態
        エラー CS0246 型または名前空間の名前 ‘ReactivePropertySlim’ が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)。 WpfTestAppModels D:\visualstudioソフトテスト\WpfPractises-master\11_episode13\WpfTestAppModels\SampleItem.cs 12 アクティブ
        エラー CS0246 型または名前空間の名前 ‘Reactive’ が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)。 WpfTestAppModels D:\visualstudioソフトテスト\WpfPractises-master\11_episode13\WpfTestAppModels\SampleItem.cs 6 アクティブ

        10_episode12
        09_episode11
        07_episode09
        06_episode08
        05_episode07 は
        —————————
        Microsoft Visual Studio
        —————————
        クラス ライブラリの出力タイプを持つプロジェクトを直接起動することはできません。

        このプロジェクトをデバッグするには、ライブラリ プロジェクトを参照するこのソリューションに実行可能なプロジェクトを追加し、それをスタートアップ プロジェクトとして設定します。
        —————————
        OK
        —————————
        重大度レベル コード 説明 プロジェクト ファイル 行 抑制状態
        エラー NU1101 パッケージ Extended.Wpf.Toolkit が見つかりません。ソース Accses, Core3, csvherper, entityframework, entityframeworkCore, Microsoft Visual Studio Offline Packages, myget prism, prsim, UnityWPF には、この ID のパッケージが存在しません。 WpfTestApp D:\visualstudioソフトテスト\WpfPractises-master\09_episode11\WpfTestApp\WpfTestApp.csproj 1

        04_episode06
        03_episode05
        02_episode04
        02_episode03_Prism7.1 は
        System.IO.FileLoadException: ‘ファイルまたはアセンブリ ‘Unity.Abstractions, Version=3.3.1.0, Culture=neutral, PublicKeyToken=6d32ff45e0ccc69f’、
        またはその依存関係の 1 つが読み込めませんでした。見つかったアセンブリのマニフェスト定義はアセンブリ参照に一致しません。 (HRESULT からの例外:0x80131040)’

        • 沖田玲朗 より:

          てすとさん
          確認していただいてありがとうございました。

          1. episode: 7 ~ 9、11、12
          これはスタートアッププロジェクトがクラスライブラリに変わっているだけなので、スタートアッププロジェクトを WpfTestApp に変えるだけでビルドは成功するはずです。

          2. episode: 3 ~ 6
          上記のソリューションは Prism が 7.1 以前のバージョンを参照しているのが理由だと思います。

          3. episode: 13 ~ 15
          この辺りで ReactiveProperty に破壊的な変更が入ったのでその影響だと思います。

          上記 2、3 はソリューションを開いた後、『Nuget パッケージの復元』を実行すると元々参照していたバージョンが Nuget からインストールされるはずだと思うので、GitHub から再度ソースを取得していただいて復元を実行すると正常にビルドされると思います。

          ただ、現時点で GitHub に上がっているソリューションは作成時から更新していないので、最新パッケージに更新することも検討したいと思います。

沖田玲朗 へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

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