[WPF/C#] Prism(6.3.0)のKeepAliveをfalseにすることとIsNavigationTargetでfalseを返すことの違いの実験

**もくじ**
→https://qiita.com/tera1707/items/4fda73d86eded283ec4f

**Prism関連**
https://qiita.com/tera1707/items/4fda73d86eded283ec4f#prism%E9%96%A2%E9%80%A3wpfxaml

**コード**
https://github.com/tera1707/WPF-/tree/master/024_PrismSample

# やりたいこと

Prismの画面遷移をさせるうえで、
`RegionManager.RequestNavigate("リージョン名",・・・);`としているが、
その時に、画面のViewModelのクラスのインスタンスを一旦破棄して、再度遷移した際にもう一度作り直したい、というケースがあった。

その際、「インスタンスを破棄」とかでやり方を調べていると、

- `INavigationAware`インターフェースの`IsNavigationTarget`メソッドを実装して、そいつにfalseを返させる
- `IRegionMemberLifetime`インターフェースの`KeepAlive`プロパティを実装して、そいつをfalseにする

という二つのやり方が見つかった。

簡単に試したところ、どっちも
画面Aから画面Bに`RequestNavigate`で遷移した後、再度画面Aに戻った際、**画面Aのコンストラクタが呼ばれている。**

つまり、どちらも画面Aは画面Bに遷移後、一旦破棄されてる？？まったく同じことに対してやり方が2通りあるのか？
でもそんなわけがない、何か違いがあるはず、と思ったので、何が違うのか調べてみる。

# 今回試した結論

結果、下記である、自分の中ではなった。

|やること|起きていること|
|:--|:--|
|①KeepAliveをfalseにする|ViewModelのインスタンスが破棄される。<br>再度同じ画面にRequestNavigateした際に、ViewModelのインスタンスが再作成されている。|
|②IsNavigationTargetにfalseを返させる|ViewModelのインスタンスは**破棄されない**。<br>再度同じ画面にRequestNavigateした際は、前の画面のインスタンスは**破棄せず保持**で、**新しいVMのインスタンスを作成**し、そっちに遷移する。|

つまり、両方ともコンストラクタが呼ばれる、イコール、新しいVMのインスタンスが作成されてるが、KeepAliveをfalseにする方は古い(別画面に遷移後の、元の画面の)インスタンスは破棄されてるが、IsNavigationTargetの戻り値をfalseにする方は、古いインスタンスが実は**破棄されてない。**

そのため、②のほうは、画面遷移回数が増えると作成される画面１のインスタンスも増えることになるので、おそらくメモリリークにつながる。
**⇒画面のインスタンスを毎回破棄して、次回遷移時に再作成したい、という目的のために、IsNavigationTarget  をfalseにする、ということを行ってはいけない！**

今回やりたい「画面遷移するたびに毎回画面のVMのインスタンスを破棄したい」ということをする場合は、**KeepAliveをOFF**してからRequestNavigateで画面遷移させるのが良い。

# 結論がでるまでに試したこと

## 実験コード

- ViewModelの`IsNavigationTarget`メソッドに、通ったことがわかるようにDebug.WriteLineでログ出力処理を入れた
- ViewModelのデストラクタに、通ったことがわかるようにDebug.WriteLineでログ出力処理を入れた
- ViewModelに、自分自身の
    - IsNavigationTargetメソッドの戻り値の値をfalseにできるようにした
    - KeepAliveをfalseにできるようにした



```実験コード1.cs
using Microsoft.Practices.Unity;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Regions;
using PrismSample.Views;
using System.Diagnostics;

namespace PrismSample.ViewModels
{
    class UserControl1ViewModel : BindableBase, INavigationAware, IRegionMemberLifetime
    {
        [Dependency]
        public IRegionManager RegionManager { get; set; }
        public DelegateCommand ButtonCommand { get; }
        public DelegateCommand ButtonKeepAliveONCommand { get; }
        public DelegateCommand ButtonKeepAliveOFFCommand { get; }
        public DelegateCommand ButtonIsNavigationTargetONCommand { get; }
        public DelegateCommand ButtonIsNavigationTargetOFFCommand { get; }
        public DelegateCommand LoadedCommand { get; }

        private static int constructorCounter = 0;
        private static int destructorCounter = 0;

        public bool KeepAlive
        {
            get
            {
                Debug.WriteLine("画面１ KeepAlive is " + keepalive);
                return keepalive;
            }
            set
            {
                keepalive = value;
            }
        }
        private bool keepalive = true;
        public bool IsNavigationTargetFlag = true;

        public UserControl1ViewModel()
        {
            constructorCounter++;
            Debug.WriteLine("画面１ コンストラクタ " + constructorCounter + " 個目");

            this.LoadedCommand = new DelegateCommand(() =>
            {
                Debug.WriteLine("画面１ LoadedCommand");
            });

            this.ButtonCommand = new DelegateCommand(() =>
            {
                // Shell.xaml.csで作成したリージョンの名前と、画面のUserControlクラス名を指定して、画面遷移させる。
                // (パラメータを渡すこともできる)
                this.RegionManager.RequestNavigate("RedRegion", nameof(UserControl2), new NavigationParameters($"id=1"));
            });

            this.ButtonKeepAliveONCommand = new DelegateCommand(() => KeepAlive = true );
            this.ButtonKeepAliveOFFCommand = new DelegateCommand(() => KeepAlive = false);

            this.ButtonIsNavigationTargetONCommand = new DelegateCommand(() => IsNavigationTargetFlag = true);
            this.ButtonIsNavigationTargetOFFCommand = new DelegateCommand(() => IsNavigationTargetFlag = false);
        }

        ~UserControl1ViewModel()
        {
            destructorCounter++;
            Debug.WriteLine("画面１ デストラクタ " + destructorCounter + " 回目");
        }

        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            Debug.WriteLine("画面１ IsNavigationTarget  return value is" + IsNavigationTargetFlag);
            // このメソッドの返す値により、画面のインスタンスを使いまわすかどうか制御できる。
            // true ：インスタンスを使いまわす(画面遷移してもコンストラクタ呼ばれない)
            // false：インスタンスを使いまわさない(画面遷移するとコンストラクタ呼ばれる)
            // メソッド実装なし：trueになる
            return IsNavigationTargetFlag;
        }

        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
            // この画面から他の画面に遷移するときの処理
            Debug.WriteLine("画面１ NavigatedFrom");
        }

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            // 他の画面からこの画面に遷移したときの処理
            Debug.WriteLine("画面１ NavigatedTo");

            // 画面遷移元から、この画面に遷移したときにパラメータを受け取れる。
            string Id = navigationContext.Parameters["id"] as string;
        }
    }
}

```

```実験3.cs
using Microsoft.Practices.Unity;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Regions;
using PrismSample.Views;
using System;
using System.Diagnostics;

namespace PrismSample.ViewModels
{
    class UserControl3ViewModel : BindableBase, INavigationAware
    {
        [Dependency]
        public IRegionManager RegionManager { get; set; }
        public DelegateCommand<object> ButtonCommand { get; }
        public DelegateCommand LoadedCommand { get; }

        public UserControl3ViewModel()
        {
            Debug.WriteLine("画面３ コンストラクタ");

            this.LoadedCommand = new DelegateCommand(() =>
            {
                Debug.WriteLine("画面３ LoadedCommand");
            });

            this.ButtonCommand = new DelegateCommand<object>((param) =>
            {
                var kind = int.Parse((string)param);
                switch (kind)
                {
                    default:
                    case 0: RegionManager.RequestNavigate("RedRegion", nameof(UserControl1), new NavigationParameters($"id=1")); break;
                    case 1: RegionManager.RequestNavigate("RedRegion", nameof(UserControl2), new NavigationParameters($"id=1")); break;
                    case 2: RegionManager.RequestNavigate("RedRegion", nameof(UserControl3), new NavigationParameters($"id=1")); break;
                    case 3: RegionManager.RequestNavigate("BlueRegion", nameof(UserControl1), new NavigationParameters($"id=1")); break;
                    case 4: RegionManager.RequestNavigate("BlueRegion", nameof(UserControl2), new NavigationParameters($"id=1")); break;
                    case 5: RegionManager.RequestNavigate("BlueRegion", nameof(UserControl3), new NavigationParameters($"id=1")); break;
                    case 10: RegionManager.Regions["RedRegion"].RemoveAll(); break;
                    case 11: RegionManager.Regions["BlueRegion"].RemoveAll(); break;
                    case 91: GC.Collect(); break;
                }

            });
        }

        public bool IsNavigationTarget(NavigationContext navigationContext) => false;
        public void OnNavigatedFrom(NavigationContext navigationContext) => Debug.WriteLine("画面３ NavigatedFrom");
        public void OnNavigatedTo(NavigationContext navigationContext) => Debug.WriteLine("画面３ NavigatedTo");
    }
}

```

## 実験内容

その上で、

### ①RequestNavigateする前にKeepAliveをOFFする（IsNavigationTargetはtrueのまま）

- 画面１で、
    - KeepAliveをOFFしてから別の画面(この場合画面２)にRequestNavigateする。
    - それを何度も繰り返す。

そうすると、下記のようなログが残る。

```
画面１ コンストラクタ 1 個目
画面１ NavigatedTo
画面１ LoadedCommand
画面１ NavigatedFrom
画面２ コンストラクタ
画面１ KeepAlive is False
画面２ NavigatedTo
画面２ LoadedCommand
画面１ デストラクタ 1 回目
画面２ NavigatedFrom
画面１ コンストラクタ 2 個目
画面１ NavigatedTo
画面１ LoadedCommand
画面１ NavigatedFrom
画面１ KeepAlive is False
画面２ NavigatedTo
画面２ LoadedCommand
画面２ NavigatedFrom
画面１ コンストラクタ 3 個目
画面１ NavigatedTo
画面１ LoadedCommand
画面１ NavigatedFrom
画面１ KeepAlive is False
画面２ NavigatedTo
画面２ LoadedCommand
画面２ NavigatedFrom
画面１ コンストラクタ 4 個目
画面１ NavigatedTo
画面１ LoadedCommand
画面１ NavigatedFrom
画面１ KeepAlive is False
画面２ NavigatedTo
画面２ LoadedCommand
画面１ デストラクタ 2 回目
画面１ デストラクタ 3 回目
画面１ デストラクタ 4 回目
画面２ NavigatedFrom
```

- 画面１に遷移したとき、毎回コンストラクタが走る。
- LoadCommand(Loadedイベント)も走る。
- 時間経過すると、今表示中の画面１インスタンス以外が、デストラクタが走って回収される。
- GC.Collect()をすると、時間経過しなくても、その時に表示が終わった画面１のインスタンスがGCに回収される。

### ②RequestNavigateする前にIsNavigationTargetの戻り値をfalseにする（KeepAliveはtrueのまま）

- 画面１で、
    - IsNavigationTargetの戻り値をfalseにしてから別の画面にRequestNavigateする。
    - それを何度も繰り返す。

そうすると、下記のようなログが残る。

```
画面１ コンストラクタ 1 個目
画面１ NavigatedTo
画面１ LoadedCommand
画面１ NavigatedFrom
画面２ コンストラクタ
画面１ KeepAlive is True
画面２ NavigatedTo
画面２ LoadedCommand
画面２ NavigatedFrom
画面１ IsNavigationTarget  return value isFalse
画面１ コンストラクタ 2 個目
画面１ NavigatedTo
画面１ LoadedCommand
画面１ NavigatedFrom
画面１ KeepAlive is True
画面２ NavigatedTo
画面２ LoadedCommand
画面２ NavigatedFrom
画面１ IsNavigationTarget  return value isFalse
画面１ IsNavigationTarget  return value isFalse
画面１ コンストラクタ 3 個目
画面１ NavigatedTo
画面１ LoadedCommand
画面１ NavigatedFrom
画面１ KeepAlive is True
画面２ NavigatedTo
画面２ LoadedCommand
画面２ NavigatedFrom
画面１ IsNavigationTarget  return value isFalse
画面１ IsNavigationTarget  return value isFalse
画面１ IsNavigationTarget  return value isFalse
画面１ コンストラクタ 4 個目
画面１ NavigatedTo
画面１ LoadedCommand
画面１ NavigatedFrom
画面１ KeepAlive is True
画面２ NavigatedTo
画面２ LoadedCommand
画面２ NavigatedFrom
画面１ IsNavigationTarget  return value isFalse
画面１ IsNavigationTarget  return value isFalse
画面１ IsNavigationTarget  return value isFalse
画面１ IsNavigationTarget  return value isFalse
画面１ コンストラクタ 5 個目
画面１ NavigatedTo
画面１ LoadedCommand
```

- 結果
    - 画面遷移時、コンストラクタは通らない。(=画面１のインスタンスは使いまわされている)
    - LoadedCommand(Loadedイベント)は実行されている。
    - 画面遷移時、IsNavigationTargetが毎回呼ばれている。
    - IsNavigationTarget  をfalseにして画面遷移した回数だけ、IsNavigationTarget  が呼ばれるようになってしまう
    - 時間がたっても、デストラクタは呼ばれない。
    - GC.Colect()を実行しても、デストラクタは呼ばれない。
        - ＝画面１のインスタンスは使いまわされている。
    - **画面のインスタンスが、画面遷移するたびに新しく作られ、さらにそれがGCに回収されないままたくさん残ってしまっている！？**

そうなると、画面遷移回数が増えると作成される画面１のインスタンスも増えることになるので、おそらくメモリリークにつながる。
**⇒画面のインスタンスを毎回破棄して、次回遷移時に再作成したい、という目的のために、IsNavigationTarget  をfalseにする、ということを行ってはいけない！**

# 思ったこと

現状正直なところ、仕事で出会った流用元のコードでprismを使ってて、prismを理解しないまま出てきたとこだけを場渡り的に調べて乗り切っている状態なので(この記事はその時のメモ)、prismの全体的な理解が足りてないために、こういう部分で詰まってしまう感触を感じ出した。

一度しっかりprismの基礎を勉強しなおした方が、結局は急がば回れで早いかもしれない。
(ただ年強するなら6.3.0ではなく新しいのを勉強したいが...)

# 参考

https://www.nuits.jp/entry/prism-wpf-remove-with-goback