WeakEventパターンを考える

WPFWeakEventパターンというものがあります。これについてすこし考えてみたいと思います。まずはMSDNからの抜粋でどのようなものか見てみましょう。

一般的なアプリケーションでは、イベント ソースにアタッチされているハンドラが、このハンドラをソースにアタッチしたリスナ オブジェクトとの関連によって、破棄されないことがあります。このような状況は、メモリ リークにつながる可能性があります。Windows Presentation Foundation (WPF) では、特定のデザイン パターンが導入されており、このデザイン パターンを使用して、特定のイベントの専用マネージャ クラスを提供し、そのイベントのリスナにインターフェイスを実装することによって、この問題に対処できます。このデザイン パターンは、WeakEvent パターンと呼ばれます。

通常、リスナのイベント ハンドラをアタッチすると、リスナはオブジェクトの有効期間を持ちますが、この有効期間はソースのオブジェクトの有効期間に影響されます (イベント ハンドラが明示的に削除されない場合)。ただし、特定の状況では、リスナのオブジェクトの有効期間を、ソースの有効期間によってではなく、アプリケーションのビジュアル ツリーに現在属しているかどうかなどの別の要素によってのみ制御したい場合もあります。ソース オブジェクトの有効期間がリスナのオブジェクトの有効期間を超える場合、常に通常のイベント パターンによってメモリ リークが発生します。つまり、このリスナは想定した以上に長く維持されます。

WeakEvent パターンの実装は、主にコントロール作成者にとって意味があります。それは、コントロールの作成者には、作成したコントロールの動作と格納およびそれを組み込むアプリケーションに与える影響に対する主な責任があるためです。これには、コントロール オブジェクトの有効期間の動作 (特に、上に示したメモリ リークの問題への対応) が含まれます。

まとめると『WeakEvent トパターンはWPF上の特定のデザインパターンで、リスナ オブジェクトが想定した以上に長く維持されるメモリリークに対処するもので、主にコントロール作成者にとって意味がある』ということです。

利用範囲を考える

デザインパターンなのでどのような状況下で利用するかを考えます。上記の資料よりポイントをひらうと以下の2つの条件がある場合にこのパターンを利用する状況と判断できます。

    • ソースがリスナーより長いライフサイクルである
    • 明示的にイベントの解除ができない

上記を掘り下げてみましょう。まず前者のソースがリスナーより長いライフサイクルということはソースがリスナー以外の強い参照を持っているということです。いいかえれば以下のような場合にあたります。

    • ソースがリスナー以外からの参照がある場合

後者はリスナー側に後処理のタイミングが用意されていなくということなで具体的には以下のようになります。

    • リスナー側でイベント解除できるタイミングの処理(イベントやメソッド)がない

このように考えると適用範囲はある程度絞られる感じがします。実際に適用範囲を表に整理します。

リスナー種別ソースのライフサイクルがリスナーよりも
長い同じ短い
解除タイミングがある
解除タイミングがない

    • ○:WeakEventパターンを適用すべき
    • △:WeakEventパターンを適用しなくても問題ない
    • ?:明示的なイベント解除を行う必要がある(解除タイミングがない場合はイベント設定のタイミングを代用できないか考える。それも難しい場合は別の仕組みを考える)

ソースのライフサイクルが長いのはリスナー以外の参照ある場合、同じはリスナーのみの参照という定義です。ソースのライフサイクルが短いというのは違和感を感じるかもしれませんが、1つのViewやControlに割り当てるViewModelやその要素を切り替えるケースをこれにあたります。一覧+明細画面を1つの画面にして、一覧データを選択すると明細画面をデータを切りかえるように場合がこれにあたります。あとListBoxにVirtualizingStackPanel.VirtualizationMode="Recycling"などを指定する場合もこのケースになります。

適用範囲を考える

次は適用範囲について考えてみます。MSDNには「主にコントロール作成」という表現があるので、コントロールを作成する際に注意すべきことになっています。では通常のWindowクラスをベースとしたViewにはWeakEvent パターンを適用しなくてもよいのでしょうか?ViewModelには考慮は不要なのでしょうか?
この点を考える際のヒントとして様々なWeakEvent パターンを説明している資料がありますので参考します。この資料によると、WeakEvent パターンの実装方式には大きくソース側に実装する方式とそのリスナー側で実装する2つのパターンがあるようです。

    • リスナー側で実装
      • ノーマルなC#のイベントを利用できる
      • リスナー側でクリーンアップを行う仕組みが必要である
    • ソース側で実装
      • イベントソースを変更する必要がある
      • スレッドセーフに登録/解除ができる

各方式それぞれメリット・デメリットがありますので一概に決めれませんが、WPFの組み込みではWeakEventManagerというリスナー側の仕組みが用意されているので、WPFを利用する場合はリスナー側のViewにWeakEvent パターンを適用し、ViewModelはノーマルなC#のイベント実装が第1選択肢になると考えられます。ソース側でWeakEventパターンを実装するほうが抜けがなくて堅実のような気もしますが、このような場合ViewModelの各要素やModelに対しても実装するかを検討する必要があります。また、WeakEventパターンを適用するためには実装コストがかかるため、実装コストとメモリリークのリスク低減に見合うのかの検討が必要です。

MVVMの構成要素に分解してイベント処理を考える

まずはMVVMの構成要素を分解します。View、ViewModel、Modelは当たり前ですね。さらにそれぞれを内部的な構成要素考えると、ViewについてはControl(解除タイミングあり)とViewItem(解除タイミングない)、ViewModelはViewModelのデータになりますがこれはModelと考えても良いので以上の構成要素でイベント処理パターンを整理します。

なおここでの前提として、ViewとViewModelは1対1でViewModelはView以外の外部からの参照を持っていないこととして考えます。したがって、ライフサイクルの関係は以下のようになります。

      • View=ViewModel、 View > Control,ViewItem、 ViewModel > Model

上記以外のModelとViewItem間などのライフサイクルには前提なしになるため不明と考えます

これらを考慮してどのようにイベントをどのように処理するかを整理すると以下のようになります。リスナー(縦)がソース(横)のイベントをリッスンする場合にどのように行うかを表しています。

リスナー/ ソースViewControlViewItemViewModelModel
ViewC#C#C#C#C#
ControlC#C#C#C#C#
ViewItemWeakEventWeakEvent+WeakEvent+WeakEventWeakEvent+
ViewModelC#NGNGC#C#
ModelNGNGNGC#C#

    • C#:通常のC#のイベント実装
    • WeakEvent:WeakEventパターンの適用
    • NG:利用しない

リスナーがViewやControlであれば解除タイミングがあるのでC#で明示的な実装が可能です。ViewModelとModelについてはC#の実装でも良いですがControlやViewItemは参照しないはずです。ViewItemは解除タイミングがないのでソースのライフサイクルが長くなる可能性がある場合はWeakEventを利用します。+が付いているものはソースのライフサイクルが短くなる可能性がありますので必要に応じて解除の仕組みを追加します。

まとめ

WeakEventパターンをどこまで利用するかは個々の事情によって変化すると思いますが、MSDNから推測すると、明示的なイベントの解除が可能か、ソースのライフサイクルの前提はあるかなどを踏まえて使用するのがポイントとになりそうです。