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件の返信