Atributo RelayCommand

O RelayCommand tipo é um atributo que permite gerar propriedades de comandos de relé para métodos anotados. O seu objetivo é eliminar completamente o boilerplate necessário para definir comandos que envolvem métodos privados num viewmodel.

Observação

Para funcionar, os métodos anotados precisam de estar numa classe parcial. Se o tipo estiver aninhado, todos os tipos na árvore sintáctica de declarações também devem ser anotados como parciais. Não o fazer 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 da plataforma:RelayCommand, ICommand, IRelayCommand, IRelayCommand<T>, IAsyncRelayCommandIAsyncRelayCommand<T>TaskCancellationToken

Como funciona

O RelayCommand atributo pode ser usado para anotar um método num tipo parcial, da seguinte forma:

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

E vai gerar um comando assim:

private RelayCommand? greetUserCommand;

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

Observação

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

Parâmetros de comando

O [RelayCommand] atributo suporta a criação de comandos para métodos com um parâmetro. Nesse caso, alterará automaticamente o comando gerado para ser antes um IRelayCommand<T>, aceitando um parâmetro do mesmo tipo:

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

Isto 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 argumento tipo.

Comandos assíncronos

O comando [RelayCommand] também suporta o encapsulamento de métodos assíncronos, através das interfaces IAsyncRelayCommand e IAsyncRelayCommand<T>. Isto é tratado automaticamente sempre que um método devolve um Task tipo. Por exemplo:

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

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

Isto resultará no seguinte código:

private AsyncRelayCommand? greetUserCommand;

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

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

Existe um caso especial em que o método tem um CancellationToken, pois isso será propagado para o comando que permite 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)
    {
    }
}

Fará com que o comando gerado passe um token para o método encapsulado. Isto permite aos consumidores simplesmente ligar IAsyncRelayCommand.Cancel para sinalizar esse token e permitir que as operações pendentes sejam paradas corretamente.

Ativação e desativação de comandos

É frequentemente útil poder desativar comandos e, mais tarde, invalidar o seu estado e fazer com que verificem novamente se podem ser executados ou não. Para suportar isto, o RelayCommand atributo expõe a CanExecute propriedade, que pode ser usada para indicar uma propriedade ou método alvo a usar 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;
}

Desta forma, CanGreetUser é invocado quando o botão é inicialmente atribuído à interface (por exemplo, a um botão), e depois é invocado novamente cada vez IRelayCommand.NotifyCanExecuteChanged que é invocado no comando.

Por exemplo, é assim que um comando pode ser associado a uma propriedade para controlar o 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 irá invocar GreetUserCommand.NotifyCanExecuteChanged() o método sempre que o seu valor mudar. A interface de utilizador tem uma associação de controlo Button a GreetUserCommand, o que significa que, sempre que o evento CanExecuteChanged for acionado, voltará a chamar o método CanExecute. Isto fará com que o método encapsulado CanGreetUser seja avaliado, o que devolverá o novo estado do botão com base em se a instância de entrada User (que, na interface, está associada à propriedade SelectedUser) está null ou não. Isto significa que sempre que SelectedUser for alterado, GreetUserCommand ficará ativado ou não, dependendo se essa propriedade tem um valor, que é o comportamento desejado neste cenário.

Observação

O comando não estará automaticamente ciente de quando o valor de retorno do CanExecute método ou propriedade mudou. Cabe ao programador chamar IRelayCommand.NotifyCanExecuteChanged para invalidar o comando e pedir que o método ligado CanExecute seja avaliado novamente para depois atualizar o estado visual do controlo vinculado ao comando.

Gestão de execuções simultâneas

Sempre que um comando é assíncrono, pode ser configurado para decidir se permite execuções simultâneas ou não. Ao usar o RelayCommand atributo, este pode ser definido através da AllowConcurrentExecutions propriedade. O padrão é false, o que significa que, até que uma execução esteja pendente, o comando sinalizará o seu estado como desativado. Se, em vez disso, for definido para true, qualquer número de invocações concorrentes pode ser colocado em fila.

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

Gestão de exceções assíncronas

Existem duas formas diferentes de os comandos de retransmissão assíncrona lidarem com exceções:

  • Await and rethrow (default): quando o comando aguarda a conclusão de uma invocação, quaisquer exceções serão naturalmente lançadas no mesmo contexto de sincronização. Isto normalmente significa que exceções lançadas apenas fazem a aplicação crashar, o que é um comportamento consistente com comandos síncronos (onde exceções lançadas também fazem a aplicação crashar).
  • Encaminhar exceções para o agendador de tarefas: se um comando estiver configurado para encaminhar exceções para o agendador de tarefas, as exceções lançadas não farão a aplicação falhar; em vez disso, ficarão disponíveis através do IAsyncRelayCommand.ExecutionTask exposto, bem como se propagarão até ao TaskScheduler.UnobservedTaskException. Isto permite cenários mais avançados (como ter componentes da interface ligados à tarefa e mostrar resultados diferentes consoante o resultado da operação), mas é mais complexo de usar corretamente.

O comportamento predefinido consiste em os comandos aguardarem e relançarem exceções. Isto pode ser configurado através da FlowExceptionsToTaskScheduler propriedade:

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

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

Neste caso, não try/catch é necessário, pois as exceções deixam de fazer a aplicação crashar. Note que isto também fará com que outras exceções não relacionadas não sejam automaticamente relançadas, por isso deve decidir cuidadosamente como abordar cada cenário individual e configurar o resto do código de forma adequada.

Comandos de cancelamento para operações assíncronas

Uma última opção para comandos assíncronos é a capacidade de solicitar a geração de um comando cancel. Este é um ICommand comando de enrolamento de um relé assíncrono que pode ser usado para solicitar o cancelamento de uma operação. Este comando sinaliza automaticamente o seu estado para refletir se pode ou não ser usado a qualquer momento. Por exemplo, se o comando ligado não estiver a ser executado, irá reportar o seu estado como também não executável. Isto pode ser usado da seguinte forma:

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

Isto fará com que uma DoWorkCancelCommand propriedade também seja gerada. Isto pode depois ser associado a outro componente da interface para permitir que os utilizadores cancelem facilmente operações assíncronas pendentes.

Adição de atributos personalizados

Tal como acontece com as propriedades observáveis, o RelayCommand gerador também inclui suporte para atributos personalizados para as propriedades geradas. Para tirar partido disto, podes simplesmente usar o [property: ] alvo nas listas de atributos em vez de métodos anotados, e o MVVM Toolkit encaminha esses atributos para as propriedades de comandos gerados.

Por exemplo, considere um método como este:

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

Isto gerará uma GreetUserCommand propriedade, com o [JsonIgnore] atributo sobre ela. Pode usar tantas listas de atributos destinadas ao método quantas quiser, e todas elas serão propagadas para as propriedades geradas.

Exemplos

  • Dá uma vista de olhos à aplicação de exemplo (para múltiplos frameworks de interface) para veres o MVVM Toolkit em ação.
  • Também podes encontrar mais exemplos nos testes unitários.