Atributo RelayCommand

O RelayCommand tipo é um atributo que permite gerar propriedades de comando de retransmissão para métodos anotados. Sua finalidade é eliminar completamente a clichê necessária para definir comandos que encapsulam métodos privados em um viewmodel.

Note

Para funcionar, os métodos anotados precisam estar em uma classe parcial. Se o tipo estiver aninhado, todos os tipos na árvore de sintaxe de declaração também deverão ser anotados como parciais. Não fazer isso resultará em erros de compilação, pois o gerador não poderá gerar uma declaração parcial diferente desse tipo com o comando solicitado.

APIs de plataforma:RelayCommand, ICommand, , IRelayCommand, IRelayCommand<T>, IAsyncRelayCommand, IAsyncRelayCommand<T>, , Task, CancellationToken

Como funciona

O RelayCommand atributo pode ser usado para anotar um método em um tipo parcial, assim como:

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

E gerará um comando como este:

private RelayCommand? greetUserCommand;

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

Note

O nome do comando gerado será criado com base no nome do método. O gerador usará o nome do método, acrescentará "Command" ao final e removerá o prefixo "On", se presente. Além disso, para métodos assíncronos, o sufixo "Async" também é removido antes de "Command" ser adicionado.

Parâmetros de comando

O [RelayCommand] atributo dá suporte à criação de comandos para métodos com um parâmetro. Nesse caso, ele alterará automaticamente o comando gerado para ser um IRelayCommand<T> em vez disso, aceitando um parâmetro do mesmo tipo:

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

Isso resultará no seguinte código gerado:

private RelayCommand<User>? greetUserCommand;

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

O comando resultante usará automaticamente o tipo do argumento como seu argumento de tipo.

Comandos assíncronos

O comando [RelayCommand] também oferece suporte ao encapsulamento de métodos assíncronos por meio das interfaces IAsyncRelayCommand e IAsyncRelayCommand<T>. Isso é tratado automaticamente sempre que um método retorna um Task tipo. Por exemplo:

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

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

Isso resultará no seguinte código:

private AsyncRelayCommand? greetUserCommand;

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

Se o método usar um parâmetro, o comando resultante também será genérico.

Há um caso especial em que o método possui um CancellationToken, pois esse CancellationToken será propagado para o comando, a fim de habilitar o cancelamento. Ou seja, um método como este:

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

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

Resultará no comando gerado passando um token para o método encapsulado. Isso permite que os consumidores chamem IAsyncRelayCommand.Cancel apenas para sinalizar esse token e permitir que as operações pendentes sejam interrompidas corretamente.

Habilitar e desabilitar comandos

Geralmente, é útil poder desabilitar comandos e, posteriormente, invalidar seu estado e fazer com que eles verifiquem novamente se eles podem ser executados ou não. Para dar suporte a isso, o RelayCommand atributo expõe a CanExecute propriedade, que pode ser usada para indicar uma propriedade ou método de destino a ser usado para avaliar se um comando pode ser executado:

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

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

Dessa forma, CanGreetUser é invocado quando o botão é vinculado pela primeira vez à interface do usuário (por exemplo, a um botão) e, em seguida, é invocado novamente sempre que IRelayCommand.NotifyCanExecuteChanged é invocado no comando.

Por exemplo, é assim que um comando pode ser associado a uma propriedade para controlar seu estado:

[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}"/>

Neste exemplo, a propriedade gerada SelectedUser invocará GreetUserCommand.NotifyCanExecuteChanged() o método sempre que seu valor for alterado. A interface do usuário tem uma vinculação de controle Button a GreetUserCommand, o que significa que, sempre que o evento CanExecuteChanged for disparado, ela chamará o método CanExecute novamente. Isso fará com que o método encapsulado CanGreetUser seja avaliado, o que retornará o novo estado do botão com base em se a instância de User de entrada (que, na interface do usuário, está vinculada à propriedade SelectedUser) é null ou não. Isso significa que, sempre que SelectedUser for alterado, GreetUserCommand será habilitado ou não, dependendo de essa propriedade ter um valor, que é o comportamento desejado neste cenário.

Note

