WPF Prism episode: 8 ~とある TreeView の状況一覧 (Context menu) ~

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

前回は遷移先画面に実装した Prism.INavigationAware インタフェースの OnNavigatedTo メソッド経由でデータオブジェクトを渡して ReactiveProperty で View へバインドする方法を紹介しました。
今回は INavigationAware インタフェースメンバから残り 2 つの IsNavigationTarget メソッド、OnNavigatedFrom メソッドの紹介と、メソッドの実行に必要な機能を TreeView のコンテキストメニューから呼び出す方法を紹介します。

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

WPF TreeView のコンテキストメニュー

サンプルアプリのデータメンバの内、リスト型の『測定日別身体検査データ』、『試験日別得点データリスト』に新規データを追加できるようにします。
新規データを追加する操作は、以下のように【カテゴリノード】のコンテキストメニューから新規データを追加する UI が直感的だと思うので、TreeView にコンテキストメニューを追加します。

fig.1 コンテキストメニュー

ツリーノードを右クリックして表示するためのコンテキストメニューは、以下のように TreeView.ItemTemplate 内の StackPanel に配置します。

『TreeView wpf コンテキストメニュー』等のキーワードでググると「コンテキストメニューのように別 Window が開く場合は DataContext が継承されないため、TreeView とバインドしている VM とはバインドできない」と言うような内容が見つかりますが、上記 XAML のコンテキストメニューの DataContext には何が設定されているか、念のため XAML をデバッグモードで確認してみました。

fig.2 コンテキストメニューの DataContext

episode: 5 でも書きましたが、この Prism Module に含まれる TreeView は下 fig.3 のような構成で作成しているため、TreeViewItem の DataContext が設定されるようです。
(コンテキストメニューは、TreeView.ItemTemplate 内に追加しているので当然でしょうが…)

fig.3 TreeViewItem の ViewModel

TreeView にコンテキストメニューを表示する方法を扱っている他ブログの記事等では、XAML のバインド定義に『RelativeSource FindAncestor』を記述するように書かれている記事を見かけますが、fig.3 のような構成で作成した場合は、Item 側の VM であれば『RelativeSource FindAncestor』を記述しなくても問題なくバインドできます。
但し、親であるコントロール側(TreeView)の VM とバインドしたい場合は、他ブログに書かれているように『RelativeSource FindAncestor』で親の DataContext を検索する必要があります。

親コントロールの VM と、項目自体の VM のどちらにバインドすべきかは、管理人的にはどちらでも構わないと思いますが、構造的に取り回しがし易い方を選択すれば良いと思います。
ここでは、コンテキストメニューを TreeViewItem の VM とバインドする方で話を進めます。

又、一般的な入門記事とは順序が逆になりましたが、コンテキストメニューの MenuItem は ICommandSource を継承している為、episode: 6 等で紹介した System.Windows.Interactivity を使用しなくても Command プロパティをバインドするだけでメニューの Click イベントを処理できます

又、コンテキストメニューの『Header=”新規データの追加(_A)”』の「A」の前に付いているアンダーバーは【アクセラレータキー】を表す文字です。Windows Form では「&A」としていましたが、WPF でアクセラレータキーを設定する場合はアンダーバーを使用するように変更されています

TreeViewItem の VM も以下のように変更します。

元々、連載開始時から細かい部分はあまり考えていなかったので、今回、TreeView へ新規アイテムを追加するために、メソッドの位置等の変更を少し多めに入れています。
ですが、全てを紹介する必要もないと思うので、興味がある方はリポジトリ を確認してください。

今回最も大きな変更は、TreeViewItemViewModel のコンストラクタに、親 VM のインスタンスを渡すようにして、TreeViewItem の VM から TreeView 本体の VM を参照できるように変更した箇所です。
変更した理由は、TreeView へ新規ノードを追加する際に、TreeViewItem コレクションへの追加だけでなく、データオブジェクトへのメンバ追加も必要になるのが最も大きな理由です。

最初は TreeViewItem へデータオブジェクトを渡そうかとも考えましたが、それぞれのクラスの責務を考えると TreeViewItem がデータオブジェクト全体を保持するのは行き過ぎな気がします。
ですが、自分の親を保持するのは結構見かける構成なので、そちらを採用する事にしました。

VM の 37 ~ 44 行目に初登場の Rx (Reactive Extension)がありますが、これは追加した【新規データの追加コンテキストメニュー】が有効になるのは、カテゴリノードが選択された場合のみにするためで、ReactiveProperty のメンテナーであるかずきさんの【MVVMをリアクティブプログラミングで快適にReactivePropertyオーバービュー】でも同じような記述が紹介されています。

ReactiveCommand は、IObservable<bool> からも生成できるので、メニューの Enabled が true になる条件から ReactiveCommand を生成します。

上記 Rx は Reactive.Bindings.Extensions 名前空間に含まれる CombineLatestValuesAreAllTrue メソッドを使用して IsSelected プロパティと IsCategory プロパティが全て true になった場合のみ AddNewDataCommand を有効にする ReactiveCommand を生成すると言う意味になり、結果としてカテゴリノードが選択されている場合のみ【新規データの追加メニュー】が有効になります。

