ObservableObject は、INotifyPropertyChanged と INotifyPropertyChanging インターフェースを実装することによって observable(監視)できるオブジェクトの base クラスです。プロパティの変更通知をサポートする必要のある、あらゆるオブジェクトの起点として使用することができます。
APIs:
- ObservableObject
- TaskNotifier
- TaskNotifier
どのように機能するか (How it works)
ObservableObject
の主な特徴はつぎのとおりです:
- INotifyPropertyChanged と INotifyPropertyChanging の基本実装を提供して、PropertyChanged と PropertyChanging イベントを公開します。
- ObservableObject を継承した型のプロパティの値を簡単に設定して、適切なイベントを自動的に発生させるために使用できる SetProperty メソッドを提供します。
- SetPropertyAndNotifyOnCompletion メソッドを提供します。このメソッドは SetProperty に似ています。Task プロパティを設定して、割り当てられたタスクが完了したときに通知イベントを発生させる機能を備えています。
- OnPropertyChanged および OnPropertyChanging メソッドを公開しています。派生型を override することで、通知イベントの発生方法をカスタマイズすることができます。
単純な例
以下は、カスタムプロパティに通知のサポートを実装する例です:
public class User : ObservableObject { private string name; public string Name { get => name; set => SetProperty(ref name, value); } }
提供される SetProperty
サンプル
private string name; /// <summary> /// Gets or sets the name to display. /// </summary> public string Name { get => name; set => SetProperty(ref name, value); }
<StackPanel Spacing="8"> <TextBox PlaceholderText="Type here to update the text below" Text="{x:Bind ViewModel.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="{x:Bind ViewModel.Name, Mode=OneWay}"/> </StackPanel>
non-observable モデルのラッピング
例えば、データベースのアイテムを扱う際によくあるシナリオを挙げます。データベースモデルのプロパティをリレーして、プロパティが変更されたことを通知する "bindable" な(バインディング可能な)ラップモデルを作成します。
これは、INotifyPropertyChanged インターフェースを実装していないモデルに通知機能のサポートを追加したい場合に必要なラッピングです。ObservableObject は、この処理を簡単にするための専用メソッドを提供します。
以下の例では、User
は ObservableObject を継承せずに、データベースのテーブルを直接にマッピングしたモデルになります:
public class ObservableUser : ObservableObject { private readonly User user; public ObservableUser(User user) => this.user = user; public string Name { get => user.Name; set => SetProperty(user.Name, value, user, (u, n) => u.Name = n); } }
この場合、SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string)
の overload を使用します。これは、以前のシナリオのように backing field にアクセスできなくても、コードを効率的にするために必要なことです。このメソッドのシグネチャの各部分を詳しく見て、コンポーネントの役割を理解しましょう:
TModel
は型引数で、ラップする(ターゲットの)モデル型のことを示しています。この例だとUser
クラスのことです。C# コンパイラーは、SetProperty メソッドをどのように呼び出しているのかによって、自動的に判断します。T
は設定したいプロパティの型です。TModel
と同様に判断されます。T.oldValue
が最初の引数で、この例ではプロパティの現在値として user.Name を渡している。T.newValue
はプロパティに設定をする新しい値です。例では プロパティの入力値である value を渡しています。TModel
は、ラップする対象のモデルで、例では user インスタンスを渡します。Action<TModel, T> callback
は、プロパティに与えられた新しい値が現在値と違うとき、プロパティに新しい値を設定する際に呼び出される Action です。この callback 関数は、ターゲットのモデルTModel
と設定する新しい値を引数として受け取っています。(入力値 n を Name プロパティに代入してるだけ)ここで重要なことは、現在のスコープから値の読み取りを避け、callback の引数として与えられた値のみと対話することです。この理由は、C# コンパイラが callback 関数をキャッシュして、多くのパフォーマンス改善ができるようになります。なので、ここではフィールドや setter のパラメーターにアクセスするのではなくて、ラムダ式の入力パラメーター(引数)だけを使用しています。
SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string)
メソッドは、非常にコンパクトな API を提供しながら、ターゲット+プロパティの取得と設定の両方ができるので、ラッピング プロパティの作成をかなり簡単にしてくれます。
Note: LINQ 式を使ったメソッドの実装に比べると、特に callback パラメーターの代わりに Expression<Func
Task の扱い (Handling Task properties)
もしも、プロパティが Task なら、タスクが完了したときに通知イベントを発生させ、binding を適切なタイミングで更新する必要があります。(例えば、タスクに紐づいた読み込みのインジケーター、または、その他のステータス情報の表示)ObservableObject はこのシナリオのための API を持っています:
public class MyModel : ObservableObject { private TaskNotifier<int>? requestTask; public Task<int>? RequestTask { get => requestTask; set => SetPropertyAndNotifyOnCompletion(ref requestTask, value); } public void RequestValue() { RequestTask = WebService.LoadMyValueAsync(); } }
SetPropertyAndNotifyOnCompletion<T>(ref TaskNotifier<T>, Task<T>, string)
メソッドが、フィールドの更新、新しいタスクの監視、タスクが完了したときの通知イベントを行います。タスクプロパティに binding するだけで、そのステータスが更新されたときに通知を受け取ることができます。
TaskNotifier<T>
は ObservableObject によって公開される特別な型で、ターゲットの Task
Note: SetPropertyAndNotifyOnCompletion メソッドは、Microsoft.Toolkit パッケージの NotifyTaskCompletion
Sample
private TaskNotifier myTask; /// <summary> /// Gets or sets the name to display. /// </summary> public Task MyTask { get => myTask; private set => SetPropertyAndNotifyOnCompletion(ref myTask, value); } /// <summary> /// Simulates an asynchronous method. /// </summary> public void ReloadTask() { MyTask = Task.Delay(3000); }
<StackPanel Spacing="8"> <Button Content="Click me to load a Task to await" Click="{x:Bind ViewModel.ReloadTask}"/> <TextBlock Text="{x:Bind ViewModel.MyTask.Status, Mode=OneWay}"/> </StackPanel>