WPF Prism episode: 14 ~ ListBox 相手は ReactiveCollection、ダイアログな、Prism。 ~

前回記事「カスタマイズしたらメッセージボックスだった件【episode: 13 WPF Prism】」

 

前回は Prism 組み込みのメッセージボックスをカスタマイズする方法を紹介したので、今回は Prism でダイアログウィンドウを表示する方法を紹介します。

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

Prism のダイアログウィンドウを MVVM パターンで表示する

WPF でアプリを作成する場合、Windows Form と違ってダイアログはあまり使用せず、シングルウィンドウ 内で View を動的に切り替えるような UI が主流のように感じますし、この連載でも画面遷移と言えば Prism の IRegionManager.RequestNavigate を使用した View の切替しか紹介していません。

とは言えダイアログの表示が必要な場合も当然あると思います。Prism でもダイアログを表示する方法は用意されていて、前回のエントリ で紹介した自作メッセージボックスを表示する方法を応用してダイアログウィンドウも表示することができます。

前回のエントリ で作成したメッセージボックスはコードビハインドベースで作成しましたが、今回作成するダイアログは MVVM パターンで作成します。

この連載では episode: 3 から同じサンプルアプリに手を入れ続けてきましたが、今までのサンプルアプリではダイアログを表示するための良い例が思い付かないので新しくサンプルプロジェクトを作成しました。
あくまでも別ウィンドウを表示するためだけの簡単なサンプルアプリなので、今までのように作成方法を細かには説明しません。アプリを 1 から新規作成する方法は episode: 3 から連載物として公開しているので、そちらを読んでもらえるとありがたいです。

今まで紹介してきたサンプルアプリのソリューションに【PrismDialog】プロジェクトを新規で追加しているので、実行する際はスタートアッププロジェクトを【PrismDialog】に設定してください

Prism のダイアログウィンドウと複数行・複数列表示の ListBox

今回は最終的に下 fig. 1 のようなダイアログウィンドウを表示します。

fig. 1 作成するダイアログウィンドウ

実はダイアログウィンドウと言っても Prism ではダイアログウィンドウとメッセージボックスは同じものなので、前回のエントリで紹介した内容を応用するだけで作成できますが、それだけでは記事に取り上げる意味が無いので ReactiveCollection を ListBox へバインドする方法も併せて紹介します。

又、fig. 1 の画面は業務系ではよく見かけるポップアップする検索ダイアログですが、単純な単項目リストでは面白くないと思ったので 1 レコードを複数行・複数列で表示する ListBox にしました。

src. 1 は ListBox を置いた UserControl の XAML です。

Windows Form の ListBox で複数行・列表示するにはそれなりに手間でしたが、WPF では List 系コントロールの ItemTemplate を Grid 等で行・列方向に分割してコントロールを配置するだけで簡単に複数行・複数列の表示ができますし、表示系コントロール以外にもボタンや TextBox 等の入力系コントロールを置くこともできます
複数行・複数列構成の List を簡単に作成できるのは Windows Form と比べて WPF の大きなアドバンテージだと思います。
※ 但し、ListView も DataGrid もヘッダの複数行・列表示はできません。

検索ダイアログの VM です。

まず、メッセージボックスを自作した時と同じく VM で IInteractionRequestAware インタフェースを継承して、ListBox の ItemsSource、SelectedItems とバインドするプロパティも作成しています。

又、ListBox の SelectedItem プロパティと MouseDoubleClick をバインドしてダイアログ起動時に ListBox の 1 行目を選択して、選択項目をダブルクリックすると選択項目が確定されるようにしています。
ワンポイント MouseDoubleClick イベントをハンドルする場合は、ListBox.HorizontalContentAlignment=”Stretch” を設定しないとマウスに反応しないエリアができてしまうので併せて設定することをお勧めします。

そして、XAML 側でキャンセルボタンの IsCancel を true に設定すると Windows Form の場合と同じくボタンクリックでダイアログが閉じるので、画面をただ閉じるだけであればキャンセルボタンの Command はバインド不要です。
(これはメッセージボックスの場合も同じ)



ReactiveCollection を List 系コントロールにバインドする rev.2

