WPF Prism episode: 7 ~ 画面遷移のパラメータたちが INavigationAware から来るそうですよ? ~
前回は TreeView の SelectedItemChanged イベントで MainWindow 左側の View を動的に切り替える方法と、View の切り替え時にパラメータを渡す方法を紹介しました。
今回は Prism の RequestNavigate メソッドに設定したパラメータを View 側(VM)で受け取る方法を紹介します。
※ Google 等の検索エンジンから来た方へ
このエントリは 2019/6/7 をもってリニューアルしたため、元々 episode: 7 のタイトルで公開していた内容は extra: 2、episode: 8 に分割しました。お手数ですがそれぞれの記事へ移動してください。
尚、この記事は Visual Studio 2017 Community Edition で .NET Framework 4.7.2 以上 と C# + Prism 7.1 + ReactiveProperty を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。
目次
Prism から動的に表示する View へパラメータを受け渡す
Prism の IRegionManager.RequestNavigate メソッドは fig. 1 の矢印で示した方向へパラメータを渡すことができます。
(View1 → View2 → View3 の順に切り替わる場合の例)
IRegionManager.RequestNavigate 呼び出し時にパラメータを設定する方法は episode: 6.5 で紹介しましたが、設定したパラメータを View 側(VM)で受け取るには INavigationAware を継承する必要があります。
Prism の INavigationAware を使用したパラメータの受け取り
episode: 6.5 でも紹介しましたが、RequestNavigate にパラメータをセットするには src. 1 のように記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System.Windows; namespace WpfTestApp.ViewModels { public class NavigationTreeViewModel : BindableBase, IDisposable { ~ 略 ~ /// <summary>SelectedItemChangedイベントハンドラ。</summary> /// <param name="e">イベントデータを格納しているRoutedPropertyChangedEventArgs<object>。</param> private void nodeChanged(RoutedPropertyChangedEventArgs<object> e) { ~ 略 ~ var param = new Prism.Regions.NavigationParameters(); param.Add("TargetData", current.SourceData); this.regionManager.RequestNavigate("EditorArea", viewName, param); } ~ 略 ~ } } |
src. 1 のように RequestNavigate にセットされたパラメータを受け取るために必要な INavigationAware には以下の 3 つのメンバが含まれています。
- void OnNavigatedTo(NavigationContext)
- void OnNavigatedFrom(NavigationContext)
- bool IsNavigationTarget(NavigationContext)
RequestNavigate にセットしたパラメータは上記 3 つ全てで受け取る事が出来ますが、View が表示されたタイミングでパラメータを受け取りたい場合は OnNavigatedTo イベント(メソッド)を使用します。
OnNavigatedTo はメソッドとして定義されていますが、イベントと同じようなものなのでこの連載では OnNavigatedTo イベントと表記します。
OnNavigatedTo イベントは INavigationAware を継承した View(VM)へ遷移したタイミングで呼出され、RequestNavigate でセットされたパラメータを受け取るには src. 2 のように実装します。
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 46 47 48 49 50 51 52 53 54 | using Prism.Mvvm; using Prism.Regions; namespace WpfTestApp.ViewModels { public class PersonalEditorViewModel : BindableBase, INavigationAware { /// <summary>生徒氏名を取得・設定します。</summary> private string _name; public string Name { get { return _name; } set { SetProperty(ref _name, value); } } /// <summary>所属クラスを取得・設定します。</summary> private string classNum; public string ClassNumber { get { return classNum; } set { SetProperty(ref classNum, value); } } /// <summary>性別を取得・設定します。</summary> private string _sex; public string Sex { get { return _sex; } set { SetProperty(ref _sex, value); } } private PersonalInformation personInfo = null; /// <summary>Viewを表示した後呼び出されます。</summary> /// <param name="navigationContext">Navigation Requestの情報を表すNavigationContext。</param> void INavigationAware.OnNavigatedTo(NavigationContext navigationContext) { this.personInfo = navigationContext.Parameters["TargetData"] as PersonalInformation; this.Name = this.personInfo.Name; this.ClassNumber = this.personInfo.ClassNumber; this.Sex = this.personInfo.Sex; } /// <summary>表示するViewを判別します。</summary> /// <param name="navigationContext">Navigation Requestの情報を表すNavigationContext。</param> /// <returns>表示するViewかどうかを表すbool。</returns> bool INavigationAware.IsNavigationTarget(NavigationContext navigationContext) { return true; } /// <summary>別のViewに切り替わる前に呼び出されます。</summary> /// <param name="navigationContext">Navigation Requestの情報を表すNavigationContext。</param> void INavigationAware.OnNavigatedFrom(NavigationContext navigationContext) { return; } } } |
38 行目のように RequestNavigate にパラメータをセットした時と同じキーワードを指定してパラメータを取得します。
OnNavigatedTo 以外のメンバは後で紹介するので、とりあえず例外が飛ばないようにしておきます。
取得したパラメータの値をバインドプロパティへセット(40 ~ 42 行目)すると fig. 2 のように編集 View が表示されたタイミングでデータが表示されるようになります。
View の PersonalEditor.xaml は特に説明が必要なところも無さそうなので GitHub リポジトリ で見てください。
尚、今回紹介するサンプルコードは ReactiveProperty ではなく全て Prism の BindableBaseを使用して View とバインドします。
ReactiveProperty を編集データにバインドする例は次回 episode: 8 で紹介する予定です。
前回の extra: 3 で TreeViewItem へコンテキストメニューを追加して『新しい測定データ』や『新しい試験日』を追加できるようにしました。
先に extra でコンテキストメニューの追加を紹介したのはこのメソッドを紹介するためです。
又、episode: 6.5 で紹介した通り RequestNavigate で表示する View は src. 3 のように予め DI コンテナ(ここでは Unity)へ登録する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using WpfTestApp.Views; using Prism.Ioc; using Prism.Modularity; using Prism.Regions; namespace WpfTestApp { public class EditorViewsModule : IModule { public void OnInitialized(IContainerProvider containerProvider) { } public void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigation<PersonalEditor>(); containerRegistry.RegisterForNavigation<PhysicalEditor>(); containerRegistry.RegisterForNavigation<TestPointEditor>(); containerRegistry.RegisterForNavigation<CategoryPanel>(); } } } |
前章で紹介した個人識別情報データのように必要な View のインスタンスが 1 つだけであれば良いのですが、『新しい測定データ』や『新しい試験日』のように View のインスタンスが複数必要な場合は View を新たに生成する必要があるか、既存の View を再表示するかを判別する必要があります。
そのような場合に呼び出されるのが IsNavigationTarget で、false を返した場合は View の新規インスタンスが生成され、true を返した View が表示されます。
IsNavigationTarget は RequestNavigate で指定した View のインスタンスが DI コンテナ内に 1 つ以上存在する場合は OnNavigatedTo より前に呼び出され、View のインスタンスが存在しない場合は呼び出されません。
src. 2 のように IsNavigationTarget で常に true を返すと fig. 3 のような動作になります。
fig. 3 のように新しい項目を追加して画面を切り替えても常に同じ画面が表示されます。
IsNavigationTarget も OnNavigatedTo と同じく RequestNavigate にセットしたパラメータを受け取れるので、src. 4 のように受け取ったパラメータとフィールドに保持したインスタンスを比較した結果を返すよう変更します。
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 | /// <summary> 身体測定データの編集画面を表します。 </summary> public class PhysicalEditorViewModel : BindableBase, IDisposable, INavigationAware { ~ 略 ~ /// <summary>身体測定データを取得します。</summary> /// <param name="navigationContext">Navigation Requestの情報を表すNavigationContext。</param> /// <returns>NavigationContextから取得したPhysicalInformation。</returns> private PhysicalInformation getPhysicalData(NavigationContext navigationContext) { return navigationContext.Parameters["TargetData"] as PhysicalInformation; } /// <summary>表示するViewを判別します。</summary> /// <param name="navigationContext">Navigation Requestの情報を表すNavigationContext。</param> /// <returns>表示するViewかどうかを表すbool。</returns> bool INavigationAware.IsNavigationTarget(NavigationContext navigationContext) { if (this.physical == null) return false; var physicalInfo = navigationContext.Parameters["TargetData"] as PhysicalInformation; return this.physical.Id == physicalInfo.Id; } ~ 略 ~ } |
src. 4 のように IsNavigationTarget をデータを判定する処理に変更すると fig. 4 のようにデータに一致した View が表示されるようになります。
INavigationAware.OnNavigatedFrom イベントは OnNavigatedTo の逆で、現在の View から別の View へ遷移する前(非表示になる前)に呼び出される為、src. 5 のように View に入力された値を Model へ反映することもできます。
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 | using Prism.Mvvm; using Prism.Regions; namespace WpfTestApp.ViewModels { public class PersonalEditorViewModel : BindableBase, INavigationAware { ~ 略 ~ private PersonalInformation personInfo = null; /// <summary>Viewを表示した後呼び出されます。</summary> /// <param name="navigationContext">Navigation Requestの情報を表すNavigationContext。</param> void INavigationAware.OnNavigatedTo(NavigationContext navigationContext) { this.personInfo = navigationContext.Parameters["TargetData"] as PersonalInformation; this.Name = this.personInfo.Name; this.ClassNumber = this.personInfo.ClassNumber; this.Sex = this.personInfo.Sex; } /// <summary>表示するViewを判別します。</summary> /// <param name="navigationContext">Navigation Requestの情報を表すNavigationContext。</param> /// <returns>表示するViewかどうかを表すbool。</returns> bool INavigationAware.IsNavigationTarget(NavigationContext navigationContext) { return true; } /// <summary>別のViewに切り替わる前に呼び出されます。</summary> /// <param name="navigationContext">Navigation Requestの情報を表すNavigationContext。</param> void INavigationAware.OnNavigatedFrom(NavigationContext navigationContext) { this.personInfo.Name = this.Name; this.personInfo.ClassNumber = this.ClassNumber; this.personInfo.Sex = this.Sex; } } } |
OnNavigatedFrom で VM の値を Model へ戻して fig. 5 のような動作にすることもできます。
このサンプルアプリ内のデータ構造は fig. 6 のようになっていて、TreeViewItem が保持しているデータと編集 View に渡したデータは同一のインスタンスなので、Model へ値を反映すると TreeViewItem の ItemText も fig. 5 のように同時に更新されます。
但し、現状ではバリデーション等のチェックを何も行っていないため、不正なデータであっても無条件で Model が更新されてしまいます。
バリデーションについては又次回以降で紹介する予定です。
OnNavigatedFrom イベントでは src. 5 のように Model へ値を戻す用途だけでなく、次に表示する View へパラメータを渡すこともできますが、このサンプルアプリのように各 View が独立しているような構造のアプリでは有用な例が見つかりませんでした。
ウィザード形式のような画面であれば次に表示する View に渡すデータをセットする等の用途はあると思いますが、パラメータの設定等は episode: 6.5 で紹介している方法をそのまま使用すれば良いはずなので具体例の紹介はしません。
INavigationAware の 3 つのメンバ全てに渡される NavigationContext パラメータは値を受け渡しするための Parameters プロパティ以外に NavigationService プロパティも備えています。
NavigationService は IRegionNavigationService 型のプロパティで Journal や Region 等のプロパティを備えていてウィザード形式の画面で使えそうです。
この連載で作成しているサンプルアプリには相性が良くなさそうなので単なる紹介だけにしますが、興味があるなら『Prism.WpfでRequestNavigateと戻る、進む(IRegionNavigationService, IRegionNavigationJournal) – Qiita』で詳しく説明されています。
IRegionMemberLifetime で非 Active になった View を破棄する
INavigationAware.IsNavigationTarget では View の生成 or 再利用 を Prism に指示することができましたが、src. 6 のように IRegionMemberLifetime インタフェースを継承すると View が 非 Active になった際に View を破棄することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | using Prism.Mvvm; using Prism.Regions; namespace WpfTestApp.ViewModels { /// <summary>カテゴリパネルのViewModel</summary> public class CategoryPanelViewModel : BindableBase, IRegionMemberLifetime { /// <summary>非Active時にインスタンスを保持するかを取得します。</summary> public bool KeepAlive => false; /// <summary>コンストラクタ。</summary> public CategoryPanelViewModel() { } } } |
IRegionMemberLifetime インタフェースの唯一のメンバである KeepAlive 読み取り専用プロパティで false を返すと View が 非 Active になった時にインスタンスが破棄されるようになります。
逆に true を返すと今まで紹介してきた View と同じように非 Active になってもインスタンスは破棄されません。
src. 6 は CategoryPanelViewModel が IRegionMemberLifetime を継承しているので紹介以上の意味はありませんが、毎回表示される度にインスタンスを再生成したい View(VM)で継承します。
今回は INavigationAware インタフェースと IRegionMemberLifetime を紹介して終了とします。
次回は今回から引き続き RequestNavigate にセットされたパラメータを INavigationAware.OnNavigatedTo で受け取って ReactiveProperty にバインドする方法を紹介します。
今回作成したコードもいつも通り GitHub リポジトリへ上げておきます。
この記事が、2018 年最後の episode になります。
次回は 2019 年 1 月中に公開できれば良いな… と考えています。
次回記事「ReactiveProperty がバインドできないのはどう考えても Navigation が悪い!【episode: 8 WPF Prism】」