RelayCommand 属性

RelayCommand型は、注釈付きメソッドのリレー コマンド プロパティを生成できる属性です。 その目的は、ビューモデルでプライベート メソッドをラップするコマンドを定義するために必要な定型句を完全に排除することです。

Note

機能するためには、注釈付きメソッドが 部分クラスに含まれる必要があります。 型が入れ子になっている場合は、宣言構文ツリー内のすべての型にも部分注釈を付ける必要があります。 そうしないとコンパイル エラーが発生します。ジェネレーターは、要求されたコマンドを使用してその型の別の部分宣言を生成できないためです。

プラットフォーム API:RelayCommandICommandIRelayCommandIRelayCommand<T>IAsyncRelayCommandIAsyncRelayCommand<T>TaskCancellationToken

どのように機能するのか

RelayCommand属性を使用すると、次のように、部分型のメソッドに注釈を付けることができます。

[RelayCommand]
private void GreetUser()
{
    Console.WriteLine("Hello!");
}

そして、次のようなコマンドが生成されます。

private RelayCommand? greetUserCommand;

public IRelayCommand GreetUserCommand => greetUserCommand ??= new RelayCommand(GreetUser);

Note

生成されたコマンドの名前は、メソッド名に基づいて作成されます。 ジェネレーターはメソッド名を使用し、末尾に "Command" を追加し、"On" プレフィックス (存在する場合) を削除します。 さらに、非同期メソッドの場合は、"Command" がアペンドされる前に "Async" サフィックスも削除されます。

コマンド パラメーター

[RelayCommand]属性は、パラメーターを持つメソッドのコマンドの作成をサポートしています。 その場合、生成されたコマンドは自動的に IRelayCommand<T> に変更され、同じ型のパラメーターが受け入れられます。

[RelayCommand]
private void GreetUser(User user)
{
    Console.WriteLine($"Hello {user.Name}!");
}

これにより、次のコードが生成されます。

private RelayCommand<User>? greetUserCommand;

public IRelayCommand<User> GreetUserCommand => greetUserCommand ??= new RelayCommand<User>(GreetUser);

結果のコマンドでは、引数の型が型引数として自動的に使用されます。

非同期コマンド

[RelayCommand] コマンドは、IAsyncRelayCommandインターフェイスとIAsyncRelayCommand<T> インターフェイスを介した非同期メソッドのラップもサポートしています。 これは、メソッドが Task 型を返すたびに自動的に処理されます。 次に例を示します。

[RelayCommand]
private async Task GreetUserAsync()
{
    User user = await userService.GetCurrentUserAsync();

    Console.WriteLine($"Hello {user.Name}!");
}

これにより、次のコードが生成されます。

private AsyncRelayCommand? greetUserCommand;

public IAsyncRelayCommand GreetUserCommand => greetUserCommand ??= new AsyncRelayCommand(GreetUserAsync);

メソッドがパラメーターを受け取る場合、結果のコマンドもジェネリックになります。

メソッドに CancellationTokenがある場合は、キャンセルを有効にするためにコマンドに伝達されるため、特別なケースがあります。 つまり、次のようなメソッドです。

[RelayCommand]
private async Task GreetUserAsync(CancellationToken token)
{
    try
    {
        User user = await userService.GetCurrentUserAsync(token);

        Console.WriteLine($"Hello {user.Name}!");
    }
    catch (OperationCanceledException)
    {
    }
}

生成されたコマンドは、ラップされたメソッドにトークンを渡します。 これにより、コンシューマーは IAsyncRelayCommand.Cancel を呼び出してトークンを通知し、保留中の操作を正しく停止できます。

コマンドの有効化と無効化

多くの場合、コマンドを無効にし、後で状態を無効にし、実行できるかどうかを再度確認してもらうと便利です。 これをサポートするために、 RelayCommand 属性は CanExecute プロパティを公開します。これは、コマンドを実行できるかどうかを評価するために使用するターゲット プロパティまたはメソッドを示すために使用できます。

[RelayCommand(CanExecute = nameof(CanGreetUser))]
private void GreetUser(User? user)
{
    Console.WriteLine($"Hello {user!.Name}!");
}

private bool CanGreetUser(User? user)
{
    return user is not null;
}

この方法では、ボタンが最初に UI (ボタンなど) にバインドされたときに CanGreetUser が呼び出され、コマンドで IRelayCommand.NotifyCanExecuteChanged が呼び出されるたびに再度呼び出されます。

たとえば、コマンドをプロパティにバインドして状態を制御する方法を次に示します。

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(GreetUserCommand))]
private User? selectedUser;
<!-- Note: this example uses traditional XAML binding syntax -->
<Button
    Content="Greet user"
    Command="{Binding GreetUserCommand}"
    CommandParameter="{Binding SelectedUser}"/>