episode: 5 で紹介したように List 系コントロールとバインドする場合は、項目単位にバインドする VM(ここでは SearchItemViewModel)も作成しますがここでは ReadOnly の項目にバインドするだけの単純なものなので、GitHub リポジトリ で見てください。

List 系コントロールとのバインドは基本的に episode: 4.5 で紹介した場合と同じく、ItemsSource と ReactiveCollection をバインドしますが、episode: 4.5 で紹介しているサンプルコードは ReactiveCollection を使用する例として適していなかったと後悔してるので改めて紹介します。

src. 3 は ダイアログの VM からコンストラクタの部分のみ抜粋しました。

episode: 5 で紹介した通り、一般的な MVVM パターンでは List 系コントロールの各項目用 VM と Model が 1 対 1 になるように作成するので、ソース変数に指定する List のメンバ(Model)と View に公開する List のメンバ(VM)の型が違うことは多々あります。
そのため、ReactiveProperty には ObservableCollection → ReadOnlyReactiveCollection に変換するための ToReadOnlyReactiveCollection 拡張メソッドが用意されています

上のサンプルコード 9 ~ 10 行目のように書くことで ObservableCollection(bleachCharacters フィールド)を SearchItemViewModel(Characters プロパティ)に変換できます。

この変換ができて何が嬉しいかと言うと、例えばダイアログへ【追加ボタン】を追加して VM へ src. 4 のハイライト部を追加します。

addButtonClick メソッド内で bleachCharacters(ObservableCollection<Character>)へ Model を追加するだけで Characters プロパティ(ReactiveCollection)にもメンバが追加され、以下のように View にも追加されます。

fig.2 List 系コントロールへメンバを追加。

このような動作になるのは ReactiveCollection 初期化時に指定する ToReadOnlyReactiveCollection のパラメータに以下のような変換式を指定しているからです。

.ToReadOnlyReactiveCollection(c => new SearchItemViewModel(c))

上記の変換式を指定していると、bleachCharacters フィールドへメンバを追加した時 Characters プロパティ側にもメンバを自動で追加してくれます

上記のような変換式を書きやすくするため、子項目用 VM のコンストラクタパラメータへ Model を定義することが必然的に多くなります
(ReactiveProperty を使用しない場合でも構造を考えればそうなるのが当然だと思います)

ReactiveProperty の罠

前回エントリまで使用していたサンプルプロジェクトの Model は BindableBase を継承して作成していましたが、今回新たに追加した PrismDialog サンプルプロジェクトの Model は今までと異なり、バインドプロパティを ReactiveProperty で定義していますが、ダイアログの VM を書いていた時、以下のコードでドハマリしました… orz

this.bleachCharacters = new ObservableCollection<Character>(charaList.OrderBy(c => c.Code.Value));

別に何てことは無い単なる Linq の OrderBy ですが、太字にした部分の【.Value】を非常に!ひじょうに!ひっじょうに!よく忘れます!

上のコードで【.Value】を付け忘れると実行時例外:「System.ArgumentException: ‘少なくとも 1 つのオブジェクトで IComparable を実装しなければなりません。’」が飛んで来るので、Linq の仕様が変わったのかと思って小一時間悩みましたw
ReactiveProperty は使っていることを忘れるくらい直感的で出来の良いライブラリなので Model に使用する場合でもあまり意識せず使うことができるので、使っていることをついつい忘れてしまいがちです。
Linq 等の引数に ReactiveProperty を使った Model を指定して、よく分からない実行時エラー等が出た場合は【.Value】の付け忘れが無いか見直すと良いかもしれません

ReactiveCollection の使いどころ

Model → 項目用 VM の同期もできて使い勝手が良さそうな ReactiveCollection ですが、使用するのは VM のバインドプロパティに限定するのが良い気がしています。(あくまでも現時点での管理人個人的な意見です)

そう思った 1 番の理由は Linq の戻り値を ReactiveCollection に代入できない点です。
例えば取得した Model の List を VM のフィールド(ReactiveCollection 型)へ src. 5 のように代入したくても上手くいきません。

