自前でC#の変更通知クラスを作って勉強になったことまとめ(前編)


車輪の再発明
...ですが, ブラックボックスから脱出する良いきっかけになったので, 備忘録として記録しておきます.
MVVMパターンで使われている内部実装に興味がある方にとって, 理解の一助になれば幸いです.

きっかけ

Livetに乗っかっておまじない的に使ってきたMVVMの中で何が起こってるのかをちゃんと理解したかったので, 一度自前で実装してみることに.
Livetが使えない環境でも同じようなコードを組めると色々嬉しい.

ソースはこちら

NotificationObjectとPropertyChangedEventListener

NotificationObject

プロパティ変更通知を発行します.

C#にはすでに「PropertyChangedEventHandler」型のイベントが標準で用意されています.
このイベントを「PropertyChanged」というプロパティでもつように定めたのが「INotifyPropertyChanged」インタフェースです(using System.ComponentModelが必要)

NotificationObject.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class NotificationObject : INotifyPropertyChanged
{
    // INotifyPropertyChangedは必ずこのイベントをもっていなければならない
    public event PropertyChangedEventHandler PropertyChanged;

    // 自作したプロパティのセッター
    // 必ずプロパティ変更通知を飛ばす
    public void SetProperty<T>(ref T target, T value, [CallerMemberName] string caller ="")
    {
        target = value;

        if (PropertyChanged == null)
            return;
        PropertyChangedEventArgs arg = new PropertyChangedEventArgs(caller);
        PropertyChanged.Invoke(this, arg);
    }
}

LivetのNotificationObjectは「RaisePropertyChanged」メソッドを呼び忘れるとプロパティ変更イベントが飛んできません.
それは嫌だったので, SetValueというメソッドを用意しました.
(これはPrismのパクり)

このメソッド経由でプロパティを設定すると, 自動的にプロパティ変更イベントがInvokeされるようにしています.
また, [CallerMemberName]指定することで, 呼び出し「元」のプロパティ名が勝手に「caller」にセットされます(using System.Runtime.CompilerServices;が必要)

PropertyChangedEventListener

プロパティの変更通知を行う側はできたので, 今度はプロパティ変更通知を受け取るためのクラスを作っていきます.

PropertyChagnedEventListener.cs
public class PropertyChangedEventListener : IDisposable
{
    INotifyPropertyChanged Source;
    PropertyChangedEventHandler Handler;

    public PropertyChangedEventListener(INotifyPropertyChanged source, PropertyChangedEventHandler handler)
    {
        Source = source;
        Handler = handler;
        Source.PropertyChanged += Handler;
    }

    public void Dispose()
    {
        if (Source != null && Handler != null)
            Source.PropertyChanged -= Handler;
    }
}

コンストラクタでINotifyPropertyChangedを参照するようにして, プロパティ変更通知のイベント登録(handlerの登録)も行うようにしました.
NotificationObject側でプロパティの変更があれば, 登録しておいたHandlerがInvokeされます.
「もうプロパティの変更通知は受け取らなくて良いよ」というタイミングになったら, Disposeを呼び出してプロパティ変更イベントの登録解除をおこないます.

「プレイヤー(プロパティ変更通知送信側)」と「HPゲージ(プロパティ変更通知受信側)」

Player.cs

public class Player : NotificationObject
{
    public int HP
    {
        get
        {
            return _HP;
        }
        set
        {
            SetProperty(ref _HP, value);
        }
    }
    int _HP;
}
HPGauge.cs

public class HPGauge : IDisposable
{
    IDisposable listener;

    public HPGauge(Player pl)
    {
        listener = new PropertyChangedEventListener(pl,
          (sender, e)=>{
            if (e.PropertyName == "HP")
            {
               // UIに値を書き込む
               // WPFならxaml(View)にプロパティを書き込むことになる
            }
        });
    }

    public void Dispose()
    {
        listener?.Dispose();
    }
}
Program.cs(具体例)
...
var player = new Player();
var gauge = new HPGauge(player);

// HPに値が入ったら, 自動的にgauge側で登録した処理が行われる
player.HP = 200;
...

これでプレイヤーのHP変更を監視するUIプログラムを作ることができました.
MVVMのViewModelに相当しますね.

後編に続く

単一のObjectについてはプロパティ変更を送受信する仕組みが整いました.
Collectionに関しても自作してみたので, 後編に続きます.
自前でC#の変更通知クラスを作って勉強になったことまとめ(後編)

今回はここまで...