尚、先程紹介したかずきさんのページでは、以下のように配列で束ねるよう書かれています。

最初、管理人が上記のように書くと、赤波線(コンパイルエラー)が消えなくて困りましたが、最終的に型を指定することで赤波線も消え、正常に実行できるようになりました。
これは、IsCategory:ReactivePropertySlim、IsSelected:ReactivePropertyで宣言していることが原因かもしれませんが、詳しく突っ込んで調べていません。
かずきさんのページが書かれたのは 3 年以上前なので、それ以降何かの変更で配列では通らなくなったのかもしれませんが、型を指定すれば問題無いので良しとします。

又、上記コードのように ReactiveCommand の Enabled 判定に使用するための項目を列挙する場合は、単なる bool を指定すると、型不一致でコンパイルエラーになるため、判定項目は IObservable<bool> を継承した ReactiveProperty 等で宣言した項目が必要になるので注意が必要です。

ここまで書いた状態で実行して、コンテキストメニューを表示してみてください。
カテゴリノードが選択されている場合のみ【新規データの追加メニュー】が有効になっているのが確認できるはずです。

fig.4 コンテキストメニューの有効・無効

TreeViewItem を右クリックで選択

【新規データの追加メニュー】の内部処理を紹介する前に、このアプリを動かしてみて、右クリックで TreeView のノードが選択されないのは非常に使いにくいとは思わなかったでしょうか?
管理人的にはメチャクチャ使いにくいと思います。

管理人は VB4.0 から VisualStudio 系を触っていて、その頃からずっと感じていますが、Microsoft は何故コントロールの標準機能をデフォルトで組み込まないんですかね…?
全ての機能取り込め!等とはサードパーティのコントロールベンダーの存在を考えたら言うつもりはありませんが、ツリーノードを右クリックで選択するとか、フォーカスを受け取った TextBox の全テキストを自動で選択するとか OS 標準動作の中でもポピュラーな動作位は対応してくれても良いんじゃないかと思います…
自分でアプリを作る時も、仕事で新しいプロジェクトに入った時も、毎回似たようなコードを書き過ぎて、さすがにもう飽きました…

と、愚痴はこの位にして、このアプリでも Windows エクスプローラーと同じように右クリックでもツリーノードが選択できるようにします。
まず、XAML です。

そして、NavigationTree のコードビハインドです。

えー… すいません… この処理はコードビハインドに書きます… episode: 1 で、「コードビハインドには何も書かない原理主義を目標に進める」と書きましたが、この部分では断念しました…

少し言い訳を書かせてもらうと…
Blend.Interactivity に含まれる ChangePropertyAction が使えそうな予想をしていて、【[wpf] ContextMenuを表示する前に右クリックしてツリービューノードを選択 – CODE Q&A 問題解決】と言うようなページも見つかったので、XAML に記述するだけでイケると考えていましたが、実際に試してみると、「依存プロパティ以外は対象外です」と冷たい実行時例外エラーが… orz

この程度の処理で、依存プロパティや Behavior を作成するのは面倒すぎるので却下。
コードビハインドにイベントハンドラを書くのは抵抗がありましたが、よく考えると、この NavigationTree は Prism の Module であり、それに含まれる UserControl です

MVVM パターンで作成するとしても、追加した UserControl の内部動作をコードビハインドに書くのは当たり前なので、これはこれでアリじゃないかと思えてきました。
とは言っても、コードビハインドに書くのは、あくまでもユーザのアクションに対する表示のフィードバック等だけで、ビジネスロジック等には全く関わらない箇所に限定すべきだと思います

それに、管理人がこのサンプルアプリを実際に作成する場合であれば、おそらく TreeView を継承したカスタムコントロールを作成して『IsSelectItemByMouseRightButton』のようなプロパティを追加して対応すると思います。
ですが、今回の記事でカスタムコントロールの作成まで盛り込むのは、本筋から外れ過ぎだと思いますし、何よりまだ管理人の知識が追い付いてないので、こう言うのは割り切りも大事!と自分に言い聞かせつつ、ChangePropertyAction が使えなかったのが残念すぎると嘆きながら、コードビハインドへの記述を選択しました。

上記のように記述して実行すると、右クリックで TreeViewItem が選択され、コンテキストメニューも問題なく表示されるはずなので、本題に戻ります。

ReactiveCollection の Add と TreeView の子ノード追加

今回、メソッド位置や内部処理等を大きく変更したので、新規データオブジェクトと新規 TreeViewItem の VM の作成は、親の NavigationTreeViewModel 側で以下のように行っています。

各クラスの責務に合わせてメソッドの場所を整理したので、以前のエントリとは違うメソッドかもしれませんが、特に説明が必要そうな所がある訳でもなく、見ての通り、データオブジェクトを作成して、TreeViewItemViewModel を new しているだけです。

データオブジェクトにも以下のようなメソッドを追加しています。

データオブジェクトをちゃんと設計していれば、もう少しシンプルな構造にできたと思いますが、そこまで変更するまでもないと思ったので、オブジェクトをベタで生成しています。

