0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-11-25

概要

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を使用しています。

コード

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

    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をプロパティとして保持していて自身のメソッドを接続しているとします。ViewModelBaseWithModelOnPropertyChanged()の引数である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の連動処理はメモリリークにもつながる場合もあり、知識や実装も注意をもって行う必要があると改めて感じました。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?