BleachAgent.GetAllCharacters() メソッドは List<Character> を返すので、① のように代入したいと思っても ToReactiveCollection 拡張メソッドは無いのでコンパイルエラーになります。

② のようにすれば ReactiveCollection への代入はできますが ③ のように代入後に Linq で加工しようとすると、これもコンパイルエラーになります。
そして ④ のようにプロパティにセットしようとしてもこれもコンパイルエラーになります… orz
管理人がやり方を知らないか、間違えているだけかもしれませんが、このように ReactiveCollection を一時保管用の変数として使用するのは少し取り回ししずらい印象を受けました。

そのため、VM のフィールドや Model の List 型プロパティには ReactiveCollection より ObservableCollection を使う方が融通が利きそうだと思っています。

ここまでは Prism から表示するダイアログ本体の作成方法を紹介してきたので、次はダイアログの呼出元ウィンドウの作成について紹介します。

ダイアログの呼出元親 Window

ダイアログの呼び出し元である MainWindow(Prism の Shell)には業務系では見かけることも多い、コードを入力すると名称を表示する UI を下 fig.3 のように配置しています。

fig.3 ダイアログ呼出元の親 Window

TextBox にコードを入力すると右側の TextBlock に名称が表示され、【検索ボタン】を Click するとダイアログが表示されます。最初に書いた通り Prism ではメッセージボックスとダイアログは同じものなので、ダイアログの表示方法は基本的に episode: 12 で紹介した内容と同じですが、INotification(IConfirmation)を継承したクラスは親画面とデータをやり取りするためのプロパティ等を定義する必要があるため、通常はダイアログごとに作成します。

ダイアログと呼出元親 Window 間でデータを受け渡す役目の INotification

検索結果ダイアログ用の INotification として ISearchDialogNotificationSearchDialogNotification を作成します。この Notification はダイアログで選択した項目を取得・設定するための SelectedCharacter プロパティを 1 つ定義しているだけのシンプルなクラスなので、GitHub リポジトリ で見てください。
一言メモ インタフェースの作成は必須ではありませんが作成しておくとユニットテストの作成が楽になります。

ダイアログの表示には episode: 12 で紹介した Service を使用しますが、メッセージボックス表示 Service とは別に scr. 6 のような Service クラスを新規で作成しました。

内部の処理は episode: 12 で作成したメッセージボックス表示 Service と変わりませんが、ダイアログ表示用と分かるようなメソッド名に変更しています。
※ この Service は別のダイアログ表示にも使えるようインタフェースは汎用的な型で定義しています

又、MainWindow の VM からダイアログを呼び出す方法も episode: 12 と同じくコンストラクタへインジェクションした Service から ShowDialog メソッドを呼び出すので、ソースコードは GitHub リポジトリで見てください。

MainWindow の XAML に指定する PopupWindowAction は episode: 12 で紹介した設定と全く同じで、違うのは PopupWindowAction.WindowContent に指定するダイアログ用の View 名のみです。

実行して MainWindow の検索ボタンを Click するとダイアログが表示されます。
(以下はダイアログを 2 回表示しています)

fig.4 ダイアログウィンドウの表示

WindowContent に指定したダイアログの生存期間

上のキャプチャをよく見ると 2 回目にダイアログを表示した時(キャンセルボタン Click 後の再表示)、ListBox の選択位置が先頭ではなく前回選択した位置から変わっていません。VM のコンストラクタで SelectedItem に List の先頭項目をセットしているのに選択位置は前回のままです。

これには当然原因があって、SearchDialogViewModel のコンストラクタへブレークポイントを張って実行すると一目瞭然ですが、Prism から表示するダイアログは MainWindow(ダイアログの呼び出し元)表示前にインスタンスが作成され、それをそのまま使い回しています。
Prism のダイアログは Windows Form と違ってダイアログを表示する度に新規インスタンスを作成しないので、コンストラクタに記述した処理も 1 度しか呼ばれないことが選択位置がリセットされない原因です。

Prism ではそれを回避するため『公式サンプル No.28 CustomRequest』のように INotification へバインドプロパティを定義する方法があります。

