RelayCommand 属性

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

Note

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

Platform 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] コマンドは、IAsyncRelayCommandIAsyncRelayCommand<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;
}

こうすると、CanGreetUser ボタンが最初に UI (ボタンなど) にバインドされたときに呼び出され、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 には、GreetUserCommand への Button コントロール バインドがあります。つまり、CanExecuteChanged イベントが発生するたびに、その CanExecute メソッドが再び呼び出されます。 これにより、ラップされた CanGreetUser メソッドが評価され、入力 User インスタンス (UI で SelectedUser プロパティにバインドされている) が null かどうかに基づいて、ボタンの新しい状態が返されます。 つまり、SelectedUser が変更されるたびに、そのプロパティに値があるかどうかに基づいて、GreetUserCommand が有効になるかならないかが決まります。これは、このシナリオでの望ましい動作です。

Note

このコマンドは、CanExecute メソッドまたはプロパティの戻り値がいつ変更されたかを自動的に認識しません。 IRelayCommand.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}!");
}

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

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