SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

WPFでもBindableBaseを使ってINotifyPropertyChangedを実装する

Win8のストアアプリのテンプレートでは、BindableBaseというクラスが作られています。そして、データバインディングのソースとして使うようなクラスはこのクラスから派生するようにデザインされています。
で、このBindableBaseでは、OnPropertyChangedを呼び出す際に、メンバ名を文字列で渡したり、リフレクションを使うことなく実現しています。
コードを見てみると、.net4.5の新機能のCallerMenberName属性を使って、呼び出し元のメンバ名を解決しています。


これはWPFでも使える!!と思い、試してみました。
usingや一部の属性の部分を書き換えるだけで、WPFでも使えました。素晴らしい!!

BindableBaseクラス

ストアアプリ用のBindableBaseクラスを、以下のように書き換えました。
書き換えた点は2点
・「using Windows.UI.Xaml.Data;」の部分を削除
・BindableBaseクラスのWebHostHidden属性を削除

BindableBase.cs
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WpfApplication1
{
    /// <summary>
    /// モデルを簡略化するための <see cref="INotifyPropertyChanged"/> の実装。
    /// </summary>
    public abstract class BindableBase : INotifyPropertyChanged
    {
        /// <summary>
        /// プロパティの変更を通知するためのマルチキャスト イベント。
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// プロパティが既に目的の値と一致しているかどうかを確認します。必要な場合のみ、
        /// プロパティを設定し、リスナーに通知します。
        /// </summary>
        /// <typeparam name="T">プロパティの型。</typeparam>
        /// <param name="storage">get アクセス操作子と set アクセス操作子両方を使用したプロパティへの参照。</param>
        /// <param name="value">プロパティに必要な値。</param>
        /// <param name="propertyName">リスナーに通知するために使用するプロパティの名前。
        /// この値は省略可能で、
        /// CallerMemberName をサポートするコンパイラから呼び出す場合に自動的に指定できます。</param>
        /// <returns>値が変更された場合は true、既存の値が目的の値に一致した場合は
        /// false です。</returns>
        protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
        {
            if (object.Equals(storage, value)) return false;

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

        /// <summary>
        /// プロパティ値が変更されたことをリスナーに通知します。
        /// </summary>
        /// <param name="propertyName">リスナーに通知するために使用するプロパティの名前。
        /// この値は省略可能で、
        /// <see cref="CallerMemberNameAttribute"/> をサポートするコンパイラから呼び出す場合に自動的に指定できます。</param>
        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var eventHandler = this.PropertyChanged;
            if (eventHandler != null)
            {
                eventHandler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

使い方

こんな風に、this.SetProperty(ref this.変数名, value);と書くだけ!!

Person.cs
    public class Person : BindableBase
    {
        private string name;
        public string Name
        {
            get { return name; }
            set { this.SetProperty(ref this.name, value); }
        }

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

速度の計測

一応、実行速度も計測してみました。
以下の記事を参考に速度を測っています。
http://d.hatena.ne.jp/okazuki/20091227/1261930083

実験用コード
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                var emp = new NormalEmp();
                emp.PropertyChanged += (sender, e) => { };

                var watch = Stopwatch.StartNew();
                foreach (var i in Enumerable.Range(0, 100000))
                {
                    emp.Name = "test" + i;
                }
                Console.WriteLine("通常の実装: {0}ms", watch.ElapsedMilliseconds);
            }

            {
                var emp = new BindableBaseEmp();
                emp.PropertyChanged += (sender, e) => { };

                var watch = Stopwatch.StartNew();
                foreach (var i in Enumerable.Range(0, 100000))
                {
                    emp.Name = "test" + i;
                }
                Console.WriteLine("BindableBase版: {0}ms", watch.ElapsedMilliseconds);
            }
        }
    }

    // BindableBaseを使った実装
    public class BindableBaseEmp : BindableBase
    {
        private string name;
        public string Name
        {
            get { return name; }
            set { this.SetProperty(ref this.name, value); }
        }
    }

    // 普通の実装
    public class NormalEmp : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged メンバ
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string name)
        {
            if (PropertyChanged == null) return;
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
        #endregion
        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged("Name");
            }
        }
    }
}

Releaseビルドで、プロパティ変更を100000回繰り返して計測してます。

結果
通常の実装版 18ms
BindableBase版 18ms

当たり前かもしれませんが、リフレクションをや拡張メソッドなどを使っていないので非常に高速です。というか、文字列を渡して書いた場合とほぼ一緒。


これは便利かも!!