Prismで「新しく画面が開かれたとき」と「次画面から戻ってきたとき」を区別して処理する

# Prismにおける画面遷移のおさらい
画面遷移時の処理として、Prismの`INavigationAware`インターフェイスには以下のメソッドが用意されています。To、Fromが直感と反するかもしれませんが、後ろに来るのが`this`だと思えば間違えないでしょう。

* `OnNavigatedTo(NavigationContext navigationContext)` 
    * 他の画面からこの画面に遷移したときのイベントです。
* `OnNavigatedFrom(NavigationContext navigationContext)` 
    * この画面から他の画面に遷移するときのイベントです。

`navigationContext`は勝手に設定されます。画面を開くときは`navigationContext.NavigationService`が必要です。`NavigationService.RequestNavigate(string target, NavigationParameters navigationParameters)`メソッド(拡張メソッドです)を使用することで別画面を開くことができます。そのため、以下のように`OnNavigatedTo`のときに必ず`navigationContext.NavigationService`を保持することになるでしょう。

なお、処理の順番は以下となります。

1. コンストラクタ
1. `OnNavigatedTo`メソッド
1. 画面の`Loaded`イベント

```c#
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`メソッドの第２引数でパラメータを指定できるので、以下のように毎回フラグを渡してやる方法もあるでしょう。

::: note 
このやり方だと処理の判別を外部から受け取るデータ（画面B）に依存しています。新しく開くかどうかは自分自身（画面A）側で保持すべきでしょう。その実装方法は後述します。
:::

```c#
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`インターフェイスを実装すると、画面がいなくなるたびに破棄するかどうか（＝表示のたびにインスタンスを再生成するか否か）を制御できます。
この２つを実装することで、「戻ってきたときは過去の画面」「新しく開くときは再生成」を実現することが可能です。
２つの違いについては[わかりやすくまとめられた記事がありますのでこちらも参照してください](https://qiita.com/tera1707/items/ba525b43fa2482913c85)。

```c#
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`メソッドは毎回実行されるので、要は最初の１回めだけ処理されるように実装をすればよいでしょう。全体のコードを以下に示します。

```c#
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`参照の未設定警告が出ます。