RelayCommand 特性

RelayCommand 类型是一个特性,允许为带批注的方法生成中继命令属性。 其目的是完全消除在 viewmodel 中定义命令包装私有方法所需的模板。

注意

为了正常工作,带批注的方法需要位于分部类中。 如果对类型进行嵌套,则必须也将声明语法树中的所有类型批注为分部。 否则将导致编译错误,因为生成器无法使用请求的命令生成该类型的其他分部声明。

平台 API:RelayCommandICommandIRelayCommandIRelayCommand<T>IAsyncRelayCommandIAsyncRelayCommand<T>TaskCancellationToken

工作原理

RelayCommand 特性可用于对分部类型中的方法进行批注,如下所示:

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

它将生成如下所示的命令:

private RelayCommand? greetUserCommand;

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

注意

将基于方法名称创建生成的命令的名称。 生成器将使用方法名称并在末尾追加“Command”,并且去除“On”前缀(如果存在)。 此外,对于异步方法,“Async”后缀也会在追加“Command”之前去除。

命令参数

[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;
}

这样,在按钮首次绑定到 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 具有绑定到 GreetUserCommandButton 控件,这意味着每次引发其 CanExecuteChanged 事件时,它都会再次调用其 CanExecute 方法。 这将导致计算包装的 CanGreetUser 方法,从而基于输入 User 实例(UI 中绑定到 SelectedUser 属性)是否为 null 返回按钮的新状态。 这意味着,每当 SelectedUser 发生更改时,GreetUserCommand 都将根据该属性是否具有值来启用或禁用,这是此方案中所需的行为。

注意

该命令不会自动知道 CanExecute 方法或属性的返回值何时发生更改。 而是由开发人员调用 IRelayCommand.NotifyCanExecuteChanged 以使命令失效,并请求重新评估链接的 CanExecute 方法,然后更新绑定到命令的控件的可视状态。

处理并发执行

每当命令是异步的,都可以将其配置为决定是否允许并发执行。 使用 RelayCommand 特性时,可以通过 AllowConcurrentExecutions 属性设置。 默认值为 false,这意味着,在执行挂起之前,命令将指示其状态为已禁用。 如果改为设置为 true,则可以将任意数量的并发调用排入队列。

请注意,如果命令接受取消令牌,则请求并发执行时也会取消令牌。 主要区别是,如果允许并发执行,该命令将保持启用状态,它将启动新的请求执行,而无需等待上一个执行实际完成。

处理异步异常

异步中继命令处理异常有两种不同的方法:

  • 等待和重新引发(默认):当命令等待完成调用时,任何异常自然都会在同一同步上下文中引发。 这通常意味着引发的异常只会使应用崩溃,该行为与同步命令的行为一致(其中引发的异常也会使应用崩溃)。
  • 将异常流式传送到任务计划程序:如果命令配置为将异常流式传送到任务计划程序,则引发的异常不会使应用崩溃,而是通过公开的 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,因为异常将不再使应用崩溃。 请注意,这还会导致不会自动重新引发其他不相关的异常,因此应仔细决定如何处理每个单独的方案并适当配置其余代码。

取消异步操作的命令

异步命令的最后一个选项是请求生成取消命令功能。 这是 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] 特性。 可根据需要使用任意多个针对方法的特性列表,所有这些特性列表都将转发到生成的属性。

示例

  • 查看示例应用(适用于多个 UI 框架),以了解 MVVM 工具包的实际运行情况。
  • 还可以在单元测试中查找更多示例。