概要
この記事はC# Advent Calendar 2017の14日目の記事です。
n番煎じですが、INotifyPropertyChangedの実装について、今さらながらにまとめてみます。
ネットで調べると既に数限りない実装方法の説明がありますが、それ故に初心者の方は結局どれがいいんだがわかんねー、となることも多いと思います。
そこで実行結果は全て同じにした上で、C#3~C#7記法、さらにライブラリの継承やAOPなどちょっと変わったやつまで含めて、色々な方法で実装して独断と偏見で比較します。
実行結果
まず最初に全部で共通の実行結果について説明します。
TextBoxとTextBlockが並べてあるだけです。
上のTextBoxを変更すると、下のTextBlockに反映されます。
なお、Jは発音しませんが必要です。
(参考画像)
共通View
ViewはReactiveProperty以外は全て同じです。
ReactivePropertyのみ、プロパティ名に.Value
を付けます。
コードビハインドは何も書いていないので省略します。
<Window
x:Class="C7InotifyPropertyChangedTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:C7InotifyPropertyChangedTest"
Width="325" Height="150">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<StackPanel>
<!-- ReactiveProperty以外 -->
<TextBox Text="{Binding Person.Name, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding Person.FullName}" />
<!-- ReactiveProperty専用 -->
<!--<TextBox Text="{Binding Person.Name.Value, UpdateSourceTrigger=PropertyChanged}" />-->
<!--<TextBlock Text="{Binding Person.FullName.Value}" />-->
</StackPanel>
</Window>
MainWindowViewModel
ViewModelでは各スタイルで書いたPersonクラスをプロパティとして公開しています。
class MainWindowViewModel
{
public object Person { get; set; }
= new Person3(); //C#3版
//= new Person5(); //C#5版
//= new Person6(); //C#6版
//= new Person7(); //C#7版
//= new Person3X(); //C#3式木版
//= new PersonVM(); //独自ViewModel継承版
//= new PersonNB(); //独自ViewModel継承バッキングフィールド無し版
//= new PersonEX(); //拡張メソッド使用版
//= new PersonMV(); //MVVMライブラリ使用版
//= new PersonRP(); //ReactiveProperty版
//= new PersonFD(); //Fody使用版
}
個別クラス
1つのクラスで閉じているパターンです。
INotifyPropertyChangedの基本的な解説ではここから始めるのが多いですが、プロダクトコードでは使われることは多くないかも?
C#のバージョンごとに書いていきます。
C#3版
長いです。
変更通知するプロパティ名が文字列なのでVisualStudioの名前変更で解決するのが難しいです。
public class Person3 : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private string _Name = "Hejlsberg";
public string Name
{
get { return _Name; }
set
{
if (value == _Name)
return;
_Name = value;
RaisePropertyChanged("Name");
RaisePropertyChanged("FullName");
}
}
public string FullName
{
get { return "Anders " + Name; }
}
}
メリット(◯)/デメリット(✖)
◯ C#3で書ける
◯ 継承の必要なし
✖ 変更通知するプロパティ名の指定が文字列
✖ 長い
✖ 個別のクラスごとに実装する必要がある
C#5版
C#4は特に変化ないので、飛ばします。
長さはC#3と変わらないです。
CallerMemberNameを使用して、呼び出し元プロパティ名だけは省略できます。
public class Person5 : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName]string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private string _Name = "Hejlsberg";
public string Name
{
get { return _Name; }
set
{
if (value == _Name)
return;
_Name = value;
RaisePropertyChanged();
RaisePropertyChanged("FullName");
}
}
public string FullName
{
get { return "Anders " + Name; }
}
}
メリット(◯)/デメリット(✖)
◯ C#5で書ける
◯ 継承の必要なし
✖ 変更通知するプロパティ名の指定が一部文字列
✖ 長い
✖ 個別のクラスごとに実装する必要がある
参考
C# 5.0 の新機能 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
C#6版
nameof演算子のおかげでプロパティ名を文字列で指定するのを回避できます。
他にもNull条件演算子やGet-Onlyプロパティのラムダ式化などで、記述が簡潔になっています。
public class Person6 : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private string _Name = "Hejlsberg";
public string Name
{
get { return _Name; }
set
{
if (value == _Name)
return;
_Name = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(FullName));
}
}
public string FullName => $"Anders {Name}";
}
メリット(◯)/デメリット(✖)
◯ C#6で書ける
◯ 継承の必要なし
◯ C#5版よりは短い
✖ それでも長い
✖ 個別のクラスごとに実装する必要がある
参考
C# 6 の新機能 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
[C# / WPF] 最新のC# 6.0でMVVMパターンを実装する
C#6.0での実装パターンまとめ
C#7.0版
C#6とほとんど同じです。
プロパティのGet部分をラムダ式にできます。
public class Person7 : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private string _Name = "Hejlsberg";
public string Name
{
get => _Name;
set
{
if (value == _Name)
return;
_Name = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(FullName));
}
}
public string FullName => $"Anders {Name}";
}
メリット(◯)/デメリット(✖)
◯ 継承の必要なし
◯ C#5版よりは短い
✖ それでも長い
✖ 個別のクラスごとに実装する必要がある
✖ C#7が必要
参考
expression-bodied な関数 C# によるプログラミング入門 | ++C++; // 未確認飛行 C
C#7版コードスニペット
C#6のコードスニペットはありましたが、C#7版は無かったので置いておきます。
NotifyProperty_CSharp7.snippet -GitHub
ショートカットは「propn」です。
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2008/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>NotifyProperty</Title>
<Shortcut>propn</Shortcut>
<Author>soi</Author>
<Description>変更通知プロパティを作成します</Description>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>type</ID>
<ToolTip>プロパティの型</ToolTip>
<Default>string</Default>
</Literal>
<Literal>
<ID>name</ID>
<ToolTip>プロパティ名</ToolTip>
<Default>MyProperty</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[
private $type$ _$name$;
public $type$ $name$
{
get => _$name$;
set
{
if(_$name$ == value)
return;
_$name$ = value;
RaisePropertyChanged();
}
}
]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
C#3式木版 (2017/12/16追記)
式木を使えば、C#3でも文字列を使わずプロパティ名を指定できます。
C#3版よりも更に長くなりますが、Typoなどを防げるのでより安全です。
nameof演算子が使えないC#5までは有効な方法です。
public class Person3X : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged<T>(Expression<Func<T>> propertyName)
{
if (PropertyChanged == null)
return;
// ラムダ式のBodyを取得する。MemberExpressionじゃなかったら駄目
var memberEx = propertyName.Body as MemberExpression;
if (memberEx == null)
throw new ArgumentException();
PropertyChanged(this, new PropertyChangedEventArgs(memberEx.Member.Name));
}
private string _Name = "Hejlsberg";
public string Name
{
get { return _Name; }
set
{
if (value == _Name)
return;
_Name = value;
RaisePropertyChanged(() => Name);
RaisePropertyChanged(() => FullName);
}
}
public string FullName
{
get { return "Anders " + Name; }
}
}
メリット(◯)/デメリット(✖)
◯ 継承の必要なし
◯ C#3で書ける
✖ さらに長い
✖ 個別のクラスごとに実装する必要がある
参考
https://tpodolak.com/blog/2014/01/19/implementing-inotifypropertychanged-without-lambda-expressions-and-magic-strings/
よねやんさんに教えていただきました。
情報ありがとうございます。
独自ViewModel継承版
これ以降は基本的にC#7の記法で書きますが、C#3などでも書けます。
毎回同じのを書くのは面倒くさい場合、INotifyPropertyChangedを実装した基底ViewModel的なものを作り、それを継承します。
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
/// <summary>
/// 前と値が違うなら変更してイベントを発行する
/// </summary>
/// <typeparam name="TResult">プロパティの型</typeparam>
/// <param name="source">元の値</param>
/// <param name="value">新しい値</param>
/// <param name="propertyName">プロパティ名/param>
/// <returns>値の変更有無</returns>
protected bool RaisePropertyChangedIfSet<TResult>(ref TResult source,
TResult value, [CallerMemberName]string propertyName = null)
{
//値が同じだったら何もしない
if (EqualityComparer<TResult>.Default.Equals(source, value))
return false;
source = value;
//イベント発行
RaisePropertyChanged(propertyName);
return true;
}
}
public class PersonVM : MyViewModel
{
private string _Name = "Hejlsberg";
public string Name
{
get => _Name;
set
{
if (RaisePropertyChangedIfSet(ref _Name, value))
RaisePropertyChanged(nameof(FullName));
}
}
public string FullName => $"Anders {Name}";
}
ついでに同値判定部分とプロパティ変更イベント発行を同時にやるメソッドを定義しました。
注意点として、多重継承を認めないというC#の仕様上、必ずこのMyViewModelクラスを継承する必要があるというのは状況によっては大きな制約になる場合があります。
メリット(◯)/デメリット(✖)
◯ (子クラスは)けっこう短い
✖ 継承が必要
✖ 基底クラスが別に必要
独自ViewModel継承 バッキングフィールド無し版(2017/12/24更新)
今までのプロパティは基本的にバッキングフィールドが別に必要でした。
このバッキングフィールドを無くすために、基底クラスに代わりの値の置き場所を作り、取得・変更のメソッドを公開します。
上記独自ViewModelをさらに継承して基底クラスを作ります。
public class MyViewModelNobackingField : MyViewModel
{
//プロパティ名をKeyとしたバッキングフィールド代わりのDictionary
private Dictionary<string, object> currentPropertyValues = new Dictionary<string, object>();
/// <summary>
/// 現在のプロパティ値を取得
/// </summary>
protected TResult GetPropertyValue<TResult>([CallerMemberName]string propertyName = null)
//プロパティの型の既定値を初期値とする
=> GetPropertyValue(default(TResult), propertyName);
/// <summary>
/// 現在のプロパティ値を取得
/// </summary>
/// <param name="initialValue">初期値</param>
protected TResult GetPropertyValue<TResult>(TResult initialValue,
[CallerMemberName]string propertyName = null)
{
//キーに値が無かったら初期値を現在値に入力
if (!currentPropertyValues.ContainsKey(propertyName))
currentPropertyValues[propertyName] = initialValue;
//Dictionaryから現在値を取得してプロパティの型に変換する
return (TResult)currentPropertyValues[propertyName];
}
/// <summary>
/// 前と値が違うなら変更してイベントを発行する
/// </summary>
/// <param name="value">新しい値</param>
/// <returns>値の変更有無</returns>
protected bool RaisePropertyChangedIfSet<TResult>(TResult value,
[CallerMemberName]string propertyName = null)
{
//値が同じだったら何もしない
if (EqualityComparer<TResult>.Default.Equals(GetPropertyValue<TResult>(propertyName), value))
return false;
//プロパティの現在値に入力
currentPropertyValues[propertyName] = value;
//イベント発行
RaisePropertyChanged(propertyName);
return true;
}
}
public class PersonNB : MyViewModelNobackingField
{
public string Name
{
get => GetPropertyValue(initialValue: "Hejlsberg");
set
{
if (RaisePropertyChangedIfSet(value))
RaisePropertyChanged(nameof(FullName));
}
}
public string FullName => $"Anders {Name}";
}
これにより、以下の2つの利点があります。
・バッキングフィールドがいらない
・クラス内部からの変更であっても必ずset句を通る
特に後者の点は、誤ってクラス内部からプロパティではなくバッキングフィールドを直接変更して通知が飛ばないことによる混乱を招くのを防ぐことができます。
ただし取得・変更時にアンボクシングを伴う操作を行っているので、パフォーマンスは少し落ちます。
また注意点として、Getterより先にSetterが呼ばれると指定した初期値(GetPropertyValue(initialValue:...
)が無視されます。
メリット(◯)/デメリット(✖)
◯ (子クラスは)けっこう短い
◯ バッキングフィールドが不要
✖ 継承が必要
✖ 基底クラスが別に必要
✖ アンボクシングが発生する
参考:
INotifyPropertyChanged (2 of 3) – without the backing fields
拡張メソッド版
継承をしたくない、だけど毎回同じコードを書くのは嫌だ、という時は拡張メソッドで解決できます。
PropertyChangedEventHandler(INotifyPropertyChangedではなく)に対する拡張メソッドを作ります。
(2018/02/21更新 プロパティ名が正しく伝わらないバグ修正)
public static class PropertyChangedEventHandlerExtensions
{
/// <summary>
/// イベントを発行する
/// </summary>
/// <typeparam name="TResult">プロパティの型</typeparam>
/// <param name="_this">イベントハンドラ</param>
/// <param name="propertyName">プロパティ名を表すExpression。() => Nameのように指定する。</param>
public static void Raise<TResult>(this PropertyChangedEventHandler _this, Expression<Func<TResult>> propertyName)
{
// ハンドラに何も登録されていない場合は何もしない
if (_this == null) return;
// ラムダ式のBodyを取得する。MemberExpressionじゃなかったら駄目
if (!(propertyName.Body is MemberExpression memberEx))
throw new ArgumentException();
// () => NameのNameの部分の左側に暗黙的に存在しているオブジェクトを取得する式をゲット
// ConstraintExpressionじゃないと駄目
if (!(memberEx.Expression is ConstantExpression senderExpression))
throw new ArgumentException();
// ○:定数なのでValueプロパティからsender用のインスタンスを得る
var sender = senderExpression.Value;
// 下準備が出来たので、イベント発行!!
_this(sender, new PropertyChangedEventArgs(memberEx.Member.Name));
}
/// <summary>
/// 前と値が違うなら変更してイベントを発行する
/// </summary>
/// <typeparam name="TResult">プロパティの型</typeparam>
/// <param name="_this">イベントハンドラ</param>
/// <param name="propertyName">プロパティ名を表すExpression。() => Nameのように指定する。</param>
/// <param name="source">元の値</param>
/// <param name="value">新しい値</param>
/// <returns>値の変更有無</returns>
public static bool RaiseIfSet<TResult>(this PropertyChangedEventHandler _this, Expression<Func<TResult>> propertyName, ref TResult source, TResult value)
{
//値が同じだったら何もしない
if (EqualityComparer<TResult>.Default.Equals(source, value))
return false;
source = value;
//イベント発行
Raise(_this, propertyName);
return true;
}
}
public class PersonEx : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _Name = "Hejlsberg";
public string Name
{
get => _Name;
set
{
if (PropertyChanged.RaiseIfSet(() => Name, ref _Name, value))
PropertyChanged.Raise(() => FullName);
}
}
public string FullName => $"Anders {Name}";
}
メリット(◯)/デメリット(✖)
◯ 継承の必要なし
◯ (本体クラスは)けっこう短い
✖ 拡張メソッドを含んだ静的クラスが別に必要
参考
イケテルINotifyPropertyChangedの実装の改善 - かずきのBlog@hatena
MVVMライブラリで継承版
MvvmLightなど、他のライブラリのViewModelBaseを継承して作るともっと簡潔に書けます。
public class PersonMV : GalaSoft.MvvmLight.ViewModelBase
{
private string _Name = "Hejlsberg";
public string Name
{
get => _Name;
set
{
if (Set(ref _Name, value))
RaisePropertyChanged(nameof(FullName));
}
}
public string FullName => $"Anders {Name}";
}
独自のViewModelクラスの代わりにライブラリ内の基底クラスを継承しています。
独自ViewModel継承版と同様にSet~
部分に同値判定や呼び出し元プロパティ通知などが含まれています。
メリット(◯)/デメリット(✖)
◯ けっこう短い
✖ 継承が必要
✖ MVVMライブラリの導入が必要
ReactiveProperty版
MVVMライブラリの1つReactivePropertyを使用する場合、Prism等と異なり基底クラスの継承は必要ありません。
public class PersonRP
{
public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>("Hejlsberg");
public ReadOnlyReactiveProperty<string> FullName { get; }
public PersonRP()
{
FullName = Name
.Select(x => $"Anders {x}")
.ToReadOnlyReactiveProperty();
}
}
かなり雰囲気が変わりますね。
Personクラスの代わりに、ReactiveProperty自体がINotifyPropertyChangedを実装しています。
そのため、基底クラスもINotifyPropertyChanged自体も継承が必要がありません。
ただしこれのみViewを変更する必要があり、プロパティ名の後に.Value
が必要です。
またReactivePropertyの特徴として、プロパティの変更をObservableなStreamとして扱え、ReactiveExtensionの多様な拡張メソッド群を使用できます。
上記コードでもName
プロパティの変更Streamを加工したものをReadOnlyReactivePropertyに変換、それをFullName
プロパティとして公開しています。
同じ実行結果であっても、その内部の動き他の方式とは異なります。
メリット(◯)/デメリット(✖)
◯ 継承は必要ない
◯ 短い、単純なプロパティであれば1行で書ける
◯ ReactiveExtensionsの力で色々できる
◯ バッキングフィールドが不要
✖ MVVMライブラリの導入が必要
✖ 他とは雰囲気が違うので慣れが必要
✖ View側に.Value
を追加する必要がある
参考
MVVMをリアクティブプログラミングで快適にReactivePropertyオーバービュー - かずきのBlog@hatena
2017/12/31追記
WPFでDataContextに指定する場合、INotifyPropertyChangedを継承していないとメモリーリークするそうです。
http://mobile.aridai.net/article/?p=15
Fody版
AOP(アスペクト指向プログラミング)ライブラリのFody(PropertyChanged.Fody)を使用すると、こんな何の通知も書いていないクラスが上記コードと同じ動きをします。
[AddINotifyPropertyChangedInterface]
public class PersonFD
{
public string Name { get; set; } = "Hejlsberg";
public string FullName => $"Anders {Name}";
}
出力ウインドウで何やらやっているのが見て取れます。
------ ビルド開始: プロジェクト: C7InotifyPropertyChangedTest, 構成: Debug Any CPU ------
Fody: Fody (version 2.2.1.0) Executing
Fody/PropertyChanged: Removing reference to 'PropertyChanged'.
Fody: Finished Fody 69ms.
~
\C7InotifyPropertyChangedTest.exe
Fody: Skipped Verifying assembly since it is disabled in configuration
Fody: Finished verification in 0ms.
========== ビルド: 1 正常終了、0 失敗、0 更新不要、0 スキップ ==========
かなり黒魔術感ありますが、コードはめっさ簡潔。
注意点として、プロジェクトにFodyを導入していると、すべてのINotifyPropertyChangedを実装したクラスが自動で加工されます。
Fodyで加工してほしくない場合はDoNotNotify
属性を付与する必要があります。
ブレークポイントで止めたりすると書いた覚えのないメソッドがあったりして面白い。
IL書くのは怖くない!
ILSpyで中間言語(IL)からC#に再変換して、読みやすくしたのが以下です。
やはりコンパイル時にINotifyPropertyChangedを実装しています。
public class PersonFD_ILSpy : INotifyPropertyChanged
{
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public string Name
{
[CompilerGenerated]
get { return this.< Name > k__BackingField; }
[CompilerGenerated]
set
{
if (string.Equals(this.< Name > k__BackingField, value, StringComparison.Ordinal))
{
return;
}
this.< Name > k__BackingField = value;
this.OnPropertyChanged("FullName");
this.OnPropertyChanged("Name");
}
}
public string FullName { get { return string.Format("Anders {0}", this.Name); } }
public PersonFD()
{
this.< Name > k__BackingField = "Hejlsberg";
base..ctor();
}
}
◯ 継承は必要ない
◯ 短い、ダントツ短い
✖ Fodyライブラリの導入が必要
✖ 実際にどう実装されたかはILから読み直さないとわからない
参考
Fody-PropertyChanged
C# の素晴らしさを語る会 で喋ってきました。 - 亀岡的プログラマ日記
まとめ
いかがでしたでしょうか。思いつく限りのINotifyPropertyChanged実装のパターンを並べてみました。
どれを導入するべきかの判断材料として、このアクティビティ図を参考にしてください。
他にもこんなパターンもあるぜ!というのがあったら教えてください。
C#Future?(おまけ)
C#の開発予定を見る限り、特にINotifyPropertyChanged実装に関わる機能は無さそう?
ただCommunity内で議論自体は上がっていて、こんなのが提案されていたりします。
//!!このコードは2017年時点ではコンパイルできません!!
public class PersonX : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged[CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public string Name
{
get;
set
{
if (value == field)
return;
field = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(FullName));
}
} = "Anders";
public string FullName => $"{Name} Hejlsberg";
}
field
をキーワードとしてバッキングフィールドの代わりに使うようです。
独自ViewModel継承 バッキングフィールド無し版と同様のメリットがあり、
うっかりクラス内部からバッキングフィールドを変更して通知が飛ばないことで混乱した経験がある身としては使ってみたくなります。
参考
https://github.com/dotnet/csharplang/issues/140
2020/04/05追記
岩永さんいわく、C#デザインチームはやるつもり無かったけど、あまりに要望が来るので(笑)、検討しているそうです。
C# vNext 始動! Visual Studio 16.6 Preview 2 / .NET 5 Preview 2 - YouTube 1:04:19から
ソースコード
今回の完全なソースコードをGithubにおいておきます。
https://github.com/soi013/INotifyPropertyChangedImplementations
環境
VisualStudio2017
.NET Framework 4.7
MvvmLight 5.3.0
ReactiveProperty 4.2.0
Fody 2.4.1
PropertyChanged.Fody 2.2.5