WPF Prism episode: 11 ~ Prism が画面遷移キャンセルするのは IConfirmNavigationRequest だけど INavigationAware じゃない ~
前回は ReactiveProperty で定義したプロパティへ Validation を設定するシリーズの最終章として、Validation の ErrorTemplate を設定する方法を紹介しました。
今回は久々 Prism に戻り、VM の継承元を INavigationAware インタフェースから IConfirmNavigationRequest インタフェースに変更することで Prism の Navigation(画面遷移)をキャンセルする方法を紹介します。
尚、この記事は Visual Studio 2017 Community Edition で .NET Framework 4.7.2 以上 と C# + Prism 7.1 + ReactiveProperty + Extended WPF Toolkit™ を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。
Prism で画面遷移をキャンセル
このサンプルアプリのように、ツリーノード等をクリックしたタイミングで View が切り替わるような UI を作成する場合、他 View への切り替えをキャンセルしたい場合もあると思います。
そんな場合は、以前から何度も紹介すると予告していた Prism の IConfirmNavigationRequest インタフェースを使用すると、画面遷移をキャンセルできるようになります。
このインタフェースは Prism の実装 を見て分かる通り、INavigationAware インタフェースを継承しているため、epsode: 7、episode: 8 で紹介したメソッドに加えて、ConfirmNavigationRequest メソッドが定義されています。
episode: 9 でも書きましたが、このサンプルアプリでは身体測定データの測定日は表示上のキーとして扱いたいので、「未入力」や「既にリスト内に同一日付のデータが存在する」等の場合(要するに Validation の結果がエラーになっている場合)は他の View へ遷移させたくありません。
そのような場面で使用するのが ConfirmNavigationRequest メソッドです。
画面遷移をキャンセルしたい View の VM を以下のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | using System.ComponentModel.DataAnnotations; using Prism.Mvvm; using Prism.Regions; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace WpfTestApp.ViewModels { /// <summary> 身体測定データの編集画面を表します。 </summary> public class PhysicalEditorViewModel : BindableBase, IDisposable, IConfirmNavigationRequest { ~ 略 ~ /// <summary>他 View への遷移を確認します。</summary> /// <param name="navigationContext">遷移先の情報を表すNavigationContext。</param> /// <param name="continuationCallback">遷移を続行するかを判定するコールバック。</param> void IConfirmNavigationRequest.ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback) { this.MeasurementDate.ForceValidate(); this.Height.ForceValidate(); this.Weight.ForceValidate(); var isMove = !(this.MeasurementDate.HasErrors | this.Height.HasErrors | this.Weight.HasErrors); continuationCallback(isMove); } /// <summary>測定日のエラー文字列を取得します。</summary> /// <param name="value">View で入力されたDateTime?。</param> /// <returns>測定日のエラー文字列</returns> private string getMeasurementDateError(DateTime? value) { if (!value.HasValue) return "必須入力です。"; if (this.appData.HasPhysicalKey(value, this.physical)) { this.MeasurementDate.Value = this.physical.MeasurementDate; return "既に同一の測定日が存在するため、別の日付を設定してください。"; } else return null; } ~ 略 ~ } } |
まず、VM の継承元を INavigationAware インタフェースから IConfirmNavigationRequest インタフェースに変更しています。(10 行目)
そして View の遷移判定に使用する測定日プロパティは、前回紹介した通り初回表示時の Validation を無視する設定にしているので、測定日からフォーカスが移動していない状態でエラーの有無を取得すると当然『エラー無し』が返されます。
View が初回表示直後であってもエラーの有無が取得出来るように、ConfirmNavigationRequest メソッドの先頭で ReactiveProperty.ForceValidate() メソッドを呼び出して Validation を強制的に実行しています。
(全プロパティの Validation を強制実行しています)
エラーが 1 つでも存在する場合は他 View への遷移を中止したいので、Validation の強制実行後に各プロパティの HasErrors プロパティ からエラーの有無を取得して遷移判定に利用しています。
実際に他 View への遷移をコントロールするには、ConfirmNavigationRequest メソッドの 第 2 パラメータ:continuationCallback へ false をセットすると中止され、true をセットすると正常に遷移します。
サンプルアプリを実行すると、下 fig.2 のように『エラー無し』の場合は他の View へ遷移できますが、エラーが存在する場合は他 View への Navigation(遷移)が中止されるようになります。
このように VM の継承元を INavigationAware インタフェースから IConfirmNavigationRequest インタフェースに変更すると、ConfirmNavigationRequest メソッド内で遷移する・しないをコントロールできるようになります。
但し、上の fig.2 を見れば分かると思いますが、View の切り替え自体はキャンセルされていますが、左側の TreeView は選択ノードがクリックしたノードに変更されてしまっています。
これでは見た目的にイマイチなので、TreeView の SelectedItemChanged イベント内でノードの選択がキャンセル出来るか試してみます。
Prism で画面遷移の結果を受け取って処理を実行する
前章で紹介した ConfirmNavigationRequest メソッドで画面遷移をキャンセルすると Prism 自体には通知されますが、TreeView には何も通知されないため選択ノードが変更されてしまいます。
では、TreeView を配置した NavigationTree Module へ画面遷移のキャンセルが通知できると TreeView の ノード選択もキャンセルできそうですが、NavigationTree Module と EditorViews Module は独立した別々のプロジェクトなので、互いに参照関係もありません。
このような場合でも、episode: 6 で紹介した IRegionManager.RequestNavigate メソッドは Prism Navigation(画面遷移)の結果通知を受け取る事が出来ます。
RequestNavigate メソッドには Navigation の結果をコールバックで受け取る overload が定義されているので、そのコールバックで TreeView のノード選択がキャンセルできるか試してみます。
IRegionManager.RequestNavigate メソッドの呼び出し部を以下のように書き換えると、Navigation の結果が受け取れるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | using System.Windows; using Prism.Mvvm; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace WpfTestApp.ViewModels { ~ 略 ~ /// <summary>NavigationTreeのViewModelを表します。</summary> public class NavigationTreeViewModel : BindableBase, IDisposable { ~ 略 ~ private bool skipNodeChange = false; /// <summary>SelectedItemChangedイベントハンドラ。</summary> /// <param name="e">イベントデータを格納しているRoutedPropertyChangedEventArgs<object>。</param> private void nodeChanged(RoutedPropertyChangedEventArgs<object> e) { if (this.skipNodeChange) { this.skipNodeChange = false; return; } ~ 略 ~ var param = new Prism.Regions.NavigationParameters(); param.Add("TargetData", current.SourceData); this.regionManager.RequestNavigate("EditorArea", viewName, r => { if ((r.Result.HasValue) && (!r.Result.Value)) { var oldNode = e.OldValue as TreeViewItemViewModel; this.skipNodeChange = true; oldNode.IsSelected.Value = true; } }, param); } ~ 略 ~ } } |
IRegionManager.RequestNavigate メソッドの第 3 パラメータに指定するコールバックの定義は以下のようになっています。
前章で Navigation(画面の遷移)をキャンセルする際のコールバックにセットした bool 値は上記コールバックのパラメータ:NavigationResult.Result プロパティに返されます。
そして、TreeView.SelectedItemChanged イベントのパラメータ:RoutedPropertyChangedEventArgs.OldValue には直前に選択されていたツリーノード(ここでは TreeViewItemViewModel)が格納されている為、その IsSelected = true にセットして再選択することでノード選択のキャンセルをエミュレートしてみます。
TreeView ノードの IsSelected プロパティを変更すると TreeView.SelectedItemChanged イベントも当然再実行されるため、不恰好ですがフラグを用意してイベントの再実行を抜けています。
イマイチイケてないのでググってみると、以下の 2 つの方法でイベントの再実行が防げそうなので試してみましたが結局ダメでした…。結果はダメでしたが一応紹介します。
まず 1つ目は ReactiveProperty Ver.2.7.0 から追加された BusyNotifier。
管理人の書き方が悪いのかもしれませんが、上記リンク先の通りに書いて実行しても無限ループに突入してしまいました。
そして 2 つ目は ReactiveProperty Ver.2.9.0 から追加された AsyncReactiveCommand。
このクラスはボタンのダブルクリック防止には便利そうです。
この【AsyncReactiveCommand】ほとんど情報がヒットしませんが、Qiita の『AsyncReactiveCommandでWPFのお手軽ダブルクリック抑制』で紹介されていたので試してみましたが、今回のようにイベントハンドラ内部からイベントが発火するトリガーを変更するようなパターンでは無理のようで、BusyNotifier のときと同じく無限ループに突入してしまいました。
今回はダメでしたが、ボタンのダブルクリックを防止したいような場合に又、試してみます。
イベント再発火の防止は上記コード通りフラグで制御するとして、実際に実行してみると、以下のように選択ノードも変更されなくなります。
と、イケるやん!と喜んだのも束の間… マウスの右ボタンでノードを選択すると、コンテキストメニューが表示されて、クリックしたノードへ選択が変更されてしまいます… orz
episode:8 で右クリックでもノードを選択できるようにした影響かもしれません…
これ以上、アプリ側で対応するのは面倒 難しそうなので、SelectedItemChanged イベントをキャンセルするのはここまでにします。
実際に対応する場合はカスタムコントロールとして TreeView を継承して作成する方が逆に楽そうなので、対応方法が分かればエントリとして公開するかもしれません。
上の理由から今回 GitHub リポジトリ へ公開している実装は、Validation のエラーが存在する場合、マウスの左クリックではノードの選択がキャンセルされるが、マウスの右クリックでは選択ノードが変わってしまう状態のままでコミットしています。
TreeView ノードの選択キャンセルは完全動作とは言えない状態ですが、Prism の IConfirmNavigationRequest インタフェースと IRegionManager.RequestNavigate メソッドが連動していて、Navigation(画面遷移)をキャンセルした結果は Navigation を要求した側に通知されることは分かってもらえたと思います。
今回はここまでで、次回は Prism の Navigation(画面遷移)をキャンセルしたタイミングで Prism 組み込みのメッセージボックスを表する方法を紹介する予定です… が、今回までは週一で更新していましたが episode:12 を来週(2019/3/17辺り)公開するのは難しいかもしれません。
いつもの通り、ここで紹介したソースコードは GitHub リポジトリ へアップしています。
次回記事「Prism メッセージボックスの Service な日常【episode: 12 WPF Prism】」