導入
ViewModelのプロパティが変わったとき、画面(View)に「変わったよ」と伝える仕組みがINotifyPropertyChangedです。これがないと、コードの上では値が変わっていても画面表示は古いままになります。ただし、プロパティのsetterひとつひとつに通知処理を書くのは面倒で、コピペミス(プロパティ名の書き間違い)も起きやすい作業です。共通の土台ViewModelBaseを作り、この面倒を1箇所に押し込みましょう。
図解
classDiagram
class INotifyPropertyChanged {
<<interface>>
+PropertyChanged event
}
class ViewModelBase {
+PropertyChanged event
#OnPropertyChanged(name)
#SetProperty~T~(ref field, value, name) bool
}
class TaskListViewModel {
+Tasks
+NewTaskTitle
}
INotifyPropertyChanged <|.. ViewModelBase
ViewModelBase <|-- TaskListViewModel
サンプル
using System.ComponentModel;
using System.Runtime.CompilerServices;
// ViewModelBase を継承するだけで、プロパティ変更通知が使えるようになる
int changedCount = 0;
string? lastPropertyName = null;
var person = new PersonViewModel();
person.PropertyChanged += (sender, e) => { changedCount++; lastPropertyName = e.PropertyName; };
person.Name = "Alice";
Console.WriteLine($"{person.Name} / 通知{changedCount}回 / プロパティ名={lastPropertyName}");
// Alice / 通知1回 / プロパティ名=Name
class PersonViewModel : ViewModelBase
{
private string _name = "";
public string Name
{
get => _name;
set => SetProperty(ref _name, value); // 変更判定+通知をまとめて委譲する
}
}
abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? name = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false; // 同じ値なら何もしない
field = value;
OnPropertyChanged(name);
return true;
}
}
[CallerMemberName]は、呼び出し元のプロパティ名(この場合"Name")をコンパイラが自動で埋め込む属性。文字列を手で渡さないのでタイプミスによるバグを防げるSetProperty<T>は「値が変わっていなければ何もしない」判定と、通知の発行を1行にまとめるViewModelBaseを継承するだけで、以降のViewModelはSetPropertyを呼ぶだけでよくなる
演習
using System.ComponentModel;
using System.Runtime.CompilerServices;
int changedCount = 0;
string? lastPropertyName = null;
var person = new PersonViewModel();
person.PropertyChanged += (sender, e) => { changedCount++; lastPropertyName = e.PropertyName; };
person.Name = "Alice";
Assert(person.Name == "Alice", "値が変わる");
Assert(changedCount == 1, "1回だけ通知される");
Assert(lastPropertyName == "Name", "プロパティ名がNameで通知される");
person.Name = "Alice"; // 同じ値を再セット
Assert(changedCount == 1, "同じ値なら通知は増えない");
Console.WriteLine("全テスト成功");
class PersonViewModel : ViewModelBase
{
private string _name = "";
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
}
// TODO: INotifyPropertyChanged を実装する ViewModelBase を完成させてください。
// ・PropertyChanged イベントを持つ
// ・SetProperty<T> は、値が変わっていれば field を更新して true を、
// 変わっていなければ何もせず false を返す
// ・プロパティ名は呼び出し元から渡させず [CallerMemberName] で自動取得する
abstract class ViewModelBase : INotifyPropertyChanged
{
___
}
void Assert(bool condition, string name)
{
if (!condition) throw new Exception($"FAIL: {name}");
}
- 期待される出力:
全テスト成功
ヒント1を見る
public event PropertyChangedEventHandler? PropertyChanged; を宣言し、protected void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); を用意します
ヒント2を見る
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? name = null) { if (EqualityComparer<T>.Default.Equals(field, value)) return false; field = value; OnPropertyChanged(name); return true; }
まとめ
INotifyPropertyChangedは「プロパティが変わったこと」を画面へ知らせる仕組み- 通知処理は
ViewModelBaseに共通化し、SetPropertyを呼ぶだけにする [CallerMemberName]でプロパティ名の手入力(=タイプミスの温床)をなくす
次回: この土台の上に、実際に状態を持つTaskListViewModelを作ります。