2011/03/02 コメントの指摘を受けて修正。 間違ってた部分。 WeakPropertyChangedViewModelBaseクラスの以下の処理。 PropertyChangedEventManager.AddListener( this.Model, new PropertyChangedWeakEventListener( base.RaisePropertyChanged), string.Empty); 正しいものに差し替えました。taguoさん指摘ありがとうございました。
今、UxeenというWPF製でMVVM適用して頑張って作ってるTwetterクライアントのプロジェクトのリーダーのid:anis774さんにプロジェクトに入れてもらってMVVM関連のところを見たり、気になるところに手を入れたりしてる今日この頃です。
そこで、こんな感じのプログラムを書いていたのですよ。
namespace WpfApplication1 { using System.ComponentModel; // INotifyPropertyChangedの実装クラス class NotificationObject : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { var h = this.PropertyChanged; if (h != null) { h(this, new PropertyChangedEventArgs(propertyName)); } } } // Modelクラス class Person : NotificationObject { private string name; public string Name { get { return this.name; } set { this.name = value; base.RaisePropertyChanged("Name"); } } } // PersonのViewModel class PersonViewModel : NotificationObject { private Person model; public PersonViewModel(Person model) { this.model = model; this.model.PropertyChanged += ModelPropertyChanged; } public string Name { get { return this.model.Name; } set { this.model.Name = value; } } private void ModelPropertyChanged(object sender, PropertyChangedEventArgs e) { // ModelとViewModelのプロパティ名一緒だからそのまま使おう base.RaisePropertyChanged(e.PropertyName); } } }
このプログラムの特性としてModelは結構長期間にわたってメモリ上に保持されるけど、ViewModelはModelに比べて短命だというのがあります。この条件が成り立つとき以下のようなことが起きてViewModelのインスタンスがいつまでたってもGCに回収されないという動きをして、結果としてメモリリークが起きていました。
流れとしては以下のような感じです。
- ViewModelのコンストラクタでModelのイベントを購読する
- this.model.PropertyChanged += ModelPropertyChanged;
- ここでModelがViewModelに対する強い参照を保持する
- ViewModelの変数が誰からも参照されない状態になる
- しかしModelはViewModelをしっかり参照している
- ViewModelはいつまでたってもGCに回収されない
- 結果メモリリーク\(^o^)/
弱いイベントパターン
Weak Event Patternというのが、こういう問題を解決するソリューションとして提供されています。MSDNの以下のページです。
この弱いイベントパターンの実装例ってあまりなくて、日本語の情報では、以下のサイトを見つけました。
ということで、上記のサイトとMSDNとPrism内部でも使われてたので、それらを参考にして以下のように修正してみました。
// ModelのPropertyChangedを弱いイベントパターンでリッスンする class WeakPropertyChangedViewModelBase<TModel> : NotifyPropertyChangedBase where TModel : INotifyPropertyChanged { protected TModel Model { get; private set; } // イベントのリスナ保持のためのフィールド private IWeakEventListener propertyChangedListener; public WeakPropertyChangedViewModelBase(TModel model) { this.Model = model; // ここが超ポイント!! // IWeakEventListenerを作成してEventManagerに渡す。 // このときListenerのインスタンスは、フィールドなどで管理して // ViewModelがGCの対象になるまで破棄されないようにする this.propertyChangedListener = new PropertyChangedWeakEventListener( base.RaisePropertyChanged); PropertyChangedEventManager.AddListener( this.Model, this.propertyChangedListener, string.Empty); } // IWeakEventListenerの実装 class PropertyChangedWeakEventListener : IWeakEventListener { private Action<string> raisePropertyChangedAction; public PropertyChangedWeakEventListener(Action<string> raisePropertyChangedAction) { this.raisePropertyChangedAction = raisePropertyChangedAction; } public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { // PropertyChangedEventManagerじゃないと処理しない if (typeof(PropertyChangedEventManager) != managerType) { return false; } // PropertyChangedEventArgsじゃないと処理しない var evt = e as PropertyChangedEventArgs; if (evt == null) { return false; } // コンストラクタで渡されたコールバックを呼び出す this.raisePropertyChangedAction(evt.PropertyName); return true; } } }
このような、クラスを用意してViewModelクラスの実装を以下のような感じにします。
class PersonViewModel : WeakPropertyChangedViewModelBase<Person> { public PersonViewModel(Person model) : base(model) { } public string Name { get { return base.Model.Name; } set { base.Model.Name = value; } } }
こうすることで、ViewModelは必要なくなったら心置きなく成仏できるようになりました。めでたしめでたし。ってかハマルねこれは。