この例では、生成された SelectedUser プロパティは、値が変更されるたびに GreetUserCommand.NotifyCanExecuteChanged() メソッドを呼び出します。 UI には、ButtonへのGreetUserCommand コントロール バインドがあります。つまり、CanExecuteChanged イベントが発生するたびに、CanExecute メソッドが再度呼び出されます。 これにより、ラップされた CanGreetUser メソッドが評価されます。これにより、入力 User インスタンス (UI で SelectedUser プロパティにバインドされている) が null かどうかに基づいて、ボタンの新しい状態が返されます。 つまり、 SelectedUser が変更されるたびに、そのプロパティが値を持っているかどうかに基づいて、 GreetUserCommand が有効になるか無効になります。これは、このシナリオでの望ましい動作です。

Note

メソッドまたはプロパティの戻り値がいつ変更されたかは、コマンドによって自動的に認識CanExecuteIRelayCommand.NotifyCanExecuteChangedを呼び出してコマンドを無効にし、リンクされたCanExecute メソッドをもう一度評価して、コマンドにバインドされたコントロールの表示状態を更新するように要求するのは開発者次第です。

同時実行の処理

コマンドが非同期の場合は常に、同時実行を許可するかどうかを決定するように構成できます。 RelayCommand属性を使用する場合は、AllowConcurrentExecutions プロパティを使用して設定できます。 既定値は false です。つまり、実行が保留中になるまで、コマンドはその状態を無効として通知します。 代わりに true に設定されている場合は、任意の数の同時呼び出しをキューに登録できます。

コマンドがキャンセル トークンを受け入れる場合、同時実行が要求されるとトークンも取り消されることに注意してください。 主な違いは、同時実行が許可されている場合、コマンドは有効なままになり、前の実行が実際に完了するのを待たずに新しい要求された実行を開始することです。

非同期例外の処理

非同期リレー コマンドが例外を処理する方法は 2 つあります。

  • 待機して再スロー(既定): コマンドが呼び出しの完了まで待機すると、例外はそのまま同一の同期コンテキスト上でスローされます。 これは通常、スローされる例外がアプリをクラッシュさせるだけであることを意味します。これは同期コマンドの動作と一致する動作です (例外がスローされると、アプリもクラッシュします)。
  • タスク スケジューラへの例外のフロー: コマンドがタスク スケジューラに例外をフローするように構成されている場合、スローされる例外はアプリをクラッシュさせるのではなく、公開された IAsyncRelayCommand.ExecutionTask を介して使用できるようになり、 TaskScheduler.UnobservedTaskExceptionにバブルアップされます。 これにより、より高度なシナリオ (UI コンポーネントをタスクにバインドし、操作の結果に基づいて異なる結果を表示するなど) が可能になりますが、正しく使用する方が複雑になります。

既定の動作では、例外を待機して再スローするコマンドがあります。 これは、 FlowExceptionsToTaskScheduler プロパティを使用して構成できます。

[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task GreetUserAsync(CancellationToken token)
{
    User user = await userService.GetCurrentUserAsync(token);

    Console.WriteLine($"Hello {user.Name}!");
}

この場合、例外によってアプリがクラッシュすることはなくなったので、 try/catch は必要ありません。 これにより、他の関連のない例外も自動的に再スローされないため、個々のシナリオにアプローチする方法を慎重に決定し、残りのコードを適切に構成する必要があることに注意してください。

非同期操作をキャンセルするコマンド

非同期コマンドの最後のオプションの 1 つは、cancel コマンドの生成を要求する機能です。 これは、操作の取り消しを要求するために使用できる非同期リレー コマンドをラップする ICommand です。 このコマンドは、任意の時点で使用できるかどうかを反映するように状態を自動的に通知します。 たとえば、リンクされたコマンドが実行されていない場合、その状態も実行可能でないと報告されます。 これは次のように使用できます。

[RelayCommand(IncludeCancelCommand = true)]
private async Task DoWorkAsync(CancellationToken token)
{
    // Do some long running work...
}

これにより、 DoWorkCancelCommand プロパティも生成されます。 これにより、ユーザーが保留中の非同期操作を簡単にキャンセルできるように、他の UI コンポーネントにバインドできます。

カスタム属性の追加

監視可能なプロパティと同様に、RelayCommand ジェネレーターには、生成されたプロパティのカスタム属性のサポートも含まれています。 これを利用するには、注釈付きメソッドに対して属性リストで [property: ] ターゲットを使用するだけで、MVVM ツールキットはこれらの属性を生成されたコマンド プロパティに転送します。

たとえば、次のようなメソッドを考えてみましょう。

[RelayCommand]
[property: JsonIgnore]
private void GreetUser(User user)
{
    Console.WriteLine($"Hello {user.Name}!");
}

これにより、 GreetUserCommand プロパティが生成され、その上に [JsonIgnore] 属性が含まれます。 メソッドをターゲットとする属性リストはいくつでも使用でき、そのすべてが生成されたプロパティに転送されます。

例示

  • MVVM Toolkit の動作を確認するには、 サンプル アプリ (複数の UI フレームワークの場合) を確認してください。
  • 単体テストでさらに例を見つけることもできます。