追加したコンテキストメニューから呼び出される Command は以下のように、親のメソッドを呼び出して返される TreeViewItemViewModel を Children プロパティへ Add() するだけになります。

ReactiveCollection はメンバの追加・削除を View へ通知してくれるので、Add() するだけで、以下のように View の表示も更新されて、TreeView へ新規データが追加されます。

fig.5 新規身体測定データの追加

Prism の IsNavigationTarget で表示する View を判別

個人識別情報データのように 1 つの View で表示するデータが 1 つだけであれば今まで紹介した内容で対応できますが、リスト型のように 1 つの View で複数のデータを扱いたい場合には、View を生成するのか、既存の View を再表示するのかを判別する必要があるので、IsNavigationTarget メソッドへ以下のように記述すると Prism 側で適切な View を返してくれます。

この IsNavigationTarget メソッドは、RequestNavigate() メソッドで生成した View のインスタンスが 1 つ以上存在する場合、OnNavigatedTo メソッドより前に呼び出され、View のインスタンスが 1 つも存在しない場合は呼び出されません。

IsNavigationTarget メソッド内でパラメータで受け取った値を比較して、false を返した場合は新規 View を new して表示され、true を返した View はその View のインスタンスが表示されるため、データを一意に判別する必要があります。

と、Prism のみ使用している場合であれば上記の内容は正しいのですが、ReactiveProperty と併用していて、episode: 7 で紹介した方法でバインドしている場合には若干の違いがあります。
現状は OnNavigatedTo メソッドで、以下のように編集対象インスタンス = null の場合のみ、View とバインドしています。

この null 比較している部分をコメントアウトして、IsNavigationTarget メソッドでは常に true を返すように変更して実行するとどうなるでしょう?

実際に動かしてみると一目瞭然ですが、IsNavigationTarget メソッド内で View を識別する値を返した場合と同じ動作になります。

現状、このサンプルアプリのデータオブジェクトは、ReactiveProperty を介して View – VM – Model が連環で接続されている為、episode: 7 でも書いた通り、View でユーザが編集した内容が即座に Model まで伝播する構造になっています。

fig.6 サンプルアプリのデータ構造

上図のように、NavigationTree が保持するデータオブジェクトと、EditorViews に表示するデータオブジェクトは同一インスタンスで、RequestNavigate メソッドを呼び出すごとにバインドし直しているため、表示データがちゃんと入れ替わって表示されています。
が、管理人的には避けた方が良いと思っています

このサンプルアプリのような単純な View で、動作を理解してあえてそのように書いたのであれば問題ないと思いますが、通常 View では編集するデータ以外のデータを保持することも多々あります。
それら編集対象外のデータを個々の View で保持したい場合は、やはり対象オブジェクトごとに対応した View のインスタンスを持つ方が実装する立場からすれば楽であり、安全だと思います。(メモリの消費量は増えるでしょうが…)

そのため、Prism の Navigation と ReactiveProperty を組み合わせて episode: 7 で紹介した方法で画面遷移時のパラメータに値を渡す場合は、OnNavigatedTo メソッドで、フィールド変数 = null の場合のみバインドする方が安全だと思います。

Prism OnNavigatedFrom は画面遷移前に呼び出される

この OnNavigatedFrom メソッドは、OnNavigatedTo の逆で、現在の View から別の View へ遷移する前(非表示になる前)に呼び出されます。

ただ、このメソッドについてはここでは紹介しません。
このサンプルアプリでは、有用な使用箇所が思い付かないと言うより、使うケースがありません。

Model – VM – View が連環で接続されてないような場合であれば、VM の値を Model へ反映する場合に使用することもあるかもしれませんが、通常その場合であれば不正データが入力されていれば弾く必要がある為、実装場所として適しているとは言いにくいです。

このサンプルアプリのように各 View が独立しているような構造のアプリでは、使用機会が無いでしょうが、ウィザード形式のような画面であれば、次画面に渡すデータをセットする等の用途があると思います
使用方法は OnNavigatedTo メソッドと同じなので、用途がある場合は使ってみてください。

 

 

さて、2019 年初エントリは結構軽めな内容でお送りしました。
今年の年初からサーバ契約を変更して PHP モジュールが実行できる環境に移ったので、去年までと比べると表示が速くなったと思いますが、サーバ移行のごたごたもあって、初エントリの公開が既に 1 月も終わりかけのこんな時期になってしまいました。

画面遷移時のデータ受け渡しについて 2 回に分けて書いてきて、INavigationAware インタフェースの紹介はこれで終了です。
Prism にはもう 1 つよく似た IConfirmNavigationRequest インタフェースがあるので、次回はそのインタフェースとインタフェースの紹介に必要な WPF での Validation を実装する方法を紹介したいと思います。
ただ、少々検証に手間取っているので、次回エントリの公開はひと月程先になるかもしれません。

今回作成したコードもいつも通り GitHub リポジトリへ上げておきます。

 

【WPF Prism episode: 9 ~ ReactiveProperty の Validation は DataAnnotation じゃないと思った?~】次回記事 →

あわせて読みたい

コメントを残す

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