INotifyPropertyChangedの実装とプロパティ名 #1

WPFを使ったアプリの実装をすると多くの場合にINotifyPropertyChangedの実装が必要となります。INotifyPropertyChangedとそのときに必要となるプロパティ名の実装についての投稿です。

GUIの実現にWPFを使ってアプリケーションを実装すると、多くの場合、ViewModel層やModel層のプロパティの値の変化をView層に伝えるために、INotifyPropertyChangedの実装が必要となります。

プロパティの値の変更の検出

まず、説明のために、以下の二つを定義します

  • 他のモジュール・クラスからクラスのプロパティが参照されているクラス (プロパティ値を供給する側)
  • 上記クラスのプロパティを参照する別のモジュール・クラス (プロパティ値を参照する側)

ここで、前者の「プロパティ値を供給する側」として以下のclass DataAを想定します。

#nullable enable
namespace Test
{
    public class DataA
    {
        string _propertyB = "";
        public string PropertyB
        {
            get { return _propertyB; }
            set { _propertyB = value; }
        }
    }
}

DataAクラスのPropertyBプロパティに新しい値が設定されると、プロパティの値が変化します。このとき、「プロパティ値を参照する側」は、プロパティの値の変化に気づくことができなければ、新しい値で状態の更新ができません。たとば、「プロパティ値を参照する側」がWPFなどのGUIのモジュールであれば、GUI上に表示しているプロパティの値を更新することはできません。

では、どのようにすれば、「プロパティ値を参照する側」はプロパティ値の変化に気づくことができるでしょうか?

方法は二つしかありません。

  • 「プロパティ値を参照する側」でPropertyBの値を定期的に確認(ポーリング)する
  • 「プロパティ値を供給する側」から「プロパティ値を参照する側」に値が変化したことを教える

「プロパティ値を参照する側」でPropertyBの値を定期的に確認

前者の方法の場合、PropertyBの値を定期的に確認して、前回の値から変化がないかを確認します。参照する側では、いつ値が変化するかわからないので、値の変化の有無にかかわらず定期的に確認処理をします。

値が変化することが少なければ、定期的な確認処理のほとんどが無駄な処理となります。また、値の変化にすぐに反応するためには、定期的な確認処理を短い間隔で実行する必要があります。短い間隔で定期的な確認処理をすると、アプリのパフォーマンスに影響が出てしまいます。

この方法では、「プロパティ値を供給する側」は、特に何もする必要はありません。しかし、「プロパティ値を参照する側」のコードが複雑化したり、アプリのパフォーマンスに影響がでたります。

「プロパティ値を供給する側」から「プロパティ値を参照する側」に値が変化したことを教えてもらう

後者の場合は、「プロパティ値を参照する側」は、変化の通知を受け取るためのイベントハンドラーを「プロパティ値を供給する側」に登録します。プロパティの値が変化したら、「プロパティ値を供給する側」から登録したイベントハンドラーを通して変化の通知を受けます。

変化したタイミングで通知を受けることができるため、「プロパティ値を参照する側」は、定期的なプロパティ値の確認処理は必要としません。通知を受けたときに、新しい値を取得するだけでよくなります。ただし、「プロパティ値を供給する側」にプロパティ値の変化を通知する仕組みが必要となります。

この方法では、イベントハンドラーを登録することもあり、「プロパティ値を供給する側」と「プロパティ値を参照する側」で、双方向にオブジェクト参照を持つ形になります。必要がなくなった時は、適切な処理(必要に応じてイベントハンドラーの登録解除など)をしないと、不要になったインスタンスがガーベッジコレクションで回収されないことがあります。

WPFにおけるプロパティ値の変化検出の仕組み

WPFを使ったアプリでは、「プロパティ値を参照する側」がWPF(View側)となり、プロパティのバインディング(たとえば、<TextBlock text="{binding PropertyB}" DataContext={binding InstanceClassDataA}/>)で、「プロパティ値を供給する側」のプロパティを参照します。

WPFでは、ViewModel/Model層のプロパティ値の変化検出するために、主に後者の “「プロパティ値を供給する側」から「プロパティ値を参照する側」に値が変化したことを教えてもらう” の仕組みを使います。

その仕組みとして、WPFでは、INotifyPropertyChangedインターフェースを使います。プロパティの値を供給する側のクラスは、このインターフェースを実装し、プロパティの値が変更になった時に、変化があったことを通知します。

INotifyPropertyChangedインターフェース

INotifyPropertyChangedインターフェースは、System.ComponentModel名前空間に定義されているインターフェースで、以下のように定義されています。

#nullable enable
namespace System.ComponentModel
{
    //
    // Summary:
    //     Notifies clients that a property value has changed.
    public interface INotifyPropertyChanged
    {
        //
        // Summary:
        //     Occurs when a property value changes.
        event PropertyChangedEventHandler? PropertyChanged;
    }
}

