この記事は
PRISMライブラリのDialog Serviceと、ReactivePropertyを併用する場合には
ちゃんとReactivePropertyを破棄しよう!の話
Dialog Service内に限らず、使い終わったものは処分(破棄)するのがマナーです
どういうこと?
ReactivePropertyの後始末 - かずきのBlog@hatena
ReactiveProperty作者様のブログ(↑コレ)から一部引用させていただくと・・・
Disposeしないといけないケース
他のIObservableをソースとしてReactivePropertyなんかを作ったときは、内部動作としてSubscribeをしているので、Disposeを呼ぶ必要があります。
で、どういうこと?
さらに同記事から引用させていただくと・・・
こういう時は、ReactivePropertyを持っているクラスが不要になったタイミングでDisposeを呼んでやらないと、予期せぬ動作をすることがあるかもしれません。
この予期せぬ動作に遭遇したので、経緯と対策(私はどうしたか)を書き残す
なぜDialog Serviceなのか?
ダイヤログは、その性質上、呼び出しとクローズ処理が頻繁に行われる
つまり(それがシングルトンでない場合)、頻繁にインスタンスが生成され、もしそれが適切なタイミングで破棄されなければ、メモリに悪影響を及ぼすことが予想される
ダイヤログはその頻度に応じて悪影響の度合いが大きくなりがちであり
また今回も自分はダイヤログを使っていて気付いた
⇒ 以上の理由から、この記事ではダイヤログをやり玉にあげている
検証用のコード
デフォルトのMainWindowから自作MyDialog(ユーザーコントロール)を開くだけのアプリをつくる
Prism.Wpfのバージョンは7.2.0をつかった
モデルとダイヤログの準備
モデル
モデルは一定間隔で連番を生成するObservablePropをもっている
超絶参考:【Reactive Extensions】 Hot変換はどういう時に必要なのか? - 問題の解決策「Hot変換」
ModelImpl.cs
// コンストラクター内
ObservableProp = Observable
.Timer(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(2)) // はじめに3秒待って、2秒間隔
.Do(x => Debug.WriteLine($"モデル{nameof(ObservableProp)}: {x}")) // ただの表示
.Select(x => x)
.Publish()
.RefCount();
// 参考の記事を見ずに自力でやったときは
// Select文の中にDebug.Write書いたり、Publish以下HOT変換してなかったり・・・
ダイヤログ
ダイヤログは、モデルのObservableをもとにToReactivePropertyしてつくられた
_reactivePropertyAAと_reactivePropertyBBをもっている
MyDialogViewModel.cs
// コンストラクター内
// Observableから流れてきた値と、ReactivePropertyの初期値を区別するために
// -77,-88を初期値として与えている
_reactivePropertyAA = model.ObservableProp.ToReactiveProperty(initialValue: -77);
_reactivePropertyBB = model.ObservableProp.ToReactiveProperty(initialValue: -88);
// _observerAA/BBは OnNext, OnCompletedをDebug.Writeするだけのオブザーバー
_reactivePropertyAA.Subscribe(_observerAA);
_reactivePropertyBB.Subscribe(_observerBB);
Prism.Unity で DI
モデルとダイヤログをそれぞれPrism.UnityのDIコンテナに登録する
App.xaml.cs
// Dependecy Injection用コンテナへ登録
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton<IMyModel, ModelImpl>(); // モデル
containerRegistry.RegisterDialog<MyDialog, MyDialogViewModel>(); // ダイヤログ
}
Case1: ReactivePropertyを破棄しなかった場合
メインウィンドウからダイヤログを開いて閉じる動作を3回行う
以下ログに説明を入れたもの。(キャプチャした時のログとは違うので、数字が異なる箇所もある)
Case1_ReactivePropertyを破棄しなかった場合
// 出力結果
Show Dialog ボタンが押されました!!
ModelImpl: コンストラクターが呼ばれました 17294952 // <- 末尾はGetHashCodeで得たインスタンス識別用
MyDialogViewModel: コンストラクターが呼ばれました 58713911
_reactivePropertyAA OnNext -77 // ReactiveProperty初期値
_reactivePropertyBB OnNext -88
MyDialog: ビューのコンストラクターが呼ばれました 50102218
OnDialogOpened
モデルObservableProp: 0
_reactivePropertyAA OnNext 0
_reactivePropertyBB OnNext 0
モデルObservableProp: 1
_reactivePropertyAA OnNext 1
_reactivePropertyBB OnNext 1
モデルObservableProp: 2
_reactivePropertyAA OnNext 2
_reactivePropertyBB OnNext 2
Close Dialog ボタンが押されました!! // <- ここでダイヤログを閉じた
OnDialogClosed
// ダイヤログがクローズした後でも、OnNextが実行され続ける
モデルObservableProp: 3
_reactivePropertyAA OnNext 3
_reactivePropertyBB OnNext 3
モデルObservableProp: 4
_reactivePropertyAA OnNext 4
_reactivePropertyBB OnNext 4
Show Dialog ボタンが押されました!! // <- もう1度ダイヤログを開く
MyDialogViewModel: コンストラクターが呼ばれました 22985394 // <- 初めのダイヤログとは違うインスタンスが生成される(PRISM DialogServiceの挙動)
_reactivePropertyAA OnNext -77
_reactivePropertyBB OnNext -88
MyDialog: ビューのコンストラクターが呼ばれました 35909463
OnDialogOpened
MyDialog: ビューのデストラクターが呼ばれました 50102218 // <- ここで初めに開いたダイヤログのデストラクタが呼ばれる
MyDialogViewModel: デストラクターが呼ばれました 58713911 // デストラクタが呼ばれるタイミングは事前にわからない
モデルObservableProp: 5
_reactivePropertyAA OnNext 5
_reactivePropertyBB OnNext 5
_reactivePropertyAA OnNext 5
_reactivePropertyBB OnNext 5
モデルObservableProp: 6
_reactivePropertyAA OnNext 6
_reactivePropertyBB OnNext 6
_reactivePropertyAA OnNext 6
_reactivePropertyBB OnNext 6
モデルObservableProp: 7
_reactivePropertyAA OnNext 7
_reactivePropertyBB OnNext 7
_reactivePropertyAA OnNext 7
_reactivePropertyBB OnNext 7
Close Dialog ボタンが押されました!! // 2回目のダイヤログを閉じる
OnDialogClosed
モデルObservableProp: 8
_reactivePropertyAA OnNext 8 // <- 1回目, 2回目のダイヤログのオブザーバーが動いている
_reactivePropertyBB OnNext 8
_reactivePropertyAA OnNext 8
_reactivePropertyBB OnNext 8
モデルObservableProp: 9
_reactivePropertyAA OnNext 9
_reactivePropertyBB OnNext 9
_reactivePropertyAA OnNext 9
_reactivePropertyBB OnNext 9
Show Dialog ボタンが押されました!! // <- 3回目のダイヤログを開く
MyDialogViewModel: コンストラクターが呼ばれました 61469371
_reactivePropertyAA OnNext -77
_reactivePropertyBB OnNext -88
MyDialog: ビューのコンストラクターが呼ばれました 66482253
OnDialogOpened
モデルObservableProp: 10
_reactivePropertyAA OnNext 10
_reactivePropertyBB OnNext 10
_reactivePropertyAA OnNext 10
_reactivePropertyBB OnNext 10
_reactivePropertyAA OnNext 10
_reactivePropertyBB OnNext 10
モデルObservableProp: 11
_reactivePropertyAA OnNext 11 // <- ダイヤログを開いた数 3 x ReactivePropertyの数 2 =>
_reactivePropertyBB OnNext 11 // 6 回の OnNext が実行されつづける
_reactivePropertyAA OnNext 11
_reactivePropertyBB OnNext 11
_reactivePropertyAA OnNext 11
_reactivePropertyBB OnNext 11
ダイヤログが破棄された(デストラクタが呼ばれた)あとでも
OnNextが実行されつづけており、不気味な感じがする。
この調子でダイヤログを開いて閉じてを繰り返すと
ダイヤログから見捨てられたオブザーバーが延々とOnNextを実行しつづけて・・・
ダイヤログビューモデルクラスのprivate変数であるReactivePropertyと
それをSubscribeしているオブザーバーが
ダイヤログがいなくなってから(デストラクタ後)も存続し続けている(ように見える)
この周辺の理解がまだ足りなく、もしかしたら重大な誤解をしているかもしれない(!?)
Case2: ReactivePropertyを破棄した場合
ダイヤログビューモデルを変更する
ReactivePropertyをnewしたタイミングで、同時に_disposablesにAddToしておいて
ダイヤログが閉じたときに、呼ばれるOnDialogClosedメソッドの中で_disposablesをDispose()する
MyDialogViewModel.cs
// コンストラクター内
// 末尾に .AddTo() を付加した
_reactivePropertyAA = model.ObservableProp.ToReactiveProperty(initialValue: -77).AddTo(_disposables);
_reactivePropertyBB = model.ObservableProp.ToReactiveProperty(initialValue: -88).AddTo(_disposables);
//Rxのdisposablesに破棄するものを詰め込んで、IDailogAwareのOnDialogClosedメソッドでDisposeする
private readonly CompositeDisposable _disposables = new CompositeDisposable();
public void OnDialogClosed() => _disposables?.Dispose();
実際に動作している動画
以下ログに説明を入れたもの。(キャプチャした時のログとは違うので、数字が異なる箇所もある)
Case2_ReactivePropertyを破棄した場合
// 出力結果
Show Dialog ボタンが押されました!!
ModelImpl: コンストラクターが呼ばれました 65158399
MyDialogViewModel: コンストラクターが呼ばれました 43339000
_reactivePropertyAA OnNext -77 // ReactiveProperty初期値
_reactivePropertyBB OnNext -88
MyDialog: ビューのコンストラクターが呼ばれました 14081900 // <- ここでダイヤログを開く
OnDialogOpened
モデルObservableProp: 0
_reactivePropertyAA OnNext 0
_reactivePropertyBB OnNext 0
モデルObservableProp: 1
_reactivePropertyAA OnNext 1
_reactivePropertyBB OnNext 1
モデルObservableProp: 2
_reactivePropertyAA OnNext 2
_reactivePropertyBB OnNext 2
Close Dialog ボタンが押されました!!
OnDialogClosed
_reactivePropertyAA Completed !! // ReactivePropertyのDisposeでOnCompleted()が呼ばれる
_reactivePropertyBB Completed !!
MyDialogViewModel: デストラクターが呼ばれました 43339000
MyDialog: ビューのデストラクターが呼ばれました 14081900
// 以後、コンソール出力なし
おわりに
繰り返しになるが
ダイヤログのビューとビューモデルが破棄された(デストラクタが呼ばれた)あとでも
メンバ変数のオブザーバーが動きつづけるあたり、よくわからない
なににせよ、よそのクラスのIObservableをToReactivePropertyしたときにはAddTo()することを
忘れないようにしておけばいいと思う
ほか参考にさせていただいた記事など
本題よりも、RxのHot/Coldの理解に時間を割いた気がする
Rx入門 (14) - Cold to Hot変換
Reactive Extensions再入門 その5「HotとCold」
Reactive Extensions再入門 その36「ColdからHotへ!Publishメソッドと参照カウンタ?RefCountメソッド」
Reactive Extensions再入門 その45「Scheduler」
ReactiveProperty を編む【step: 7 .NET Core WPF MVVM ReactiveProperty 入門 2020】
【ReactiveProperty】きちんとDisposeしよう | 泥庭
↧