the sea of fertility

C#er blog - http://ugaya40.net より移転。今度のブログは落ちない

WPF/Silverlight/Windows Phone共通WeakEvent機構

この記事は2011/12/12に旧ブログ(http://ugaya40.net)に投稿した記事を私の旧ブログ閉鎖に伴い移管したものです。

注)十中八九ストアアプリでも動きます(2014/12/25追記)

この記事はSilverlight Advent Calender 2011の12/11分の記事です。前日は@neueccさんの.NETの標準シリアライザ(XML/JSON)の使い分けまとめでした。

WeakEvent機構というものはXAML系技術で必要なものです。しかし開発者がWeakEventを用意する仕組みは.NET4(WPF)でしか用意されていません。この記事ではWeakEventとは何か?どういうときに必要なのか?どうやって動くのか?を簡単に説明し、そしてWPF/SIlverlight/Windows Phone7で共通で使えるWeakEvent機構の実装を紹介します。

 

WeakEventとは?

WeakEventとはイベントハンドラの解除忘れによるメモリリークが発生しないイベント機構の事です。

.NETでメモリリークが発生する主要要因の一つとして、イベント受信側クラスがイベントハンドラの登録を解除しない事で発生するメモリリークが挙げられます。ステートレスなWebプラットフォーム上ではほとんど発生を考慮しなくて良い問題ですが、ステートフルなリッチクライアントではよく問題になります。WeakEvent機構はそのためのソリューションの一つとしてもともとはWPF用に用意されていた機構です。その証拠に.NET4ではWeakEventManagerなどのWeakEvent関連クラスはSystem.Windows名前空間に用意されています。

 

弱いイベントパターン

MSDNライブラリ

http://msdn.microsoft.com/ja-jp/library/aa970850.aspx

 

何故WeakEventが必要か?

WeakEventは.NETの標準の作法に反するものであり、すべての局面で妥当なソリューションであるとは言えません。しかしXAML系テクノロジではどうしても必要となってくる技術です。

 

例えばDataTemplateを見てみましょう。

f:id:m_onoue:20141225141145p:plain

ごく一般的なListBoxのコードです。

DataTemplateの中身がTestUserControlとして切り出されているのが少し変わったところでしょうか?。

TestUserControlのEventRaisedObjectは任意のオブジェクトをバインドし、そのオブジェクトは特定のイベントを発行してくるとします。TestUserControlでは、EventRaisedObjectにバインドされたオブジェクトのイベントをごく普通に受信し処理を行っていると考えてください。XAML上でバインディングされているItems/Testはアプリケーションの実行期間中に新しく生成されたり、消滅したり、増減したりしないと仮定してください。

ListBoxのアイテム数は100件もあればよいでしょう。スクロールをしばらく上下に動かし続けてください。メモリ使用量は増え続けます。GCが走っても、スクロールを上下し続けるとどんどんメモリ使用量が増えていきます。

そう、TestUserControlの中でEventRaisedObjectから発生したイベントを普通に受信しているとこのケースでは必ずメモリリークしてしまいます。TestUserControlのインスタンスが無尽蔵に増え続けます。

何故でしょうか。

TestUserControlはTestオブジェクトが発行するイベントを監視しています。これはつまり、TestオブジェクトはTestUserControlへの参照を保持しているという事になります。

f:id:m_onoue:20141225141156p:plain

そしてDataTemplateの中身は開発者が制御できない(しにくい)タイミングで生成や消滅を繰り返すことになります。

これはXAML系テクノロジでのMVVMパターンの大きな動機の一つ(イベントハンドラでオブジェクトを触ろうとしても存在していたりしなかったりするので、ViewModel層の導入が必至となる)なのですが、今回はまぁおいておきましょう。

 

問題は開発者がDataTemplate内のオブジェクトのライフサイクルを制御できないために、イベントを解除するタイミングがないという事です。

つまりイベントハンドラ解放漏れによるメモリリークが発生するという事になります。

(実はFrameworkElementがイベントを受信するだけならなんとかイベントハンドラ解放タイミングを確保できることが大半です。しかしイベントを受信しているオブジェクトが添付プロパティとかになると完全にイベントハンドラを解放するタイミングが存在しません。)

またXAML系テクノロジの基礎の基礎、Binding機構についても考えてみましょう。バインドされたオブジェクトが自身の変更をバインド先に通知するにはINotifyPropertyChangedインターフェースを使用します。そしてINotifyPropertyChangedインターフェースはイベントで変更をバインド先に通知します。

DataTemplateの中身などは、まさにBindingマークアップ拡張だらけですから、これはゆゆしき問題です。

しかしBindingマークアップ拡張を書いた部分でメモリリークは発生しません。なぜでしょうか?

Bindingマークアップ拡張では内部的にWeakEventを使用することで、ハンドラを解放しなくてもメモリリークが発生しないようにしているのです。

(今回のようなシンプルなケースに限ればItemsControlはバインディングのためのイベントハンドラを解除してくれます。しかしコントロールが入れ子になったり、BindingPathが複雑化したりすると途端にイベントハンドラが明示的には解放されなくなります。だからこそWeakEventが必要になるのです)

