sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# CommunityToolkit.Mvvm の学習2 ObservableProperty

ObservableProperty の属性

ObservablePropery は、アノテーションされたフィールド(つまり属性)から observable プロパティを生成できる属性です。目的は、observable プロパティを定義するために必要なボイラープレートコードを減らすことです。

NOTE: 属性が機能するためには、INotifyPropertyChanged のインターフェースを継承した class の中である必要があります。

APIs:

  • ObservableProperty
  • NotifyPropertyChangedFor
  • NotifyCanExecuteChangedFor
  • NotifyDataErrorInfo
  • NotifyPropertyChangedRecipients
  • ICommand
  • IRelayCommand
  • ObservableValidator
  • PropertyChangedMessage
  • IMessenger

API 8.0 のリンクがまだありませんでした。2024 年 3 月現在は 8.2.2 が最新です。「LINK: 7.1.0

How it works(どのように機能するのか)

ObservableProperty 属性は、次のように「フィールド型」に注釈をつけるために使う:

[ObservableProperty]
private string? name;

上の属性は、つぎのような observable プロパティを生成します:

public string? Name
{
    get => name;
    set => SetProperty(ref name, value);
}

この書き方は、最適化された実行になるので、最終的な結果はより速くなります。

Note: 生成されるプロパティの名前は、フィールド名に基づいて作成されます。ジェネレーターは、フィールドの名前が lowerCamel, _lowerCamel, m_lowerCamel のいずれかであると仮定して、適切な .NET 命名規則に従うように変換します。生成されるプロパティは、常に public(アクセス修飾子)を持ちますが、field は任意のアクセス修飾子で宣言することが可能です。(もちろん private が推奨です)

Running code upon changes(変化に応じた実行コード)

実際には、生成されるコードはすこし複雑です。その理由は、通知のロジックにフックできる実装メソッドをいくつか公開しているので、プロパティが更新前/後に追加のロジックを実行できるからです。もし、必要なら、生成されるコードは(実際は)次のコードに似ています:

public string? Name
{
    get => name;
    set
    {
        if (!EqualityComparer<string?>.Default.Equals(name, value))
        {
            OnNameChanging(value);
            OnPropertyChanging();
            name = value;
            OnNameChanged(value);
            OnPropertyChanged();
        }
    }
}

partial void OnNameChanging(string? value);
partial void OnNameChanged(string? value);

これは、(これらの)2つのメソッドのいずれかを実装して、追加のコードを挿入できます:

[ObservableProperty]
private string? name;

partial void OnNameChanging(string? value)
{
    Console.WriteLine($"Name is about to change to {value}");
}

partial void OnNameChanged(string? value)
{
    Console.WriteLine($"Name has changed to {value}");
}

これら2つのメソッドのうちどちらか一方だけを実装することも、どちらも実装しないことも自由です。

partical メソッドはマニアックな機能なのでよく知らないなら「partial メソッドの拡張」なんかを参考に。

2つのメソッドが実装されない場合(あるいは、1つしか実装されない場合)、呼び出し全体がコンパイラによって削除されます。なので、この追加機能が必要ないケースの場合、パフォーマンスへの影響は全くありません。

Note: 生成メソッドは、実装コードはない「partial メソッド」です。実装することを選択した場合は、明示的なアクセス修飾子を指定できません。つまり、メソッドの実装も単なる partial メソッドして宣言しなければならず、それらは常に private アクセス修飾子を持つこと(持っていること)になります。明示的に(public や private の)アクセス修飾子を追加しようとしても、C# ではできないのでエラーの結果になります。(が、C# 9.0 で新しい partial が出てきたので混同に注意)

旧 partial が void 型のみで、新 partial が(なんらかの)型あり。新旧で別の機能と考えていたほうがよいくらいなので、やはり混同しないほうがいいと思います。

Notifying dependent properties(依存プロパティの通知)

Name プロパティが変更されるたびに通知を発生させたい FullName プロパティがあると仮定します。この場合 NotifyPropertyChangedFor 属性を使用することでもできるようになります:

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string? name;

生成されるプロパティは、以下と同じです:

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            OnPropertyChanged("FullName");
        }
    }
}

Notifying dependent commands(依存コマンドの通知)

実行可否の状態が、プロパティの値に依存するコマンドがあると仮定します。これは、プロパティの値が変更されるたびにコマンドの実行可否の状態を再確認し直す必要があります。

言い換えれば ICommand.CanExecuteChanged を再実行することになる。この場合 NotifyCanExecuteChangedFor 属性を使用することでもできるようになります:

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(MyCommand))]
private string? name;

生成されるプロパティは、以下と同じです:

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            MyCommand.NotifyCanExecuteChanged();
        }
    }
}

この機能は、コマンドは IRelayCommand で実装してないとダメです。(つまり、前提条件ありの組み合わせの機能)

Requesting property validation(プロパティの検証を要求する)

プロパティが ObservableValidator を継承する型で宣言されている場合、任意のバリデーション属性で修飾して、生成された setter にそのプロパティのバリデーションをトリガーするように要求することも可能です。これは NotifyDataErrorInfo 属性を使用することでもできるようになります:

[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[MinLength(2)] // Any other validation attributes too...
private string? name;

生成されるプロパティは、以下のようになります:

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            ValidateProperty(value, "Value2");
        }
    }
}

生成された ValidateProperty メソッドの呼び出しは、プロパティを検証して ObservableValidator オブジェクトの状態を更新します。

Note: 設計上、ValidationAttribute を継承する field 属性のみが生成されたプロパティに移ります。これは、データ検証のシナリオをサポートするためです。他のすべての field 属性は無視されます。(field に追加のカスタム属性を追加して、生成されたプロパティに適用することも今のところできない)もしも、(シリアライズを制御するためなどに)必要だったら、伝統的なプロパティを使ってください。

Sending notification messages(通知メッセージの送信)

プロパティが「ObservableRecipient」を継承する型で宣言されている場合、NotifyPropertyChangedRecipients 属性を使用してプロパティ変更に対してプロパティ変更メッセージを送信すうrコードを挿入するようにジェネレーターに指示することができます。これによって、(変更通知に)登録した受取者は変更に対して動的に反応すうることができます。次のコード:

[ObservableProperty]
[NotifyPropertyChangedRecipients]
private string? name;
public string? Name
{
    get => name;
    set
    {
        string? oldValue = name;

        if (SetProperty(ref name, value))
        {
            Broadcast(oldValue, value);
        }
    }
}

生成された Broadcast メソッドの呼び出しは、new PropertyChangedMessage を送信します。

参考