ObservableProperty 属性

ObservableProperty は、注釈付きフィールドから監視可能なプロパティを生成できるようにする属性です。 その目的は、監視可能なプロパティを定義するために必要な定型句の量を大幅に減らすことです。

Note

機能させるには、注釈付きフィールドが、必要な INotifyPropertyChanged インフラストラクチャを持つ部分クラス内に存在する必要があります。 型が入れ子になっている場合は、宣言構文ツリー内のすべての型にも部分として注釈を付ける必要があります。 これを行わないと、ジェネレーターは要求された監視可能なプロパティを使用してその型の別の部分宣言を生成できないため、コンパイル エラーが発生します。

プラットフォーム API:ObservablePropertyNotifyPropertyChangedForNotifyCanExecuteChangedForNotifyDataErrorInfoNotifyPropertyChangedRecipientsICommandIRelayCommandObservableValidatorPropertyChangedMessage<T>IMessenger

しくみ

ObservableProperty 属性を使用すると、次のように、部分型のフィールドに注釈を付けることができます。

[ObservableProperty]
private string? name;

そして、これによって次のような監視可能なプロパティが生成されます。

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

これは最適化された実装でも同じことを行うため、最終的な結果はさらに速くなります。

Note

生成されたプロパティの名前は、フィールド名に基づいて作成されます。 ジェネレーターは、フィールドの名前が lowerCamel_lowerCamelm_lowerCamel のいずれかであることを想定し、それを適切な .NET 名前付け規則に従う UpperCamel へと変換します。 結果のプロパティは常にパブリック アクセサーを持ちますが、フィールドは任意の可視性で宣言できます (private が推奨されます)。

変更時のコードの実行

生成されるコードは実際にはこれより少し複雑です。その理由は、コードが通知ロジックにフックして、必要に応じてプロパティが更新されようとしているときと、更新された直後に追加のロジックを実行するために実装できるいくつかのメソッドも公開するためです。 つまり、生成されるコードは実際には次のようになります。

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

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

partial void OnNameChanging(string? oldValue, string? newValue);
partial void OnNameChanged(string? oldValue, string? newValue);

これにより、これらのメソッドのいずれかを実装して、追加のコードを挿入できます。 最初の 2 つは、参照する必要があるのがプロパティに設定された新しい値のみであるロジックを実行したい場合に役立ちます。 他の 2 つは、古い値と設定されようとしている新しい値の両方に基づいて何らかの状態を更新する必要がある、より複雑なロジックが存在する場合に役立ちます。

たとえば、以下に示すのは最初の 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 つのオーバーロードを使用する例です。

[ObservableProperty]
private ChildViewModel? selectedItem;

partial void OnSelectedItemChanging(ChildViewModel? oldValue, ChildViewModel? newValue)
{
    if (oldValue is not null)
    {
        oldValue.IsSelected = true;
    }

    if (newValue is not null)
    {
        newValue.IsSelected = true;
    }
}

利用可能なメソッドのうちの任意の数のメソッドだけを実装して構いません。どれも実装しなくても構いません。 どれも実装されていない場合 (または 1 つだけが実装されている場合) は、呼び出し全体がコンパイラによって削除されるだけなので、この追加機能が必要ない場合のパフォーマンスに対する影響はまったく存在しません。

Note

生成されるメソッドは実装がない部分メソッドです。つまり、それらの実装を選択する場合は、それらに対して明示的なアクセシビリティを指定することはできません。 つまり、これらのメソッドの実装も単なる partial メソッドとして宣言する必要があり、これらのメソッドは常に暗黙的にプライベート アクセシビリティを持ちます。 明示的なアクセシビリティ (例: publicprivate の追加) を追加しようとすると、それは C# では許可されないため、エラーが発生します。

依存プロパティへの通知

Name が変更されるたびに通知を生成したい FullName プロパティがあるとします。 これを行うには、次のように NotifyPropertyChangedFor 属性を使用します。

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

これにより、次と同等のプロパティが生成されます。

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

依存コマンドへの通知

実行状態がこのプロパティの値に依存するコマンドがあるとします。 つまり、プロパティが変更されるたびに、コマンドの実行状態を無効にして、もう一度計算する必要があります。 言い換えると、ICommand.CanExecuteChanged をもう一度発生させる必要があります。 これを実現するには、次のように NotifyCanExecuteChangedFor 属性を使用します。

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

これにより、次と同等のプロパティが生成されます。

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

これが機能するには、ターゲット コマンドは何らかの IRelayCommand プロパティである必要があります。

プロパティの検証の要求

プロパティが ObservableValidator を継承する型で宣言されている場合は、それに何らかの検証属性の注釈を付けて、生成されるセッターにそのプロパティの検証をトリガーするように要求することも可能です。 これは、次のように 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 オブジェクトの状態が更新され、UI コンポーネントがそれに対応して検証エラーを適切に表示できるようになります。

Note

設計上、ValidationAttribute を継承するフィールド属性のみが生成されたプロパティに転送されます。 これは、特にデータ検証シナリオをサポートするために行われます。 他のすべてのフィールド属性は無視されるため、現状では、フィールドにカスタム属性を追加して、生成されたプロパティに対してそれらも適用することは不可能です。 (シリアル化の制御などで) これが必要な場合、代わりに従来の手動プロパティを使用することを検討してください。

通知メッセージの送信

プロパティが ObservableRecipient を継承する型で宣言されている場合は、NotifyPropertyChangedRecipients 属性を使用して、プロパティ変更のプロパティ変更メッセージを送信するコードも挿入するようにジェネレーターに指示できます。 これにより、登録された受信者が変更に動的に対応できるようになります。 つまり、次のコードを考えてみましょう。

[ObservableProperty]
[NotifyPropertyChangedRecipients]
private string? name;

これにより、次のプロパティが生成されます。

public string? Name
{
    get => name;
    set
    {
        string? oldValue = name;

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

生成された Broadcast 呼び出しにより、現在のビューモデルで使用されている IMessenger インスタンスを使用して、登録されているすべてのサブスクライバーに新しい PropertyChangedMessage<T> が送信されます。

カスタム属性の追加

場合によっては、生成されたプロパティに加えていくつかのカスタム属性も設定することが役に立ちます。 これを実現するには、注釈付きフィールドに対して属性リストの [property: ] ターゲットを使用するだけでよく、MVVM Toolkit によって生成されたプロパティにそれらの属性が自動的に転送されます。

たとえば、次のようなフィールドを考えてみましょう。

[ObservableProperty]
[property: JsonRequired]
[property: JsonPropertyName("name")]
private string? username;

これにより、[JsonRequired] および [JsonPropertyName("name")] 属性が追加された状態で Username プロパティが生成されます。 プロパティをターゲットとする属性リストは必要な数だけ使用でき、そのすべてが生成されたプロパティに転送されます。

  • MVVM Toolkit の実際の動作を確認するには、サンプル アプリ (複数の UI フレームワーク向け) を参照してください。
  • 単体テスト」では、さらに他の例を確認できます。