Prismにおける画面遷移のおさらい
画面遷移時の処理として、PrismのINavigationAware
インターフェイスには以下のメソッドが用意されています。To、Fromが直感と反するかもしれませんが、後ろに来るのがthis
だと思えば間違えないでしょう。
-
OnNavigatedTo(NavigationContext navigationContext)
- 他の画面からこの画面に遷移したときのイベントです。
-
OnNavigatedFrom(NavigationContext navigationContext)
- この画面から他の画面に遷移するときのイベントです。
navigationContext
は勝手に設定されます。画面を開くときはnavigationContext.NavigationService
が必要です。NavigationService.RequestNavigate(string target, NavigationParameters navigationParameters)
メソッド(拡張メソッドです)を使用することで別画面を開くことができます。そのため、以下のようにOnNavigatedTo
のときに必ずnavigationContext.NavigationService
を保持することになるでしょう。
なお、処理の順番は以下となります。
- コンストラクタ
-
OnNavigatedTo
メソッド - 画面の
Loaded
イベント
public class ViewModel : BindableBase, INavigationAware
{
protected IRegionNavigationService RegionNavigationService { get; private set; } = null!;
public void OnNavigatedTo(NavigationContext navigationContext)
{
RegionNavigationService = navigationContext.NavigationService;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{ }
}
画面遷移の区別
さて、このOnNavigatedTo
ですが、ある画面から新しく開かれたときとある画面を開いたあとに戻ってきたときの両方で発火します。そのため、OnNavigatedTo
に初期化処理を書いてしまうと、「次画面を開いている間はVMを残しておきたい」というようなケースで困ったことになります。
具体的には以下の要件をすべて満たす場合です。
- 画面Aから画面Bを開くとき、画面Bはコンストラクタから毎回新しく作りたい。そのときにデータを画面Aから画面Bに渡したい
- 画面Bを閉じて画面Aに戻るとき、画面Bは完全に破棄したいし、画面Aは画面Bに移動する直前の状態をそのまま表示したい
- 戻ってくるときに画面Aで何らかの処理(データの更新等)はしたい
- 任意の画面Tから同じ画面Tは開けない(ループ不可)
- 画面は常に1枚しか開けない(同じ画面を同時に複数枚開けない)
要は「新しく画面が開かれたとき」と「次画面から戻ってきたとき」を区別して処理できればいいのです。そんな実装方法について紹介します。
判定するだけなら簡単
NavigationService.RequestNavigate
メソッドの第2引数でパラメータを指定できるので、以下のように毎回フラグを渡してやる方法もあるでしょう。
このやり方だと処理の判別を外部から受け取るデータ(画面B)に依存しています。新しく開くかどうかは自分自身(画面A)側で保持すべきでしょう。その実装方法は後述します。
public class ViewModel : BindableBase, INavigationAware
{
protected IRegionNavigationService RegionNavigationService { get; private set; } = null!;
public void OnNavigatedTo(NavigationContext navigationContext)
{
RegionNavigationService = navigationContext.NavigationService;
var flag = (bool?)navigationContext.Parameters["IsNewOpen"];
if (flag?.Value ?? true)
{
/* 新しく開いたとき */
}
else
{
/* 戻ってきたとき */
}
}
/// <summary>この画面を閉じる処理でtrueにします</summary>
private bool _isExiting;
public void OnNavigatedFrom(NavigationContext navigationContext)
{
var param = new NavigationParameters();
param.Add("IsNewOpen", !_isExiting);
}
}
ただしこれだけでは**「画面Aから画面Bを開くとき、画面Bはコンストラクタから毎回新しく作りたい」「画面Aに戻ってきたときはそのまま表示したい」**を満たせません。
破棄するのか保持するのかは別の方法で管理する必要があります。
破棄の判定
画面を保持するというのは、つまり画面のインスタンスを破棄せず使いまわしておけばよいということです。それを判定するメソッドとして、INavigationAware
にはIsNavigationTarget(NavigationContext navigationContext)
が用意されています。
また、IRegionMemberLifetime
インターフェイスを実装すると、画面がいなくなるたびに破棄するかどうか(=表示のたびにインスタンスを再生成するか否か)を制御できます。
この2つを実装することで、「戻ってきたときは過去の画面」「新しく開くときは再生成」を実現することが可能です。
2つの違いについてはわかりやすくまとめられた記事がありますのでこちらも参照してください。
public class ViewModel : BindableBase, INavigationAware, IRegionMemberLifetime
{
/// <summary>この画面を閉じる処理でtrueにします</summary>
private bool _isExiting;
public bool IsNavigationTarget(NavigationContext navigationContext) => !_isExiting;
public bool KeepAlive => !_isExiting;
}
この実装により、画面を閉じる際に_isExiting = true;
とすることで画面が破棄されるようになります。破棄されているので次に開くときはまたコンストラクタから再生成されます。
自分自身で新しく開かれたか戻ってきたかを判定する版
OnNavigatedTo
メソッドは毎回実行されるので、要は最初の1回めだけ処理されるように実装をすればよいでしょう。全体のコードを以下に示します。
public class ViewModel : BindableBase, INavigationAware, IRegionMemberLifetime
{
#region INavigationAwareの実装
protected IRegionNavigationService RegionNavigationService { get; private set; } = null!;
/// <summary>
/// 新しく開かれたかどうかを取得します。
/// 初回の<see cref="OnNewOpened(NavigationContext)"/>の完了後以降はfalseになります。
/// </summary>
protected bool IsNewOpend => _isNewOpend;
private bool _isNewOpend = true;
/// <summary>
/// 他の画面からこの画面に遷移したときのイベントです。
/// </summary>
/// <param name="navigationContext">現在のコンテキスト</param>
/// <remarks>処理の順番はコンストラクタ→<see cref="OnNavigatedTo(NavigationContext)"/>→<see cref="Loaded"/>です。</remarks>
public void OnNavigatedTo(NavigationContext navigationContext)
{
RegionNavigationService = navigationContext.NavigationService;
if (IsNewOpend) OnNewOpened(navigationContext);
else OnReOpened(navigationContext);
_isNewOpend = false;
}
/// <summary>
/// 他の画面から新しくこの画面を開いたときの処理を行います。
/// </summary>
/// <param name="navigationContext">現在のコンテキスト</param>
/// <remarks>処理の順番はコンストラクタ→<see cref="OnNewOpened(NavigationContext)"/>→<see cref="Loaded"/>です。</remarks>
protected virtual void OnNewOpened(NavigationContext navigationContext) { }
/// <summary>
/// 他の画面からこの画面に戻ってきたときの処理を行います。
/// </summary>
/// <param name="navigationContext">現在のコンテキスト</param>
/// <remarks>処理の順番は<see cref="OnReOpened(NavigationContext)"/>→<see cref="Loaded"/>です。
/// コンストラクタは実行されません。</remarks>
protected virtual void OnReOpened(NavigationContext navigationContext) { }
/// <summary>
/// 画面のインスタンスを使いまわすかどうかを取得します。既定値はfalseです。
/// </summary>
/// <param name="navigationContext">現在のコンテキスト</param>
/// <returns></returns>
/// <remarks>
/// <para>true:インスタンスを使いまわす(画面遷移してもコンストラクタが呼ばれない)</para>
/// <para>false:インスタンスを使いまわさない(画面遷移するとコンストラクタが呼ばれる)</para>
/// ※コンストラクタが呼ばれない場合でも、Loadedイベントは起きる
/// </remarks>
public virtual bool IsNavigationTarget(NavigationContext navigationContext) => !_isExiting;
/// <summary>
/// この画面から他の画面に遷移するときのイベントです。
/// </summary>
/// <param name="navigationContext">現在のコンテキスト</param>
public void OnNavigatedFrom(NavigationContext navigationContext)
{
OnMovingContent(navigationContext);
}
/// <summary>
/// この画面から他の画面に遷移するときの処理を行います。
/// この処理は前画面に戻るときと次画面に進むときの両方で実行されます。
/// </summary>
/// <param name="navigationContext">現在のコンテキスト</param>
protected virtual void OnMovingContent(NavigationContext navigationContext) { }
#endregion
#region IRegionMemberLifetimeの実装
/// <inheritdoc/>
/// <remarks>
/// falseの場合、非アクティブになったこの画面を破棄します。
/// </remarks>
public bool KeepAlive => !_isExiting;
/// <summary>
/// この画面を閉じているときtrue
/// </summary>
private bool _isExiting;
#endregion
}
「新しく画面が開かれたとき」はOnNewOpened
メソッドが、「戻ってきたとき」はReOpended
メソッドが実行されます。データを前画面から受け取ったり次画面から引き継いだりするときはこれらメソッド内で処理を行います1。
このような処理はほぼ全画面で共通の処理となりますので、ViewModelの共通の親クラスとして実装し、具体的な画面のコンテンツ側で継承してOnNewOpened
メソッドやOnReOpened
メソッドを必要に応じて実装することをおすすめします。
ただし、最初に条件として上げましたが画面Aから画面Aを開くのようなことをしたり、別ウィンドウで複数開くというケースでは適用できません。あくまでも紙芝居のように前画面を隠しながら1枚ずつ遷移するケースでしか今回のコードは動作しませんので注意してください。
-
OnNavigatedTo
はコンストラクタの後に実行されるため、受領データをOnNewOpened
で初めてプロパティやメンバ変数にセットする場合、その変数はコンストラクタ終了時点ではnull
であることに注意してください。これを忘れるとNullReferenceException
の原因となったり、null
参照の未設定警告が出ます。 ↩