INotifyPropertyChangedの実装では、PropertyChanged イベントを定義して、必要に応じてイベントを発行すればよいことがわかります。

さらに、イベントを発行するために必要な定義を確認します。PropertyChangedEventHandlerやイベント引数となるPropertyChangedEventArgsは以下のように定義されています。

#nullable enable
namespace System.ComponentModel
{
    //
    // Summary:
    //     Represents the method that will handle the System.ComponentModel.INotifyPropertyChanged.PropertyChanged
    //     event raised when a property is changed on a component.
    //
    // Parameters:
    //   sender:
    //     The source of the event.
    //
    //   e:
    //     A System.ComponentModel.PropertyChangedEventArgs that contains the event data.
    public delegate void PropertyChangedEventHandler(object? sender, PropertyChangedEventArgs e);

    public class PropertyChangedEventArgs : EventArgs
    {
        /// <summary>
        /// Initializes a new instance of the <see cref='System.ComponentModel.PropertyChangedEventArgs'/>
        /// class.
        /// </summary>
        public PropertyChangedEventArgs(string? propertyName)
        {
            PropertyName = propertyName;
        }

        /// <summary>
        /// Indicates the name of the property that changed.
        /// </summary>
        public virtual string? PropertyName { get; }
    }
}

プロパティの値が変化した時は、変化したプロパティ名を引数として通知することになります。

具体的には、プロパティを更新し後に、以下のように通知します。

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

propertyNameには、変更になったプロパティの名前を文字列で設定します。

はじめに例を挙げたクラスDataAを改変して、INotifyPropertyChangedを実装してみます。

#nullable enable
namespace Test
{
    public class DataA : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

        string _propertyB = "";
        public string PropertyB
        {
            get { return _propertyB; }
            set
            {
                if(_propertyB != value)
                {
                    _propertyB = value;
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PropertyB)));
                }
            }
        }
    }
}

PropertyChangedイベントの定義が追加されました。プロパティのセッター(setメソッド)では、値が変化していないときに変化通知イベントを発行すると、受け取る側で余分な負荷となるため、値が変化しているときのみ、イベントを発行しています。結果として、プロパティのセッターが少し複雑になりました。

プロパティがたくさんあると、各プロパティのセッターで同じコードを複数記載することになります。

そのため、多くの場合、INotifyPropertyChangedの実装をサポートする基底クラス(base class)を用意したり、PropertyChangedEventHandlerの拡張メソッド(extension method)を用意したりします。

実装をサポートする基底クラスの場合は、たとえば、以下のようなクラスを用意します。

#nullable enable
namespace NishySoftware.nsCommon
{
    public abstract class BindableBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

        protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] String propertyName = null!, bool needFire = true)
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return false;

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

        protected void OnPropertyChanged([CallerMemberName] string propertyName = null!)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        [Obsolete("Use nameof() instead. ex) SetProperty(ref field, value, nameof(property))")]
        protected bool SetProperty<T>(ref T field, T value, Expression<Func<T>> selectorExpression, bool needFire = true)
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return false;

            field = value;
            if (needFire)
                this.OnPropertyChanged(selectorExpression);
            return true;
        }

        [Obsolete("Use nameof() instead. ex) OnPropertyChanged(nameof(property))")]
        protected void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
        {
            if (selectorExpression == null)
                throw new ArgumentNullException(nameof(selectorExpression));
            if (!(selectorExpression.Body is MemberExpression body))
                throw new ArgumentException("The body must be a member expression");
            OnPropertyChanged(body.Member.Name);
        }
    }
}

これを基底クラスとして、先ほどのDataAクラスを書き直すと以下のようになります。

#nullable enable
using NishySoftware.nsCommon;
namespace Test
{
    public class DataA : BindableBase
    {
        string _propertyB = "";
        public string PropertyB
        {
            get { return _propertyB; }
            set { SetProperty(ref _propertyB, value); }
        }
    }
}

かなりシンプルになりました。プロパティが増えても、同じコードの複製は減らすことができます。

値の比較、更新、変更通知が、SetProperty() の一行で実現可能となります。

set { SetProperty(ref _propertyB, value); }

この一行には、変更通知のパラメーターとして必要なプロパティ名が記載されていません。

本来は、第3引数でプロパティ名を設定します。しかし、第3引数を省略するとSetProperty()を呼び出したプロパティの名前が第3引数の値として自動的に設定されるように、基底クラスのBindableBaseが実装されています。そのため、プロパティ名の設定を省略できます。


今回の投稿は、INotifyPropertyChangedの実装の基本について説明しました。次回の投稿では、今回は抜粋であった基底クラスBindableBaseの全体とプロパティ名の指定の過去と現在について説明します。

“INotifyPropertyChangedの実装とプロパティ名 #1” への1件の返信

コメントを残す