今回のエントリで紹介した PrismDialog サンプルの場合、VM へ定義していたバインドプロパティを INotification へ移して XAML のバインド定義を修正すると対応できます。
基本的に VM には IInteractionRequestAware インタフェースのメンバと OK ボタンの Command のみ残して他のバインドプロパティは INotification へ移す形になります。

INotification はダイアログを表示する度に毎回呼出元で new するので、INotification にバインドプロパティを定義すると毎回初期化されますが、その方法は Prism の公式サンプルを見て頂くとしてここでは紹介しません。
実はダイアログのインスタンス生成タイミングをコントロールする方法はもう1つあります。

WindowContentType に指定したダイアログの生存期間

Prism からダイアログを表示する際、今までは XAML の【WindowContent】へ View 名を指定していましたが、【WindowContentType】へ指定することもできます

Prism 内部でダイアログをどのように扱っているかは『PopupWindowActionのコンテンツ指定方法WindowContentとWindowContentTypeで挙動が異なる – Qiita』に書かれていますが【WindowContentType】に表示する View 名を設定すると、InteractionRequest.Raise を呼び出した後(ここではダイアログ表示サービスの ShowDialog メソッド呼出し後)にインスタンスが作成されるようになります

src. 8 は MainWindow.xaml の PopupWindowAction の部分を抜粋したものですが、ハイライト部を変更します。

WindowContent に指定していた部分はとりあえずコメントアウトして、10 行目の【WindowContentType】にダイアログの View を指定してサンプルアプリを実行すると下 fig. 5 のように動作が変わります。

fig.5 ListBox の選択位置がリセットされるようになったダイアログ

ダイアログ起動時の ListBox は常に先頭項目が選択されるような動作に変わっています。
このようにダイアログを【WindowContent】に設定した場合と【WindowContentType】に設定した場合でダイアログの生存期間が変わるので注意が必要です。

WindowContent に設定する意味が無いように感じる人もいるかもしれませんが、WindowContent へ設定すると呼出元画面と同時に Window(ダイアログの枠部分)が Load されているため、InteractionRequest.Raise を呼び出すと一瞬で表示されるメリットがあります。
そのため、メッセージボックスの表示には WindowContent へ設定するほうが適していると言えますが、その分メモリは余計に消費するとも言えます。

管理人個人的にはメッセージボックスのように単純な画面で瞬時に表示したいダイアログを指定する場合は【WindowContent】。ダイアログウィンドウとしていくつかの操作が含まれている場合は【WindowContentType】に設定して、あとは表示速度と消費メモリのトレードオフで決めれば良いと思っています。

Prism 7.2 以降のダイアログ

episode: 12 からこのエントリまで 3 回に渡って Prism の InteractionRequest で別ウィンドウを表示する方法を紹介してきましたが、実は現時点(2019/4/24)で IInteractionRequestAware、PopupWindowAction、INotification 等今まで紹介したダイアログ表示関連のクラスに【Obsolete】が付加されています

これは Prism 7.2 から IDialogService の導入が予定されているからで、Prism 7.2 以降ではメッセージボックスを含むダイアログの取り扱いがガラッと変わるようです。
ただ、IDialogService が導入されたとしても PopupWindowAction 等は完全に廃止される訳ではなく、IDialogService と並行して使用できるようです。

Prism 7.2 は 2019/2/21 に Preview 版がリリース されていますが、2019/4/24 時点での進捗は 33% となっているので本リリースは 1 ~ 2 か月程度先かな?と予想しています。(管理人の個人的な予想です)

Prism 7.2 が正式リリースされた後はここでも IDialogService についてのエントリを書こうとは思っていますが、Preview 版でダイアログを表示する情報が『Prism7.2(pre)の新機能 IDialogServiceを試してみた – Qiita』で紹介されています。

 

今回で 3 回目となった Prism から別ウィンドウを表示する方法は次回、Prism からシステムダイアログを表示する方法で最終章となる予定です。

 

そしていつものように今回紹介したコードも GitHub リポジトリ へ上げています。

 

次回記事「FolderBrowserDialog の為ならば、Prism の InteractionRequest はもしかしたらコモンダイアログも Popup できるかもしれない。【episode: 15 WPF Prism】」

 

 

おすすめ

コメントを残す

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

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