かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

INotifyPropertyChangedのいけてる実装

もう何度目かになりますが、今までで一番イケテルと思った実装を見つけたので紹介します。ただ、元ネタのHPを忘れてしまった・・・orz
やり方としては簡単です。これまでのINotifyPropertyChangedを実装したベースクラスを継承するという方法とも違うし、プロパティ名を文字列で渡すなんて、何か嫌だという問題もばっちり解決してるというナイスな方法です。

どうするかというと、PropertyChangedEventHandlerを拡張するという方法です。結構Expressionクラスとか駆使してるので、性能的には数が増えるとどうだろうという心配はあります・・・。
ということで、百聞は一見にしかずということなので見てみましょう。PropertyChangedEventHandlerを拡張するPropertyChangedEventHandlerExtensionsクラスを作成します。

public static class PropertyChangedEventHandlerExtensions
{
    /// <summary>
    /// イベントを発行する
    /// </summary>
    /// <typeparam name="TResult">プロパティの型</typeparam>
    /// <param name="_this">イベントハンドラ</param>
    /// <param name="propertyName">プロパティ名を表すExpression。() => Nameのように指定する。</param>
    public static void Raise<TResult>(this PropertyChangedEventHandler _this,
        Expression<Func<TResult>> propertyName)
    {
        // ハンドラに何も登録されていない場合は何もしない
        if (_this == null) return;

        // ラムダ式のBodyを取得する。MemberExpressionじゃなかったら駄目
        var memberEx = propertyName.Body as MemberExpression;
        if (memberEx == null) throw new ArgumentException();

        // () => NameのNameの部分の左側に暗黙的に存在しているオブジェクトを取得する式をゲット
        var senderExpression = memberEx.Expression as ConstantExpression;
        // ConstraintExpressionじゃないと駄目
        if (senderExpression == null) throw new ArgumentException();

        // 式を評価してsender用のインスタンスを得る
        var sender = Expression.Lambda(senderExpression).Compile().DynamicInvoke();

        // 下準備が出来たので、イベント発行!!
        _this(sender, new PropertyChangedEventArgs(memberEx.Member.Name));
    }
}

こいつを使うとクラス側はこうなります。

// INotifyPropertyChangedを実装するクラス
public class Emp : INotifyPropertyChanged
{
    // イベントだけ実装しておく。OnPropertyChangedは使わない
    public event PropertyChangedEventHandler PropertyChanged;

    private int _age;
    public int Age
    {
        get { return _age; }
        set
        {
            if (Equals(_age, value)) return;

            _age = value;
            // イベント発行!素敵!
            PropertyChanged.Raise(() => Age);
        }
    }
}

個人的に、この発想はなかったわと感じました。これに、この間考えたPropertyChangedイベントハンドラの実装方法と組み合わせると、コードの字面上は、とってもいいんじゃないんだろうかと思います。

イベントハンドラを実装する側のコード

var emp = new Emp();
emp.AddPropertyChanged(o => o.Age, (employee) =>
{
    Console.WriteLine("Age: {0}", employee.Age);
});

emp.Age = 100;
emp.Age = 10;

うん。ナイス。