もくじ
→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のインスタンスが破棄される。 再度同じ画面にRequestNavigateした際に、ViewModelのインスタンスが再作成されている。 |
②IsNavigationTargetにfalseを返させる | ViewModelのインスタンスは破棄されない。 再度同じ画面にRequestNavigateした際は、前の画面のインスタンスは破棄せず保持で、新しいVMのインスタンスを作成し、そっちに遷移する。 |
つまり、両方ともコンストラクタが呼ばれる、イコール、新しいVMのインスタンスが作成されてるが、KeepAliveをfalseにする方は古い(別画面に遷移後の、元の画面の)インスタンスは破棄されてるが、IsNavigationTargetの戻り値をfalseにする方は、古いインスタンスが実は破棄されてない。
そのため、②のほうは、画面遷移回数が増えると作成される画面1のインスタンスも増えることになるので、おそらくメモリリークにつながる。
⇒画面のインスタンスを毎回破棄して、次回遷移時に再作成したい、という目的のために、IsNavigationTarget をfalseにする、ということを行ってはいけない!
今回やりたい「画面遷移するたびに毎回画面のVMのインスタンスを破棄したい」ということをする場合は、KeepAliveをOFFしてからRequestNavigateで画面遷移させるのが良い。
結論がでるまでに試したこと
実験コード
- ViewModelの
IsNavigationTarget
メソッドに、通ったことがわかるようにDebug.WriteLineでログ出力処理を入れた - ViewModelのデストラクタに、通ったことがわかるようにDebug.WriteLineでログ出力処理を入れた
- ViewModelに、自分自身の
- IsNavigationTargetメソッドの戻り値の値をfalseにできるようにした
- KeepAliveをfalseにできるようにした
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("画面1 KeepAlive is " + keepalive);
return keepalive;
}
set
{
keepalive = value;
}
}
private bool keepalive = true;
public bool IsNavigationTargetFlag = true;
public UserControl1ViewModel()
{
constructorCounter++;
Debug.WriteLine("画面1 コンストラクタ " + constructorCounter + " 個目");
this.LoadedCommand = new DelegateCommand(() =>
{
Debug.WriteLine("画面1 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("画面1 デストラクタ " + destructorCounter + " 回目");
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
Debug.WriteLine("画面1 IsNavigationTarget return value is" + IsNavigationTargetFlag);
// このメソッドの返す値により、画面のインスタンスを使いまわすかどうか制御できる。
// true :インスタンスを使いまわす(画面遷移してもコンストラクタ呼ばれない)
// false:インスタンスを使いまわさない(画面遷移するとコンストラクタ呼ばれる)
// メソッド実装なし:trueになる
return IsNavigationTargetFlag;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
// この画面から他の画面に遷移するときの処理
Debug.WriteLine("画面1 NavigatedFrom");
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
// 他の画面からこの画面に遷移したときの処理
Debug.WriteLine("画面1 NavigatedTo");
// 画面遷移元から、この画面に遷移したときにパラメータを受け取れる。
string Id = navigationContext.Parameters["id"] as string;
}
}
}
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("画面3 コンストラクタ");
this.LoadedCommand = new DelegateCommand(() =>
{
Debug.WriteLine("画面3 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("画面3 NavigatedFrom");
public void OnNavigatedTo(NavigationContext navigationContext) => Debug.WriteLine("画面3 NavigatedTo");
}
}
実験内容
その上で、
①RequestNavigateする前にKeepAliveをOFFする(IsNavigationTargetはtrueのまま)
- 画面1で、
- KeepAliveをOFFしてから別の画面(この場合画面2)にRequestNavigateする。
- それを何度も繰り返す。
そうすると、下記のようなログが残る。
画面1 コンストラクタ 1 個目
画面1 NavigatedTo
画面1 LoadedCommand
画面1 NavigatedFrom
画面2 コンストラクタ
画面1 KeepAlive is False
画面2 NavigatedTo
画面2 LoadedCommand
画面1 デストラクタ 1 回目
画面2 NavigatedFrom
画面1 コンストラクタ 2 個目
画面1 NavigatedTo
画面1 LoadedCommand
画面1 NavigatedFrom
画面1 KeepAlive is False
画面2 NavigatedTo
画面2 LoadedCommand
画面2 NavigatedFrom
画面1 コンストラクタ 3 個目
画面1 NavigatedTo
画面1 LoadedCommand
画面1 NavigatedFrom
画面1 KeepAlive is False
画面2 NavigatedTo
画面2 LoadedCommand
画面2 NavigatedFrom
画面1 コンストラクタ 4 個目
画面1 NavigatedTo
画面1 LoadedCommand
画面1 NavigatedFrom
画面1 KeepAlive is False
画面2 NavigatedTo
画面2 LoadedCommand
画面1 デストラクタ 2 回目
画面1 デストラクタ 3 回目
画面1 デストラクタ 4 回目
画面2 NavigatedFrom
- 画面1に遷移したとき、毎回コンストラクタが走る。
- LoadCommand(Loadedイベント)も走る。
- 時間経過すると、今表示中の画面1インスタンス以外が、デストラクタが走って回収される。
- GC.Collect()をすると、時間経過しなくても、その時に表示が終わった画面1のインスタンスがGCに回収される。
②RequestNavigateする前にIsNavigationTargetの戻り値をfalseにする(KeepAliveはtrueのまま)
- 画面1で、
- IsNavigationTargetの戻り値をfalseにしてから別の画面にRequestNavigateする。
- それを何度も繰り返す。
そうすると、下記のようなログが残る。
画面1 コンストラクタ 1 個目
画面1 NavigatedTo
画面1 LoadedCommand
画面1 NavigatedFrom
画面2 コンストラクタ
画面1 KeepAlive is True
画面2 NavigatedTo
画面2 LoadedCommand
画面2 NavigatedFrom
画面1 IsNavigationTarget return value isFalse
画面1 コンストラクタ 2 個目
画面1 NavigatedTo
画面1 LoadedCommand
画面1 NavigatedFrom
画面1 KeepAlive is True
画面2 NavigatedTo
画面2 LoadedCommand
画面2 NavigatedFrom
画面1 IsNavigationTarget return value isFalse
画面1 IsNavigationTarget return value isFalse
画面1 コンストラクタ 3 個目
画面1 NavigatedTo
画面1 LoadedCommand
画面1 NavigatedFrom
画面1 KeepAlive is True
画面2 NavigatedTo
画面2 LoadedCommand
画面2 NavigatedFrom
画面1 IsNavigationTarget return value isFalse
画面1 IsNavigationTarget return value isFalse
画面1 IsNavigationTarget return value isFalse
画面1 コンストラクタ 4 個目
画面1 NavigatedTo
画面1 LoadedCommand
画面1 NavigatedFrom
画面1 KeepAlive is True
画面2 NavigatedTo
画面2 LoadedCommand
画面2 NavigatedFrom
画面1 IsNavigationTarget return value isFalse
画面1 IsNavigationTarget return value isFalse
画面1 IsNavigationTarget return value isFalse
画面1 IsNavigationTarget return value isFalse
画面1 コンストラクタ 5 個目
画面1 NavigatedTo
画面1 LoadedCommand
- 結果
- 画面遷移時、コンストラクタは通らない。(=画面1のインスタンスは使いまわされている)
- LoadedCommand(Loadedイベント)は実行されている。
- 画面遷移時、IsNavigationTargetが毎回呼ばれている。
- IsNavigationTarget をfalseにして画面遷移した回数だけ、IsNavigationTarget が呼ばれるようになってしまう
- 時間がたっても、デストラクタは呼ばれない。
- GC.Colect()を実行しても、デストラクタは呼ばれない。
- =画面1のインスタンスは使いまわされている。
- 画面のインスタンスが、画面遷移するたびに新しく作られ、さらにそれがGCに回収されないままたくさん残ってしまっている!?
そうなると、画面遷移回数が増えると作成される画面1のインスタンスも増えることになるので、おそらくメモリリークにつながる。
⇒画面のインスタンスを毎回破棄して、次回遷移時に再作成したい、という目的のために、IsNavigationTarget をfalseにする、ということを行ってはいけない!
思ったこと
現状正直なところ、仕事で出会った流用元のコードでprismを使ってて、prismを理解しないまま出てきたとこだけを場渡り的に調べて乗り切っている状態なので(この記事はその時のメモ)、prismの全体的な理解が足りてないために、こういう部分で詰まってしまう感触を感じ出した。
一度しっかりprismの基礎を勉強しなおした方が、結局は急がば回れで早いかもしれない。
(ただ年強するなら6.3.0ではなく新しいのを勉強したいが...)
参考