必要性
たぶん必要になるのはアーキテクチャ周りぐらいだと思う。
ViewModelがソースのイベントを購読する場合、ソースがViewModelの参照を持つ。寿命の長いソースが寿命の短いViewModelの参照を捕捉することになるので、いつまで経ってもViewModelはGCに回収されない。これが原因でメモリーリークする。
ViewModelがいらなくなった時点でイベントの購読を解除できるんなら問題はないんだけど必ずその処理を実行できる状況とは限らない。たとえば、ViewModelのメンバーとして別のViewModelを公開する場合。破棄するタイミングだとか、管理するViewModelの数によってはやってらんない。
そんなとき頼りになるのがWeakEventパターンというやつ。
仕組
通知の発行前に弱参照を一枚かませる。これだけ。
1 2 3 4 5 6 7 8 9 10 11 12 |
static THandler CreateHandler(WeakReference<Hoge> wr, Func<EventHandler<TEventArgs>, THandler> conv, params object[] その他必要な引数) { var received = new EventHandler<TEventArgs>((o, e) => { Hoge rf; if (wr.TryGetTarget(out rf)) { //ここで発行 } }); return conv(received); } |
標準ライブラリとして用意されているのはWeakEventManager
クラスとIWeakEventListner
インターフェイス。これらを使うのが割とスタンダードなんだと思う。それ以外にもブログや、Livet、Prismなどのライブラリで、それぞれいろんなアプローチによって実装されている。
理想
現状での要望としては
・ ソース側には手を加えない
・ Disposeメソッドによる解除も可能
・ ハンドラーに対してジェネリック対応(型推論も可能な限り使いたい)
実装案
今回はManagerクラスを使わずに実装する。
まずはベース兼、staticメソッドによって派生したリスナーを生成するクラス。
イベント引数は型推論がきかないので明記するしかない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class WeakEvent<TEventArgs> : IDisposable where TEventArgs : EventArgs { public static IDisposable Register<TSource, TListener, THandler>(TSource sender, TListener listener, Func<EventHandler<TEventArgs>, THandler> conv, Action<TSource, THandler> add, Action<TSource, THandler> remove, Action<TListener, object, TEventArgs> handler) where TListener : class { return new WeakEventListener<TSource, TListener, THandler, TEventArgs>(sender, listener, conv, add, remove, handler); } public static IDisposable Register<TSource, TListener>(TSource sender, TListener listener, Action<TSource, EventHandler<TEventArgs>> add, Action<TSource, EventHandler<TEventArgs>> remove, Action<TListener, object, TEventArgs> handler) where TListener : class { return new WeakEventListener<TSource, TListener, EventHandler<TEventArgs>, TEventArgs>(sender, listener, x => x, add, remove, handler); } public static IDisposable CreateListener<THandler>(Func<EventHandler<TEventArgs>, THandler> conv, Action<THandler> add, Action<THandler> remove, EventHandler<TEventArgs> handler) { return new WeakEventListener<THandler, TEventArgs>(conv, add, remove, handler); } public static IDisposable CreateListener(Action<EventHandler<TEventArgs>> add, Action<EventHandler<TEventArgs>> remove, EventHandler<TEventArgs> handler) { return new WeakEventListener<EventHandler<TEventArgs>, TEventArgs>(x => x, add, remove, handler); } internal WeakEvent() { } //以下略 } |
これを継承するクラスを2パターン。
- パターンA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class WeakEventListener<TSource, TListener, THandler, TEventArgs> : WeakEvent<TEventArgs> where TListener : class where TEventArgs : EventArgs { readonly WeakReference<TListener> listenerRef; Action _remove; static THandler CreateHandler(WeakEventListener<TSource, TListener, THandler, TEventArgs> self, TListener listener, Func<EventHandler<TEventArgs>, THandler> conv, Action<TListener, object, TEventArgs> handler) { EventHandler<TEventArgs> eh = (o, e) => { TListener tgt; if (self.listenerRef.TryGetTarget(out tgt)) { handler(tgt, o, e); } else { self.Dispose(); } }; THandler hnd = conv(eh); return hnd; } static void delegateCheck(params Tuple<Delegate, string>[] sig) { foreach (var t in sig) { if (t.Item1 == null) throw new ArgumentNullException(t.Item2); if (!t.Item1.Method.IsStatic) throw new ArgumentException("指定された式は式外の変数を参照しています。", t.Item2); } } protected internal WeakEventListener(TSource sender, TListener listener, Func<EventHandler<TEventArgs>, THandler> conv, Action<TSource, THandler> add, Action<TSource, THandler> remove, Action<TListener, object, TEventArgs> handler) { if (sender == null) throw new ArgumentNullException("sender"); if (listener == null) throw new ArgumentNullException("listener"); delegateCheck( new Tuple<Delegate, string>(conv, "conv"), new Tuple<Delegate, string>(add, "add"), new Tuple<Delegate, string>(remove, "remove"), new Tuple<Delegate, string>(handler, "handler")); listenerRef = new WeakReference<TListener>(listener); THandler eh = CreateHandler(this, listener, conv, handler); _remove = () => remove(sender, eh); add(sender, eh); } //以下略 } |
- パターンB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class WeakEventListener<THandler, TEventArgs> : WeakEvent<TEventArgs> where TEventArgs : EventArgs { EventHandler<TEventArgs> _excuteHandler; THandler _aduptHandler; Action<THandler> _remove; private static THandler CreateHandler(WeakReference<WeakEventListener<THandler, TEventArgs>> listenerWeakRef, Func<EventHandler<TEventArgs>, THandler> conv) { EventHandler<TEventArgs> eh = (s, e) => { WeakEventListener<THandler,TEventArgs> listener; if (listenerWeakRef.TryGetTarget(out listener)) { var handler = listener._excuteHandler; if (handler != null) handler(s, e); } }; return conv(eh); } protected internal WeakEventListener(Func<EventHandler<TEventArgs>, THandler> conv, Action<THandler> add, Action<THandler> remove, EventHandler<TEventArgs> handler) { _excuteHandler = handler; _remove = remove; _aduptHandler = CreateHandler(new WeakReference<WeakEventListener<THandler, TEventArgs>>(this), conv); add(_aduptHandler); } //以下略 } |
で、こう使う。
1 2 3 4 5 6 7 8 9 10 11 |
listenerA = WeakEvent<PropertyChangedEventArgs>.Register(source, this, h => new PropertyChangedEventHandler(h), (s, h) => s.PropertyChanged += h, (s, h) => s.PropertyChanged -= h, (tgt, s, e) => tgt.ChangedAction(s, e)); listenerB = WeakEvent<NotifyCollectionChangedEventArgs>.CreateListener( h => new NotifyCollectionChangedEventHandler(h), h => src.CollectionChanged += h, h => src.CollectionChanged -= h, CollectionChangedAction); |
考察
パターンAでは、ソースはリスナーを強参照し、リスナーはViewModelを弱参照する。
ソースからの通知を受信したときViewModelの存在をチェックして、あれば発行、なければリスナー自身に対する強参照をソースから外す、という流れ。だからViewModelがリスナーを生成するときに、引数のラムダ式にフィールド変数やローカル変数を使えない。これをやってしまうと、リスナーがViewModelを強参照することになってViewModelは回収されなくなる。
利点は、ViewModelは生成されるリスナーを保持しとかなくても構わないこと。解除のタイミングを制御したいときだけ使えばよろし。
これに対してパターンBは、ソースはリスナーを弱参照し、リスナーはViewModelを強参照する。
リスナーを強参照しとかないとGCにあっけなく回収されてしまうのでリスナーの保持は必須。その代り、引数のラムダ式にパターンAのような制限はない。数も少ない。
パターンAかB、どっちかがあれば事足りる。どっちがいいかというと好みとか、その場の状況とか、正直なところどっちでもいい。
ただ実際に使ってみて、面倒だなと思うところもあった。毎度毎度決まりきったラムダ式を書かにゃならんことと、各イベントに対してそれぞれリスナーを作成せにゃならんこと。Disposableなコレクションでリスナーを一括管理すればいくらか手間を省けるけど、使うとき自前で用意せにゃならんこと。
この辺りを考慮して、気が向いた遠い未来、また書いてみるかも。