WPF の MVVM パターンでコマンドを利用するとメモリーリークするという話を聞いて、チョット調べてみましたが、具体的にデータを提示して指摘しているところが見当たらなかったので、試してみました。結果を書くと、アプリケーションの終了時までオブジェクトがガベージコレクションの対象にならないパターンも確かにありました。ただ、それは「コマンドを使っているから」ではなくて、オブジェクト(ウィンドウもしくはビューモデル)への参照をクリアしていない場合でした。
試してみた手順は、つぎのとおりです。
- コードビハインドでどうなるか
- モードレスなウィンドウでコマンドを実装してみる
- モーダルなウィンドウでコマンドを実装してみる
なお、オブジェクトが解放されるかどうかは、ウィンドウを閉じた後に、
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
を行い、オブジェクトのデストラクタが起動されるかどうかで判断しています。
まずは、コードビハインドでどうなるかですが、ウィンドウを閉じるとオブジェクトは解放されます。操作手順は次のようにしています。
- モードレスウィンドウを3つ開く
- ボタンを1回クリックする
- 2回目に開いたウィンドウのタイトルバーの閉じるボタンをクリックして閉じる
- GC を行う
- 残り2つのウィンドウを親ウィンドウの「すべて閉じる」ボタンのクリックで閉じる
- GC を行う
結果は次のようになりました。
22:07:56 Window 生成: WindowUseClickEvent
22:07:59 Window 生成: WindowUseClickEvent
22:07:01 Window 生成: WindowUseClickEvent
22:07:05 ボタンがクリックされました。
22:07:11 == GC を実施します。 ==
22:07:11 Window デストラクタ起動: WindowUseClickEvent
22:07:20 == GC を実施します。 ==
22:07:20 Window デストラクタ起動: WindowUseClickEvent
22:07:20 Window デストラクタ起動: WindowUseClickEvent
ウィンドウを閉じると解放されています。
ここでちょっとイタヅラをします。親ウィンドウでは、すべて閉じる操作をするために、作ったモードレスウィンドウへの参照を保持しています。この実験ではモードレスウィンドウの WindowClosed イベントで親ウィンドウからの参照をクリアするようにしていますが、このクリア部分をコメントアウトしてみます。
22:07:44 Window 生成: WindowUseClickEvent
22:07:46 Window 生成: WindowUseClickEvent
22:07:49 Window 生成: WindowUseClickEvent
22:07:52 ボタンがクリックされました。
22:07:56 == GC を実施します。 ==
22:07:59 == GC を実施します。 ==
ということで、生きている親ウィンドウから参照されているためにガベージコレクションの対象になりません(親ウィンドウが閉じられたタイミングでガベージコレクションの対象になります)。
次は「モードレスなウィンドウでコマンドを実装してみる」を試してみました。このウィンドウにはコンテキストメニュー「編集」も作ってみました。操作手順は次のとおりです。
- モードレスウィンドウを3つ開く
- ボタンを1回クリックする
- 2回目に開いたウィンドウのコンテキストメニューをクリックする
- 2回目に開いたウィンドウのタイトルバーの閉じるボタンをクリックして閉じる
- GC を行う
- 1回目に開いたウィンドウのコンテキストメニューをクリックする
- 残り2つのウィンドウを親ウィンドウの「すべて閉じる」ボタンのクリックで閉じる
- GC を行う
結果は次のようになりました。
23:07:14 ViewModel 生成: UseCommandModelessWindowViewModel 1回目
23:07:14 Window 生成: UseCommandModelessWindow 1回目
23:07:14 RelayCommand 生成
23:07:17 ViewModel 生成: UseCommandModelessWindowViewModel 2回目
23:07:17 Window 生成: UseCommandModelessWindow 2回目
23:07:17 RelayCommand 生成
23:07:21 ViewModel 生成: UseCommandModelessWindowViewModel 3回目
23:07:21 Window 生成: UseCommandModelessWindow 3回目
23:07:21 RelayCommand 生成
23:07:43 ボタンがクリックされました。
23:07:45 RelayCommand 生成
23:07:47 編集メニューがクリックされました。
23:07:53 == GC を実施します。 ==
23:07:53 ViewModel デストラクタ起動: 2回目に生成された UseCommandModelessWindowViewModel
23:07:53 Window デストラクタ起動: 2回目に生成された UseCommandModelessWindow
23:07:53 RelayCommand デストラクタ起動
23:07:53 RelayCommand デストラクタ起動
23:07:05 RelayCommand 生成
23:07:12 編集メニューがクリックされました。
23:07:16 == GC を実施します。 ==
23:07:16 RelayCommand デストラクタ起動
23:07:16 ViewModel デストラクタ起動: 3回目に生成された UseCommandModelessWindowViewModel
23:07:16 Window デストラクタ起動: 1回目に生成された UseCommandModelessWindow
23:07:16 ViewModel デストラクタ起動: 1回目に生成された UseCommandModelessWindowViewModel
23:07:16 RelayCommand デストラクタ起動
23:07:16 RelayCommand デストラクタ起動
23:07:16 Window デストラクタ起動: 3回目に生成された UseCommandModelessWindow
コンテキストメニューの表示で生成されたコマンドも含めて、ウィンドウ、ビューモデル、コマンドが綺麗に回収されています。
ここで先ほどと同じように、親ウィンドウからの参照をクリアする部分をコメントアウトしてみます。
23:07:02 ViewModel 生成: UseCommandModelessWindowViewModel 1回目
23:07:02 Window 生成: UseCommandModelessWindow 1回目
23:07:02 RelayCommand 生成
23:07:05 ViewModel 生成: UseCommandModelessWindowViewModel 2回目
23:07:05 Window 生成: UseCommandModelessWindow 2回目
23:07:05 RelayCommand 生成
23:07:09 ViewModel 生成: UseCommandModelessWindowViewModel 3回目
23:07:09 Window 生成: UseCommandModelessWindow 3回目
23:07:09 RelayCommand 生成
23:07:14 ボタンがクリックされました。
23:07:15 RelayCommand 生成
23:07:17 編集メニューがクリックされました。
23:07:23 == GC を実施します。 ==
23:07:29 RelayCommand 生成
23:07:31 編集メニューがクリックされました。
23:07:39 == GC を実施します。 ==
ということで、先程と同じく生きている親ウィンドウから参照されているためにガベージコレクションの対象になりません(親ウィンドウが閉じられたタイミングでガベージコレクションの対象になります)。
ここでもう一つイタヅラです。変更した親ウィンドウからの参照をクリアする部分をコメントアウトを元に戻して、ビューモデルの INotifyPropertyChanged インターフェイスの実装を解除してみます( : INotifyPropertyChanged を外してみる)。この操作手順は次のようにしてみます。
- モードレスウィンドウを開く
- ボタンを1回クリックする
- ウィンドウのコンテキストメニューをクリックする
- ウィンドウのタイトルバーの閉じるボタンをクリックして閉じる
- GC を行う
- モードレスウィンドウを開く
- GC を行う
- ウィンドウのコンテキストメニューをクリックする
- 親ウィンドウの「すべて閉じる」ボタンのクリックで閉じる
- GC を行う
23:07:15 ViewModel 生成: UseCommandModelessWindowViewModel 1回目
23:07:15 Window 生成: UseCommandModelessWindow 1回目
23:07:15 RelayCommand 生成
23:07:20 ボタンがクリックされました。
23:07:21 RelayCommand 生成
23:07:23 編集メニューがクリックされました。
23:07:32 == GC を実施します。 ==
23:07:32 Window デストラクタ起動: 1回目に生成された UseCommandModelessWindow
23:07:38 ViewModel 生成: UseCommandModelessWindowViewModel 2回目
23:07:38 Window 生成: UseCommandModelessWindow 2回目
23:07:38 RelayCommand 生成
23:07:44 == GC を実施します。 ==
23:07:44 RelayCommand デストラクタ起動
23:07:45 RelayCommand デストラクタ起動
23:07:45 ViewModel デストラクタ起動: 1回目に生成された UseCommandModelessWindowViewModel
23:07:52 RelayCommand 生成
23:07:53 編集メニューがクリックされました。
23:07:59 == GC を実施します。 ==
23:07:59 Window デストラクタ起動: 2回目に生成された UseCommandModelessWindow
ウィンドウを閉じて回収されるのは、ウィンドウだけになります。ビューモデルとコマンドはメモリーリーク?と思うと、再度ウィンドウを開いて、ウィンドウ、ビューモデル、コマンドが生成されたあとに、コンテキストメニューの表示で生成されたコマンドも含めて、ビューモデルとコマンドが回収されています。どこらへんが掴んでいるのかと SDK のデバッグツール WinDbg で見てみると、Data.ValueChangedEventManager というのが見えて、値変化の監視系で掴まれたままになっているみたいですね。まぁビューモデルの実装で INotifyPropertyChanged を実装しない事はないと思うので、問題になることは無いでしょうと 😉
最後に「モーダルなウィンドウでコマンドを実装してみる」を試してみます。操作手順は次のとおりです。
- モーダルウィンドウを開く
- タイトルバーの閉じるボタンをクリックして閉じる
- GC を行う
- モーダルウィンドウを開く
- OK ボタンをクリックする
- GC を行う
結果は次のようになりました。
00:07:39 ViewModel 生成: UseCommandModalWindowViewModel
00:07:39 Window 生成: UseCommandModalWindow
00:07:39 RelayCommand 生成
00:07:45 RelayCommand デストラクタ起動
00:07:45 ViewModel デストラクタ起動: UseCommandModalWindowViewModel
00:07:45 Window デストラクタ起動: UseCommandModalWindow
00:07:49 == GC を実施します。 ==
00:07:52 ViewModel 生成: UseCommandModalWindowViewModel
00:07:52 Window 生成: UseCommandModalWindow
00:07:52 RelayCommand 生成
00:07:54 ボタンがクリックされました。
00:07:56 RelayCommand デストラクタ起動
00:07:56 ViewModel デストラクタ起動: UseCommandModalWindowViewModel
00:07:56 Window デストラクタ起動: UseCommandModalWindow
00:07:56 == GC を実施します。 ==
ガベージコレクションを実施する前に綺麗に回収されていますね 😀
モーダルなウィンドウでは、ウィンドウ管理のためにウィンドウへの参照を保持する必要がないので、ShowDialog を行うメソッドから抜けた時点で回収されています。
ということで、
- ビューモデルは INotifyPropertyChanged を実装する
- モードレスウィンドウでは、ウィンドウ管理のためにウィンドウあるいはビューモデルへの参照を保持する場合、ウィンドウが閉じられた際の参照のクリアをきちんと行えば回収される
- モーダルウィンドウでは、通常ウィンドウへの参照はローカルでしか持たないと思われるので、問題なく回収される
ということだと思われます。
「WPF でコマンドを使用するとメモリリークする?」への1件のフィードバック