【WPF】ModelのPropertyChangedをViewModelのPropertyChangedとしてそのまま発行するViewModelの基底クラスを作成してみた

# 概要
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が解放されます。

:::note
Prismを使用しています。
:::

# コード
```C#:ViewModelBaseWithModel.cs
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
```C#
    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
```C#
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を返すとメソッドが走らない形のようにもできます。

```C#
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)
    {
        // 処理
    }
}

```
:::note
上記のイベント接続はViewModelとViewModelの接続のため、+=で接続しています。ViewModelは通常、Modelより早くガベージコレクションで破棄される場合が多く、Modelのようにずっと残るようなパターンも少ないです (と思います)。
例えばViewが消去されたときに同時にViewModelもガベージコレクションで破棄されます。なので、+=で接続しています。
※メモリリークには繋がらないと思っていますが、間違っていたらご指摘ください。
:::

# おわりに
ViewModelとModelの連動処理はメモリリークにもつながる場合もあり、知識や実装も注意をもって行う必要があると改めて感じました。