かずきのBlog@hatena

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

PropertyChangedイベントの処理方法

INotifyPropertyChangedインターフェースでお馴染みのPropertyChangedイベントですが、これのイベントハンドラのコードが気に入らない。というか書いてて、ちょっとなんだかなぁと思ってしまいます。

例えば、以下のようなNameプロパティとAgeプロパティを持ったINotifyPropertyChangedインターフェースを実装したPersonクラスがあったとします。

public class Person : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            if (Equals(_name, value)) return;

            _name = value;
            OnPropertyChanged("Name");
        }
    }

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

            _age = value;
            OnPropertyChanged("Age");
        }
    }

    #region INotifyPropertyChanged メンバ

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string name)
    {
        if (PropertyChanged == null) return;

        PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
    #endregion
}

なんの変哲もない、クラスです。そして、こいつのNameとAgeプロパティに変更があったらコンソールに変更があった旨と、変更後の値を出力するような処理を書かないといけないとしたら、以下のような感じになると思います。

class Program
{
    static void Main(string[] args)
    {
        var p = new Person();
        // プロパティに変更があった場合に呼び出されるイベントを登録
        p.PropertyChanged += NameChanged;
        p.PropertyChanged += AgeChanged;
        p.Name = "田中";
        p.Age = 100;
    }

    private static void NameChanged(object sender, PropertyChangedEventArgs e)
    {
        // 文字列でプロパティ名を判別
        if (e.PropertyName != "Name") return;

        // そしてキャスト
        var p = (Person)sender;

        // 各々の処理
        Console.WriteLine("名前が変更されました: " + p.Name);
    }
    private static void AgeChanged(object sender, PropertyChangedEventArgs e)
    {
        // 文字列でプロパティ名を判別
        if (e.PropertyName != "Age") return;

        // そしてキャスト
        var p = (Person)sender;

        // 各々の処理
        Console.WriteLine("年齢が変更されました: " + p.Age);
    }
}

今回は、プロパティごとにイベントハンドラをわけましたが1つにまとめて、switch文やif文でプロパティ名で条件分岐してもOKです。その場合は、下のようになります。

class Program
{
    static void Main(string[] args)
    {
        var p = new Person();
        // プロパティに変更があった場合に呼び出されるイベントを登録
        p.PropertyChanged += PersonPropertyChanged;
        p.Name = "田中";
        p.Age = 100;
    }
    private static void PersonPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        var p = (Person)sender;
        switch (e.PropertyName)
        {
            case "Name":
                Console.WriteLine("名前が変更されました: " + p.Name);
                break;
            case "Age":
                Console.WriteLine("年齢が変更されました: " + p.Age);
                break;
            default:
                // 処理無し
                break;
        }
    }
}

どちらにしても共通するのが、ハンドラ内で文字列でプロパティ名を判断しないといけないことと、変更があったオブジェクトにアクセスしようとすると、キャストしないといけないという手間があります。ちょっとした手間ですが、タイプミスが心配だとか、毎回書くのはめんどうだとか色々個人的にメンドクサイと思ってます。

ということで、前にやったタイプセーフなINotifyPropertyChangedの実装の方法を応用すると以下のように書けるな〜と思いつきました。

まず、INofityPropertyChangedインターフェースを実装しているクラスに対して以下のような拡張メソッドを定義します。

public static class NotifyPropertyChangedEx
{
    public static void AddPropertyChanged<TObj, TProp>(this TObj _this,
        Expression<Func<TObj, TProp>> propertyName, Action<TObj> handler)
        where TObj : INotifyPropertyChanged
    {
        // プロパティ名を取得して
        var name = ((MemberExpression)propertyName.Body).Member.Name;
        // 引数で指定されたプロパティ名と同じだったら、handlerを実行するように
        // PropertyChangedイベントに登録する
        _this.PropertyChanged += (sender, e) =>
        {
            if (e.PropertyName == name)
            {
                handler(_this);
            }
        };
    }
}

そして、これを使うようにProgramクラスの中を書き換えると…

class Program
{
    static void Main(string[] args)
    {
        var p = new Person();
        // プロパティに変更があった場合に呼び出されるイベントを登録
        p.AddPropertyChanged(o => o.Name, NameChanged);
        p.AddPropertyChanged(o => o.Age, AgeChanged);

        p.Name = "田中";
        p.Age = 100;
    }
    // 名前に変更があったときの処理
    private static void NameChanged(Person p)
    {
        Console.WriteLine("名前に変更がありました: " + p.Name);
    }
    // 年齢に変更があったときの処理
    private static void AgeChanged(Person p)
    {
        Console.WriteLine("年齢に変更がありました: " + p.Age);
    }
}

プロパティ名の指定部分が、ラムダ式ですっきり書けるようになるのと、各プロパティが変更されたときの処理の引数が、.NETのイベントのobject型と***EventArgs型というみょうちくりんなものではなく、Personクラスが最初から渡ってくるようになります。

とまぁ、思いついたのでBlogに書いてありますが、はたしてコードでPropertyChangedを自分で実装するということが、どれだけあるのか・・・?と問われると、そんなに無いのかなぁとも思ったり思わなかったりしてます。はい。