かずきのBlog@hatena

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

INotifyPropertyChangedのイベントを処理したい

追記:IDisposableのAddToという拡張メソッドはReactivePropertyでのメソッドでしたorz

みたいな記事をりんちゃさんに拾われてた。

今だとどうするかな~というと制約がなければReactive Extensionsかなぁ。例えば、こんなINotifyPropertyChangedの実装をしたクラスがあるとして

public class Person : INotifyPropertyChanged
{

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        var h = this.PropertyChanged;
        if (h != null)
        {
            h(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName]string propertyName = null)
    {
        if (object.Equals(field, value))
        {
            return false;
        }

        field = value;
        this.OnPropertyChanged(propertyName);
        return true;
    }

    private string name;

    public string Name
    {
        get { return this.name; }
        set { this.SetProperty(ref this.name, value); }
    }

    private int age;

    public int Age
    {
        get { return this.age; }
        set { this.SetProperty(ref this.age, value); }
    }

}

Rx-MainをNuGetで追加してPropertyChangedイベントをIOに変換するものを作る。

static class NotifyPropertyChangedExtensions
{
    public static IObservable<PropertyChangedEventArgs> PropertyChangedAsObservable(this INotifyPropertyChanged self)
    {
        return Observable.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>(
            h => (s, e) => h(e),
            h => self.PropertyChanged += h,
            h => self.PropertyChanged -= h);
    }
}

こうすると、Whereでプロパティ名を絞ってからSubscribeしてるので、そこで好きなようにやるかんじ。

var p = new Person();
var propertyChanged = p.PropertyChangedAsObservable();

var token = new CompositeDisposable();
token.Add(propertyChanged.Where(e => e.PropertyName == "Name")
    .Select(_ => p.Name)
    .Subscribe(name => Console.WriteLine(name)));
token.Add(propertyChanged.Where(e => e.PropertyName == "Age")
    .Select(_ => p.Age)
    .Subscribe(age => Console.WriteLine(age)));

// プロパティを変更したら値が出力される
p.Name = "tanaka";
p.Age = 100;

// 購読解除
token.Dispose();

// イベントの処理やめたので何もおきない
p.Name = "kimura";
p.Age = 120;

もうちょっとプロパティの値の変更に特化して使いたいとかいうなら、ここまでラップしたメソッドを定義してもいいかも?

static class NotifyPropertyChangedExtensions
{
    public static IObservable<PropertyChangedEventArgs> PropertyChangedAsObservable(this INotifyPropertyChanged self)
    {
        return Observable.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>(
            h => (s, e) => h(e),
            h => self.PropertyChanged += h,
            h => self.PropertyChanged -= h);
    }

    public static IObservable<T> PropertyChangedAsObservable<T, TProp>(this T self, Expression<Func<T, TProp>> propertySelector)
        where T : INotifyPropertyChanged
    {
        // プロパティ名を取り出して
        var name = ((MemberExpression)propertySelector.Body).Member.Name;
        return self.PropertyChangedAsObservable()
            // プロパティ名でフィルタリングして
            .Where(e => e.PropertyName == name)
            // 自分自身を返す感じ
            .Select(_ => self);
    }
}

こうしとくと、こういう感じになる。

var p = new Person();

var token = new CompositeDisposable();
token.Add(p.PropertyChangedAsObservable(o => o.Name)
    .Subscribe(person => Console.WriteLine(person.Name)));
token.Add(p.PropertyChangedAsObservable(o => o.Age)
    .Subscribe(person => Console.WriteLine(person.Age)));

p.Name = "tanaka";
p.Age = 100;

token.Dispose();

p.Name = "kimura";
p.Age = 120;

とりあえず

ReactivePropertyを入れておけば、拡張メソッドが定義されてたりします。PropertyChangdAsObservableメソッドの他にObservePropertyメソッドというのもあって以下のような感じで使えます。

ついでReactivePropertyには、IDisposableにAddToという拡張メソッドも定義されてるのでSubscribeした結果をCompositeDisposableに追加する部分のコードもすっきりします。のいえさん素敵。

var p = new Person();

var token = new CompositeDisposable();
p.ObserveProperty(o => o.Name).Subscribe(name => Console.WriteLine(name)).AddTo(token);
p.ObserveProperty(o => o.Age).Subscribe(age => Console.WriteLine(age)).AddTo(token);

p.Name = "tanaka";
p.Age = 100;

token.Dispose();

p.Name = "kimura";
p.Age = 120;

素敵なかんじ。ということで、ObserveProperty使うのがよさげ。