概要
WPFのMVVMパターンで、ModelのPropertyChanged
をViewModelのPropertyChanged
としてそのまま発行するViewModelの基底クラスを作成してみました。
説明
作成したのは、ModelのPropertyChanged
イベントをViewModelのPropertyChanged
イベントとして発行できる、ViewModelの基底クラスです。
ModelのPropertyChanged
イベントをViewModelのPropertyChanged
イベントとして発行したい場合、通常 (?) はModelにmodel.PropertyChanged += OnPropertyChanged;
のようにして接続すると思いますが、このように接続するとViewModelの開放時にmodel.PropertyChanged -= OnPropertyChanged;
として接続を解除しなければならないと思います。これを怠ると、ModelがViewModelへの参照を保持したままになり、ずっとViewModelが解放されずにメモリリークが発生する可能性があります。
そのため、ViewModelから弱参照を使用してmodel.PropertyChanged
に接続するためのViewModelの基底クラスを作成しました。このクラスを使用すると、明示的に接続解除を行わなくてもガベージコレクションのタイミングで適切にViewModelが解放されます。
Prismを使用しています。
コード
using Prism.Mvvm;
using System;
using System.ComponentModel;
using System.Windows;
namespace Sample.ViewModels
{
public class ViewModelBaseWithModel<TModel> : BindableBase where TModel : INotifyPropertyChanged
{
protected TModel Model { get;}
private IWeakEventListener _weakEventListener;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="model"></param>
public ViewModelBaseWithModel(TModel model)
{
Model = model;
_weakEventListener = new WeakEventListener(propertyName =>
{
if (OnModelPropertyChanged(propertyName)) base.RaisePropertyChanged(propertyName);
});
PropertyChangedEventManager.AddListener(Model, _weakEventListener, string.Empty);
}
/// <summary>
/// モデルのPropertyChangedが発行されたときに呼ばれる
/// </summary>
/// <param name="propertyName"></param>
/// <returns></returns>
protected virtual bool OnModelPropertyChanged(string propertyName)
{
return true;
}
private class WeakEventListener : IWeakEventListener
{
private Action<string> _raisePropertyChangedAction;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="raisePropertyChangedAction"></param>
public WeakEventListener(Action<string> raisePropertyChangedAction) => _raisePropertyChangedAction = raisePropertyChangedAction;
public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (typeof(PropertyChangedEventManager) == managerType && e is PropertyChangedEventArgs pcEvt)
{
// PropertyChangedEventManagerかつPropertyChangedEventArgsのときのみPropertyChangedを中継する
_raisePropertyChangedAction(pcEvt.PropertyName);
return true;
}
return false;
}
}
}
}
PropertyChangedEventManager
クラスについて
PropertyChangedEventManager
クラスを使用することで、弱参照でイベント接続ができるようになります。
強参照と弱参照について
強参照は一般的に使用されているデフォルトの参照タイプで、変数やフィールドなどで使用されます。
弱参照は強参照よりも弱い参照方法でオブジェクトが強参照よりも早いタイミングで解放されるため、メモリリークの防止や何らかの制約でメモリの使用効率を意識しなくてはいけない場合等に有効です。
ただガベージコレクションの弱参照の改修タイミングは完全にコントロールできないため、設計や実装時の注意や、動作確認も必要になります。
また、PropertyChangedEventManager
クラスはソース (第一引数) とリスナー (第二引数) はどちらとも弱参照で持つので、WeakEventListener
のインスタンスをフィールドに入れておくことでViewModelより先にWeakEventListener
のインスタンスがガベージコレクションされてしまわないようにしています。
AddListener()
メソッドについて
AddListener()
で、変更通知を送るソース (第一引数) と通知を受け取るリスナー (第二引数) を関連付けます。
第一引数のプロパティが変更された際に、第二引数で渡したリスナーの中にあるReceiveWeakEvent()
メソッドが走ります。
ReceiveWeakEvent()
メソッドについて
上記記載の通り、ソースのプロパティが変更されたタイミングでこのメソッドが走ります。
ここでは、ReceiveWeakEvent()
の中の_raisePropertyChangedAction
で、ViewModelBaseWithModel
クラスのコンストラクタで記載された匿名メソッド部分if (OnModelPropertyChanged(propertyName)) base.RaisePropertyChanged(propertyName);
が走ります。
使用方法
Model
public class HogeModel : BindableBase
{
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="parent"></param>
public HogeModel()
{
}
/// <summary>
/// 自分のPropertyChangedが発生したときに呼ばれる
/// </summary>
/// <param name="args"></param>
protected override void OnPropertyChanged(PropertyChangedEventArgs args)
{
// ReceiveWeakEvent()が走る
base.OnPropertyChanged(args);
if (string.IsNullOrEmpty(args.PropertyName) || args.PropertyName == nameof(PropertyA))
{
// 処理
}
}
public string PropertyA { get => _propertyA; set => SetProperty(ref _propertyA, value); }
private string _propertyA;
}
ViewModel
public class HogeViewModel : ViewModelBaseWithModel<HogeModel>
{
/// <summary>
/// コンストラクタ
/// </summary>
public HogeViewModel(HogeModel model) : base(model)
{
}
protected override bool OnModelPropertyChanged(string propertyName)
{
return base.OnModelPropertyChanged(propertyName);
}
}
上記のようにViewModelBaseWithModel
クラスを基底クラスにしたViewModelクラスを作成すると、例えばModelのPropertyA
が変更されたときに、ViewModelのOnModelPropertyChanged()
にも通知されます。
また、下記のように別のViewModelクラスがあり、このクラスでHogeViewModel
をプロパティとして保持していて自身のメソッドを接続しているとします。ViewModelBaseWithModel
のOnPropertyChanged()
の引数であるpropertyName
で条件分岐等を行い、trueを返すとこの接続したメソッドが走り、falseを返すとメソッドが走らない形のようにもできます。
public class FugaViewModel : ViewModelBaseWithModel<FugaModel>
{
public HogeViewModel HogeViewModel { get; set; }
/// <summary>
/// コンストラクタ
/// </summary>
public FugaViewModel(FugaModel model) : base(model)
{
HogeViewModel = new HogeViewModel();
HogeViewModel.PropertyChanged += HogeViewModel_PropertyChanged;
}
protected override bool OnModelPropertyChanged(string propertyName)
{
return base.OnModelPropertyChanged(propertyName);
}
private void HogeViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// 処理
}
}
上記のイベント接続はViewModelとViewModelの接続のため、+=で接続しています。ViewModelは通常、Modelより早くガベージコレクションで破棄される場合が多く、Modelのようにずっと残るようなパターンも少ないです (と思います)。
例えばViewが消去されたときに同時にViewModelもガベージコレクションで破棄されます。なので、+=で接続しています。
※メモリリークには繋がらないと思っていますが、間違っていたらご指摘ください。
おわりに
ViewModelとModelの連動処理はメモリリークにもつながる場合もあり、知識や実装も注意をもって行う必要があると改めて感じました。