XAML系技術とWeakEvent

XAMLでUIの構造を宣言的に構築、各コントロールはそれをもとに内部を自由に実装(仮想化なども含めて)、そしてContentControl/ItemsControl/DataTemplate/ItemTemplateなど、XAML系技術の主要キーワードたちはコントロールの表現力を大幅に増強します。それはまたコントロールの入れ子構造やオブジェクトの実際の生滅などが開発者から把握しにくくなることでもあります。

もしWeakEventが無ければ(つまりBindingなども手動解放であれば)人力でとてもイベントの解放を管理できるものではありません。また、各コントロールの内部実装を踏まえた上で実装を行わなければならないのであるのなら、それはコントロールのコンポーネント化と相反することです。しかしWeakEventはCLRのお作法的には確かに反則です。どうとらえるべきでしょうか?

XAML系技術にとってWeakEventは反則ではない

XAML系技術はXAMLというDSLのために、C#(というかOOP)での原則を捻じ曲げてでも、今までのプラットフォーム(Windows Formsなど)ではメソッドで提供していたメンバをプロパティで提供しています。また、依存関係プロパティなど、CLRの本来のプロパティの上に新しいシステムを構築したりもしています。そしてBinding機構の内部ではWeakEventが使用されていることから、WeakEventはそういった枠組みの中にあると私は判断しています。「XAMLによるUI構築」を実現するための大切な縁の下の力持ちなのです。

その上でも、後述しますがデメリットもありますので、WeakEventは誰もが触るべきものだとは思いません。しかし知っておかなければライブラリ・フレームワーク作者としては不安が残るところでもあると思います。

WeakEventのメリットとデメリット

メリット

デメリット

  • ハンドラを明示的に開放しない、つまりGCにハンドラの解放を任せる事は、つまりGCが動作するまでハンドラが開発者にとって不測の発火をもたらすことがある(明示的に開放できるタイミングがある場合は、普通のイベントと一緒で解除もできる)

デメリットは、これは設計で解決可能な問題です。たとえばMVVMパターンを使用した際、主なWeakEventの活躍場はViewModelからViewへのイベント通知ですが、ViewModelの発生させたイベントにViewが反応する事は問題ありません。何のために宣言的に構築しているか考えましょう。また、アンロードされたオブジェクトはイベントを受信して何かしようにも、結局ユーザーの目に見えるアクションを起こせない事もご考慮を。え?っと思った方はシナリオを具体的に考えてみてください。

あるとすれば、ViewModelからイベント(A)→View受信(B)→ViewModelに何かを反映(C)。このシナリオだけです。これよく考えて見てください。途中でユーザー操作を介さずにViewModelから発生したイベントがまたViewModelに作用を及ぼすならば、それは最初からViewModelに何かを反映(C)→ViewModelからイベント(A)→View受信(B)で済みますよね。

WeakEventの主な用途

主にコントロールが独自のイベントを受信する場合です。親が何になるかわからないし、親が解放すべきタイミング全然教えてくれないし、まさにWeakEventでしか対応できないんですよ。

また、MVVMインフラのMessenger機構でもよくつかわれます。たとえばMVVM LightのMessengerはWeakEventを使用しています。PrismのEventAggregatorもです。もちろんLivetのInteractionRequest/ViewModelHelperでも。

WeakEventの仕組み

WeakEventの根幹を支えるのはWeakReferenceです。まずWeakReferenceを抑えましょう。

WeakReferenceクラス

WeakReferenceクラス

MSDNライブラリ

http://msdn.microsoft.com/ja-jp/library/system.weakreference(v=vs.80).aspx

WeakReferenceを使用すれば、オブジェクトをCLR的には参照していないことにして扱う事が可能です。WeakReferenceを使用されたオブジェクトはどこからも参照されていないことになっているので、つまりはいつでもGCの対象となりうるという事です。WeakReferenceは内部的には対象のオブジェクトをCLRの参照ではなく、ポインタで持っています。ポインタでアドレスを持つだけではGCによるメモリコンパクションによってアドレス移動が発生する可能性があるので、WeakReferenceではそれに配慮する仕組みを持っています。

ではWeakReferenceを使ってどうやってWeakEventを実現するのか見てみましょう。

代表的なWeakEventパターン

いくつか代表的なWeakEventパターンを紹介します。難しいと思うので理解したい方は気合入れて。(理解しなくても使用に問題はありません)

 

ハンドラを直接WeakReferenceでイベント発行側が所有するパターン(発行側WeakEventパターン)

f:id:m_onoue:20141225141235p:plain

イベントハンドラを直接WeakReferenceのdelegateとして持てば、それはまぁメモリリークは発生しえませんよね。CLR的にはイベント発行元がイベント受信側を参照していないことになるわけですから。