O comando não estará automaticamente ciente de quando o valor retornado do CanExecute método ou propriedade for alterado. Cabe ao desenvolvedor chamar IRelayCommand.NotifyCanExecuteChanged para invalidar o comando e solicitar que o método vinculado CanExecute seja avaliado novamente para atualizar o estado visual do controle associado ao comando.

Lidando com execuções concorrentes

Sempre que um comando é assíncrono, ele pode ser configurado para decidir se deseja permitir execuções simultâneas ou não. Ao usar o RelayCommand atributo, isso pode ser definido por meio da AllowConcurrentExecutions propriedade. O padrão é false, ou seja, até que uma execução esteja pendente, o comando sinalizará seu estado como sendo desabilitado. Se, em vez disso, for definido como true, qualquer número de invocações concorrentes poderá ser enfileirado.

Observe que, se um comando aceitar um token de cancelamento, um token também será cancelado se uma execução simultânea for solicitada. A principal diferença é que, se execuções simultâneas forem permitidas, o comando permanecerá habilitado e iniciará uma nova execução solicitada sem esperar que a anterior seja realmente concluída.

Tratamento de exceções assíncronas

Há duas maneiras diferentes de os comandos de retransmissão assíncrona lidarem com exceções:

  • Aguardar e relançar (padrão): quando o comando aguarda a conclusão de uma invocação, quaisquer exceções serão naturalmente relançadas no mesmo contexto de sincronização. Isso geralmente significa que as exceções que estão sendo geradas apenas travam o aplicativo, o que é um comportamento consistente com o dos comandos síncronos (em que exceções que estão sendo geradas também falharão no aplicativo).
  • Propagação de exceções para o agendador de tarefas: se um comando estiver configurado para propagar exceções para o agendador de tarefas, as exceções lançadas não farão o aplicativo falhar; em vez disso, elas ficarão disponíveis por meio do IAsyncRelayCommand.ExecutionTask exposto, assim como serão propagadas até o TaskScheduler.UnobservedTaskException. Isso permite cenários mais avançados (como ter componentes de interface do usuário associados à tarefa e exibir resultados diferentes com base no resultado da operação), mas é mais complexo usar corretamente.

O comportamento padrão é fazer com que os comandos aguardarem e lancem novamente exceções. Isso pode ser configurado por meio da FlowExceptionsToTaskScheduler propriedade:

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

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

Nesse caso, o try/catch não é necessário, pois as exceções não farão mais o app travar. Observe que isso também fará com que outras exceções não relacionadas não sejam relançadas automaticamente, portanto, você deve decidir cuidadosamente como abordar cada cenário individual e configurar o restante do código adequadamente.

Cancelar comandos para operações assíncronas

Uma última opção para comandos assíncronos é a capacidade de solicitar que um comando de cancelamento seja gerado. Trata-se de um ICommand que encapsula um comando de retransmissão assíncrono que pode ser usado para solicitar o cancelamento de uma operação. Esse comando sinalizará automaticamente seu estado para refletir se ele pode ou não ser usado a qualquer momento. Por exemplo, se o comando vinculado não estiver em execução, ele relatará seu estado como também não sendo executável. Isso pode ser usado da seguinte maneira:

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

Isso fará com que uma DoWorkCancelCommand propriedade também seja gerada. Em seguida, isso pode ser associado a algum outro componente de interface do usuário para permitir que os usuários cancelem facilmente operações assíncronas pendentes.

Adicionando atributos personalizados

Assim como acontece com as propriedades observáveis, o RelayCommand gerador também inclui suporte para atributos personalizados para as propriedades geradas. Para aproveitar isso, você pode simplesmente usar o [property: ] destino em listas de atributos em vez de métodos anotados e o Kit de Ferramentas MVVM encaminhará esses atributos para as propriedades de comando geradas.

Por exemplo, considere um método como este:

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

Isso gerará uma GreetUserCommand propriedade, com o [JsonIgnore] atributo sobre ela. Você pode usar quantas listas de atributos forem direcionadas ao método desejado e todas elas serão encaminhadas para as propriedades geradas.

Exemplos

  • Confira o aplicativo de exemplo (para várias estruturas de interface do usuário) para ver o Kit de Ferramentas MVVM em ação.
  • Você também pode encontrar mais exemplos nos testes de unidade.