かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

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使うのがよさげ。