メリットとしては、仕組みがわかりやすい事と、Staticイベントに対してWeakEventを適用するにはこれ以外方法がない事、イベント受信側は何も考慮する事がないなどが挙げられます。デメリットとしては、イベント発行側に特別な実装が求められる事。つまりイベント宣言で通常書くことがないadd/removeブロックを記述し、その中で渡されたハンドラをWeakReferenceで囲む作業が必要になります。

WPFCommandManager.RequerySuggestedメソッド(static)などはこの形のWeakEventパターンを提供しています。

 

代理のstaticハンドラがイベントを受信し、WeakReferenceで所有する各オブジェクトに配信するパターン(受信側WeakEventパターン) with Dictionary

f:id:m_onoue:20141225141245p:plain

一度イベント発行元の型に対応したstaticハンドラを用意してそこでイベントを受けます。受けたstaticハンドラは、本来イベントを受信するべきオブジェクトが持っているリスナにイベントを転送します。リスナはイベントを受信する代理のオブジェクトです。受信側クラスの本来のイベントハンドラへの参照を持ちます。staticハンドラ側はリスナの参照をWeakReferenceとして持つことでイベントとイベント受信側の参照関係を断ち切っています。

この場合、本来のイベント受信側はリスナを強い参照(通常の参照。リストにでも放り込んでおく)で保持しておく必要があります。そうしないとリスナはどこからもCLR的にはどこからも参照されていないことになってしまってGCに意図せず回収されてしまいますからね。

ハンドラを明示的に解除したい場合は、本来の受信側クラスのハンドラとリスナの紐づけを削除してやれば良いのです。リスナがIDispoable実装していると便利でしょう。

staticハンドラ側はイベントのsenderを見て、それに対応するリスナを探し出す必要があります。そのためには通常Dictionaryを使用して紐付けを行います。ハンドラが明示的に全部削除されるなどして、不要になったDictionaryエントリが生まれますのでタイマーなどで定期的にクリーンアップします。

.NET4のWeakEventManagerとIWeakEventLisetenerはこの形でWeakEventを提供します。

メリットとしては、イベント発行側は通常のイベント実装で構わない事、.NET4では標準でこのための仕組みが提供されていること、すべての方法の中で最も柔軟な制御が可能な事。デメリットとしては、イベント受信側に特別な実装が必要な事、WeakEventManagerとIWeakEventListernerを使用した場合はイベントハンドラの型ごとに対応したクラスの実装を用意する必要がある事、そして何よりもSilverlightとWindowsPhone7ではこれらWeakEventクラス群が用意されていないため、独自に実装しようとしてもその実装は非常に複雑である事があげられます。

 

代理のstaticハンドラがイベントを受信し、WeakReferenceで所有する各オブジェクトに配信するパターン(受信側WeakEventパターン) with Action

f:id:m_onoue:20141225141255p:plain

上の辞書を利用するパターンから、受信側クラスのライフサイクルとイベントリスナのライフサイクルが同じである事を前提にすることで簡略にした実装です。Dicitionaryは使用しません。代わりにstaticハンドラを拡張して、リスナ情報をイベント受信時にWeakReferenceで渡します。Dictionaryを使わず管理を単純化するのが主な目的です。また、型ごとにリスナとマネージャのクラスを定義する必要がありません。シナリオが限定されているといっても、XAML系テクノロジで想定される使い方はすべて満たせています。一つの汎用リスナクラスだけで使用できますが、デメリットとして書き方が非常にわかりにくい・・・?事が挙げられます。

僕はこのパターンをWPF/Sllverlight/WindowsPhoneの実装でソースコード共通で使用しています。つまり3プラットフォーム共通WeakEvent機構です。今回はこの実装をご紹介します。試行錯誤してなんとかこの考え方まではたどり着いたものの、要件を満たすコードがかけずしばらく放置していました。しかししばらく経ってReactive ExtensionsのObservable.FromEventのシグネクチャを見たときピンと来たんです。

試行錯誤の末にたどりついたこのパターン、なぜイベント発行元がリスナの情報をイベント通知と一所に渡せるのか?

実装を見て確認してみてください。

 

汎用WeakEvent機構(WPF/Silverlight/WindowsPhone7共通)

本来はIDisposableで実装しますが、わかりやすくするためにあえてそれを省いたコードを載せます。IDisposableで実装した正式版は後で載せます。本コードはWPF/Silverlight/WindowsPhone7のどのプラットフォームでも、一行の変更もなく動作します。

汎用WeakEvent機構のコード

このコードをIdeoneで見る場合はこちら(IDisposableな完全版)

f:id:m_onoue:20141225141308p:plain

このコードをIdeoneで見る場合はこちら(IDisposableな完全版)

使い方

使い方は以下の通りです。

このコードをIdeoneで見る場合はこちら

f:id:m_onoue:20141225141334p:plain

f:id:m_onoue:20141225141347p:plain

このコードをIdeoneで見る場合はこちら

まとめ

長くなりました。何回かに分けて書くべきだったかもしれませぬ。現在AM4・・・・いや、11日28時です。

まぁ中身を理解するのは難しいかもしれないので、実際にコードいぢくりまわして試してみてください。使えればいいんですよ!使えれば! ← 投げやり