Compartilhar via


Programação assíncrona

Padrões para aplicativos MVVM assíncronos: Comandos

Stephen Cleary

Baixar o código de exemplo

Este é o segundo artigo em uma série sobre combinar o async e o await com o estabelecido padrão Model-View-ViewModel (MVVM). Na última vez, mostrei como fazer vinculação de dados a uma operação assíncrona e desenvolvi um tipo de chave chamado NotifyTaskCompletion<TResult>, que agia como um Task<TResult> amigável à vinculação de dados (consulte msdn.microsoft.com/magazine/dn605875). Agora volto minha atenção à ICommand, uma interface .NET usada por aplicativos MVVM para definir uma operação do usuário (que é geralmente vinculada por dados a um botão), e considerarei as implicações de fazer uma ICommand assíncrona.

Os padrões aqui podem não servir para todos os cenários perfeitamente, então fique à vontade para ajustá-los às suas necessidade. De fato, este artigo inteiro é apresentado como uma série de melhorias a um tipo de comando assíncrono. Ao fim dessas iterações, você terá um aplicativo como o mostrado na Figura 1. Ele é parecido com o aplicativo desenvolvido no meu último artigo, mas dessa vez eu forneço ao usuário um comando que ele pode mesmo executar. Quando o usuário clica no botão Go, a URL é lida a partir da caixa de texto, e o aplicativo vai contar o número de bytes daquela URL (após um atraso artificial). Enquanto a operação estiver em progresso, o usuário não poderá iniciar outra, mas poderá cancelar a operação.

An Application That Can Execute One Command
An Application That Can Execute One Command
An Application That Can Execute One Command
An Application That Can Execute One Command
An Application That Can Execute One Command
Figura 1 Um aplicativo capaz de executar um comando

Depois mostrarei uma abordagem bem parecida, que pode ser usada para criar qualquer quantidade de operações. A Figura 2 ilustra o aplicativo modificado para que o botão Go represente a adição de uma operação à uma coleção de operações.

An Application Executing Multiple Commands
Figura 2 Um aplicativo executando múltiplos comandos

No decorrer do desenvolvimento deste aplicativo, farei algumas simplificações para manter o foco nos comandos assíncronos em vez de detalhes da implementação. Primeiro, eu não usarei parâmetros de execução de comandos. Eu praticamente nunca precisei usá-los nos aplicativos reais que desenvolvi; mas se você precisar, os padrões neste artigo podem ser facilmente estendidos para incluí-los. Segundo, eu não implemento ICommand.CanExecuteChanged. Um evento padrão, tipo um campo, vaza memória em algumas plataformas MVVM (consulte bit.ly/1bROnVj). Para manter o código simples, eu uso o CommandManager, embutido no Windows Presentation Foundation (WPF), para implementar o CanExecuteChanged.

Também estou usando uma “camada de serviço” simplificada, que por enquanto é apenas um único método estático, como demonstrado na Figura 3. É essencialmente o mesmo serviço que no meu último artigo, mas estendido para suportar cancelamento. O próximo artigo vai falar sobre design de serviço assíncrono de verdade, mas por enquanto esse serviço simplificado é suficiente.

Figura 3 A camada de serviço

public static class MyService
{
  // bit.ly/1fCnbJ2
  public static async Task<int> DownloadAndCountBytesAsync(string url,
    CancellationToken token = new CancellationToken())
  {
    await Task.Delay(TimeSpan.FromSeconds(3), token).ConfigureAwait(false);
    var client = new HttpClient();
    using (var response = await client.GetAsync(url, token).ConfigureAwait(false))
    {
      var data = await
        response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
      return data.Length;
    }
  }
}

Comandos assíncronos

Antes de começarmos, dê uma olhada na interface ICommand:

public interface ICommand
{
  event EventHandler CanExecuteChanged;
  bool CanExecute(object parameter);
  void Execute(object parameter);
}

Ignore o CanExecuteChanged e os parâmetros e pense um pouco sobre como um comando assíncrono funcionaria com essa interface. O método CanExecute deve ser síncrono; o único membro que pode ser assíncrono é o Execute. O método Execute foi projetado para implementações síncronas, por isso ele retorna void. Como mencionei em um artigo anterior, “Práticas recomendadas na programação assíncrona” (msdn.microsoft.com/magazine/jj991977), métodos async void devem ser evitados a menos que sejam manipuladores de eventos (ou o equiv­alente lógico de um manipulador de evento). Implementações do ICommand.Execute são logicamente manipuladores de evento, portanto, podem ser async void.

No entanto, é melhor minimizar o código dentro de um método async void e, em vez disso, expor um método async Task que contenha a lógica em si. Essa prática torna o código mais testável. Com isso em mente, eu proponho o seguinte como uma interface de comando assíncronos, e o código na Figura 4 como a classe base:

public interface IAsyncCommand : ICommand
{
  Task ExecuteAsync(object parameter);
}

Figura 4 Tipo base para comandos assíncronos

public abstract class AsyncCommandBase : IAsyncCommand
{
  public abstract bool CanExecute(object parameter);
  public abstract Task ExecuteAsync(object parameter);
  public async void Execute(object parameter)
  {
    await ExecuteAsync(parameter);
  }
  public event EventHandler CanExecuteChanged
  {
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
  }
  protected void RaiseCanExecuteChanged()
  {
    CommandManager.InvalidateRequerySuggested();
  }
}

A classe base toma conta de duas coisas: Ela joga a implementação do CanExecuteChanged para a classe CommandManager e implementa o método async void ICommand.Execute por chamar o método IAsyncCommand.ExecuteAsync. Ela espera o resultado para assegurar que quaisquer exceções na lógica do comando assíncrono sejam elevadas ao loop principal da thread de IU.

Isso tem um certo nível de complexidade, mas cada um desses tipos tem um propósito. O IAsyncCommand pode ser usado por qualquer implementação assíncrona do ICommand e pretende-se que seja exposto a partir do ViewModels e consumido pelo View e por testes de unidade. O AsyncCommandBase manipula alguns códigos clichês comuns a todos os ICommands assíncronos.

Com esse trabalho de fundação feito, estou pronto para começar a desenvolver um comando assíncrono efetivo. O tipo delegado padrão para uma operação síncrona sem um valor de retorno é o Action. O equivalente assíncrono é o Func<Task>. A Figura 5 demonstra minha primeira iteração de um AsyncCommand baseado em delegado.

Figura 5 A primeira tentativa de um comando assíncrono

public class AsyncCommand : AsyncCommandBase
{
  private readonly Func<Task> _command;
  public AsyncCommand(Func<Task> command)
  {
    _command = command;
  }
  public override bool CanExecute(object parameter)
  {
    return true;
  }
  public override Task ExecuteAsync(object parameter)
  {
    return _command();
  }
}

Neste ponto, a IU só tem uma caixa de texto para a URL, um botão para iniciar a requisição HTTP e um rótulo para os resultados. O XAML e as partes essenciais do ViewModel são simples. Aqui está o Main­Window.xaml (pulando os atributos de posicionamento, como Margin):

<Grid>
  <TextBox Text="{Binding Url}" />
  <Button Command="{Binding CountUrlBytesCommand}" 
      Content="Go" />
  <TextBlock Text="{Binding ByteCount}" />
</Grid>

O MainWindowViewModel.cs é mostrado na Figura 6.

Figura 6 O primeiro MainWindowViewModel

public sealed class MainWindowViewModel : INotifyPropertyChanged
{
  public MainWindowViewModel()
  {
    Url = "http://www.example.com/";
    CountUrlBytesCommand = new AsyncCommand(async () =>
    {
      ByteCount = await MyService.DownloadAndCountBytesAsync(Url);
    });
  }
  public string Url { get; set; } // Raises PropertyChanged
  public IAsyncCommand CountUrlBytesCommand { get; private set; }
  public int ByteCount { get; private set; } // Raises PropertyChanged
}

Se você executar o aplicativo (AsyncCommands1, no download de código de exemplo), vai reparar em quatro casos de comportamento deselegante. Primeiro, o rótulo sempre exibe um resultado, mesmo antes de o botão ser clicado. Segundo, não há indicador de ocupado depois que você clica no botão, para indicar que a operação está em progresso. Terceiro, se a requisição HTTP falhar, a exceção é passada para o loop principal da IU, causando o travamento do aplicativo. Quarto, se o usuário fizer diversas requisições, não conseguirá distinguir os resultados; é possível que os resultados de uma requisição anterior sobreponham os de uma mais recente em decorrência da variação nos tempos de resposta dos servidores.

Não são poucos problemas! Mas antes que comece a iterar o projeto, considere por um momento os tipos de problemas surgidos. Quando uma IU se torna assíncrona, isso força você a pensar sobre estados adicionais na sua IU. Eu recomendo que você se faça ao menos estas perguntas:

  1. como a IU vai exibir erros? (Eu espero que a sua IU síncrona já tenha uma resposta para isso!)
  2. como deve ser a aparência da IU enquanto a operação estiver em progresso? (Por exemplo: ela vai fornecer resposta imediata na forma de indicadores de ocupado?)
  3. como o usuário ficará restrito enquanto a operação estiver em progresso? (Os botões serão desabilitados, por exemplo?)
  4. o usuário terá comandos adicionais disponíveis enquanto a operação estiver em progresso? (Por exemplo, ele poderá cancelar a operação?)
  5. se o usuário puder iniciar múltiplas operações, como a IU fornecerá detalhes de sucesso ou erro para cada uma delas? (Por exemplo, a IU vai usar um estilo “fila de comandos” ou pop-ups de notificação?)

Manipulando conclusão de comandos assíncronos através de vinculação de dados

A maior parte dos problemas da primeira iteração do Async­Command são relacionados a como os resultados são manipulados. O que é realmente necessário é alguma espécie de tipo que possa encapsular um Task<T> e fornecer algumas capacidades de vinculação de dados para que o aplicativo possa responder de maneira mais elegante. Pois bem, o tipo NotifyTaskCompletion<T>, desenvolvido no meu último artigo, se encaixa quase perfeitamente nessas necessidades. Eu vou adicionar a esse tipo um membro que simplifica um pouco da lógica do Async­Command: uma propriedade TaskCompletion que represente a conclusão da operação, mas não propague exceções (ou retorne um resultado). Aqui estão as modificações ao NotifyTaskCompletion<T>:

public NotifyTaskCompletion(Task<TResult> task)
{
  Task = task;
  if (!task.IsCompleted)
    TaskCompletion = WatchTaskAsync(task);
}
public Task TaskCompletion { get; private set; }

A próxima iteração do AsyncCommand usa o NotifyTaskCompletion para representar a própria operação. Por fazer isso, o XAML pode fazer vinculação de dados diretamente à mensagem de resultado ou erro daquela operação, e também pode usar a vinculação de dados para exibir uma mensagem apropriada enquanto a operação estiver em progresso. O novo AsyncCommand agora tem uma propriedade que representa a operação em si, como mostrado na Figure 7.

Figura 7 A segunda tentativa de um comando assíncrono

public class AsyncCommand<TResult> : AsyncCommandBase, INotifyPropertyChanged
{
  private readonly Func<Task<TResult>> _command;
  private NotifyTaskCompletion<TResult> _execution;
  public AsyncCommand(Func<Task<TResult>> command)
  {
    _command = command;
  }
  public override bool CanExecute(object parameter)
  {
    return true;
  }
  public override Task ExecuteAsync(object parameter)
  {
    Execution = new NotifyTaskCompletion<TResult>(_command());
    return Execution.TaskCompletion;
  }
  // Raises PropertyChanged
  public NotifyTaskCompletion<TResult> Execution { get; private set; }
}

Perceba que o AsyncCommand.ExecuteAsync está usando o TaskCompletion, e não o Task. Eu não quero propagar exceções para o loop principal da IU (o que aconteceria se ele aguardasse a propriedade Task); em vez disso, eu retorno a TaskCompletion e manipulo exceções por vinculação de dados. Também adicionei ao projeto um simples NullToVisibilityConverter para que o indicador de ocupado, os resultados e a mensagem de erro estejam todos ocultos até que o botão seja clicado. A Figura 8 mostra o código ViewModel atualizado.

Figura 8 O segundo MainWindowViewModel

public sealed class MainWindowViewModel : INotifyPropertyChanged
{
  public MainWindowViewModel()
  {
    Url = "http://www.example.com/";
    CountUrlBytesCommand = new AsyncCommand<int>(() => 
      MyService.DownloadAndCountBytesAsync(Url));
  }
  // Raises PropertyChanged
  public string Url { get; set; }
  public IAsyncCommand CountUrlBytesCommand { get; private set; }
}

E o novo código XAML é mostrado na Figura 9.

Figura 9 O segundo MainWindow XAML

<Grid>
  <TextBox Text="{Binding Url}" />
  <Button Command="{Binding CountUrlBytesCommand}" Content="Go" />
  <Grid Visibility="{Binding CountUrlBytesCommand.Execution,
    Converter={StaticResource NullToVisibilityConverter}}">
    <!--Busy indicator-->
    <Label Visibility="{Binding CountUrlBytesCommand.Execution.IsNotCompleted,
      Converter={StaticResource BooleanToVisibilityConverter}}"
      Content="Loading..." />
    <!--Results-->
    <Label Content="{Binding CountUrlBytesCommand.Execution.Result}"
      Visibility="{Binding CountUrlBytesCommand.Execution.IsSuccessfullyCompleted,
      Converter={StaticResource BooleanToVisibilityConverter}}" />
    <!--Error details-->
    <Label Content="{Binding CountUrlBytesCommand.Execution.ErrorMessage}"
      Visibility="{Binding CountUrlBytesCommand.Execution.IsFaulted,
      Converter={StaticResource BooleanToVisibilityConverter}}" Foreground="Red" />
  </Grid>
</Grid>

O código agora corresponde ao projeto AsyncCommands2 do código de exemplo. Esse código cuida de todas as preocupações que eu mencionei com a solução original: os rótulos ficam ocultos até o início da primeira operação; há um indicador imediato de ocupado para fornecer uma resposta ao usuário; as exceções são capturadas e atualizam a IU através de vinculação de dados; múltiplas requisições não interferem mais umas com as outras. Cada requisição cria um novo invólucro NotifyTaskCompletion, que tem seu próprio Result independente, além de outras propriedades. O NotifyTaskCompletion age como uma abstração de uma operação assíncrona, capaz de ter dados vinculados a si. Isso permite múltiplas requisições, com a IU sempre vinculando à mais recente. No entanto, em muitos caso do mundo real, a solução apropriada seria desabilitar múltiplas requisições. Isso é, você quer que o comando retorne false do CanExecute enquanto houver uma operação em progresso. Isso é fácil de se fazer com uma pequena modificação no AsyncCommand, como demonstrado na Figura 10.

Figura 10 Desabilitando múltiplas requisições

public class AsyncCommand<TResult> : AsyncCommandBase, INotifyPropertyChanged
{
  public override bool CanExecute(object parameter)
  {
    return Execution == null || Execution.IsCompleted;
  }
  public override async Task ExecuteAsync(object parameter)
  {
    Execution = new NotifyTaskCompletion<TResult>(_command());
    RaiseCanExecuteChanged();
    await Execution.TaskCompletion;
    RaiseCanExecuteChanged();
  }
}

Agora o código corresponde ao projeto AsyncCommands3 do código de exemplo. O botão é desabilitado enquanto a operação estiver acontecendo.

Adicionando cancelamento

Muitas operações assíncronas podem levar quantidades variadas de tempo. Por exemplo, uma requisição HTTP pode normalmente responder com muita rapidez, antes que o usuário possa responder. No entanto, se a rede estiver lenta, ou o servidor ocupado, a mesma requisição HTTP pode causar um atraso considerável. Parte de projetar uma IU assíncrona é esperar e construir para esse cenário. A solução atual já tem um indicador de ocupado. Quando você projeta uma IU assíncrona, pode também escolher dar ao usuário mais opções, e o cancelamento é uma escolha comum.

O cancelamento em si é sempre uma opção síncrona — o ato de pedir cancelamento é imediato. A parte mais complicada do cancelamento é quando ele pode ser executado; ele deve poder ser executado apenas quando há um comando assíncrono em progresso. As modificações ao AsyncCommand na Figura 11 fornecem um comando de cancelamento aninhado e notificam esse comando de cancelamento quando o comando assíncrono começa e termina.

Figura 11 Adicionando cancelamento

public class AsyncCommand<TResult> : AsyncCommandBase, INotifyPropertyChanged
{
  private readonly Func<CancellationToken, Task<TResult>> _command;
  private readonly CancelAsyncCommand _cancelCommand;
  private NotifyTaskCompletion<TResult> _execution;
  public AsyncCommand(Func<CancellationToken, Task<TResult>> command)
  {
    _command = command;
    _cancelCommand = new CancelAsyncCommand();
  }
  public override async Task ExecuteAsync(object parameter)
  {
    _cancelCommand.NotifyCommandStarting();
    Execution = new NotifyTaskCompletion<TResult>(_command(_cancelCommand.Token));
    RaiseCanExecuteChanged();
    await Execution.TaskCompletion;
    _cancelCommand.NotifyCommandFinished();
    RaiseCanExecuteChanged();
  }
  public ICommand CancelCommand
  {
    get { return _cancelCommand; }
  }
  private sealed class CancelAsyncCommand : ICommand
  {
    private CancellationTokenSource _cts = new CancellationTokenSource();
    private bool _commandExecuting;
    public CancellationToken Token { get { return _cts.Token; } }
    public void NotifyCommandStarting()
    {
      _commandExecuting = true;
      if (!_cts.IsCancellationRequested)
        return;
      _cts = new CancellationTokenSource();
      RaiseCanExecuteChanged();
    }
    public void NotifyCommandFinished()
    {
      _commandExecuting = false;
      RaiseCanExecuteChanged();
    }
    bool ICommand.CanExecute(object parameter)
    {
      return _commandExecuting && !_cts.IsCancellationRequested;
    }
    void ICommand.Execute(object parameter)
    {
      _cts.Cancel();
      RaiseCanExecuteChanged();
    }
  }
}

Adicional um botão Cancel (e um rótulo de cancelado) à IU é fácil, como mostra a Figura 12.

Figura 12 Adicionando um botão de cancelamento

<Grid>
  <TextBox Text="{Binding Url}" />
  <Button Command="{Binding CountUrlBytesCommand}" Content="Go" />
  <Button Command="{Binding CountUrlBytesCommand.CancelCommand}" Content="Cancel" />
  <Grid Visibility="{Binding CountUrlBytesCommand.Execution,
    Converter={StaticResource NullToVisibilityConverter}}">
    <!--Busy indicator-->
    <Label Content="Loading..."
      Visibility="{Binding CountUrlBytesCommand.Execution.IsNotCompleted,
      Converter={StaticResource BooleanToVisibilityConverter}}" />
    <!--Results-->
    <Label Content="{Binding CountUrlBytesCommand.Execution.Result}"
      Visibility="{Binding CountUrlBytesCommand.Execution.IsSuccessfullyCompleted,
      Converter={StaticResource BooleanToVisibilityConverter}}" />
    <!--Error details-->
    <Label Content="{Binding CountUrlBytesCommand.Execution.ErrorMessage}"
      Visibility="{Binding CountUrlBytesCommand.Execution.IsFaulted,
      Converter={StaticResource BooleanToVisibilityConverter}}" Foreground="Red" />
    <!--Canceled-->
    <Label Content="Canceled"
      Visibility="{Binding CountUrlBytesCommand.Execution.IsCanceled,
      Converter={StaticResource BooleanToVisibilityConverter}}" Foreground="Blue" />
  </Grid>
</Grid>

Agora, se você executar o aplicativo (AsyncCommands4 no código de exemplo), vai encontrar o botão de cancelamento inicialmente desabilitado. Ele é habilitado quando você clica no botão Go e permanece habilitado até que a operação se complete (seja com sucesso, falha ou cancelamento). Agora você tem uma IU completa para uma operação assíncrona.

Uma simples fila de trabalho

Até agora, eu foquei na IU para apenas uma operação de cada vez. Não é necessário mais do que isso em muitas situações, mas às vezes você precisa da capacidade de iniciar mais do que uma operação assíncrona. Na minha opinião, como comunidade, nós ainda não inventamos uma experiência de usuário realmente boa para manipular mais do que uma operação assíncrona. Duas abordagens comuns são usar uma fila de trabalho ou um sistema de notificação, apesar de nenhum dos dois ser ideal.

Uma fila de trabalho exibe todas as operações assíncronas em uma coleção; isso dá ao usuário máxima visibilidade e controle, mas geralmente é muito complexo para o usuário típico. Um sistema de notificação esconde as operações enquanto elas estão sendo executadas e pula se alguma delas falhar (possivelmente também se elas tiverem sucesso). Um sistema de notificação é mais amigável, mas não oferece o poder e a visibilidade completos da fila de trabalho (por exemplo, é difícil fazer funcionar cancelamento em um sistema baseado em notificações). Eu ainda não descobri uma experiência de usuário ideal para múltiplas operações assíncronas.

Dito isso, o código de exemplo neste ponto ponde ser estendido para suportar um cenário de múltiplas operações sem muitos problemas. No código existente, o botão Go e o botão Cancel estão ambos conceitualmente relacionados a uma única operação assíncrona. A nova IU vai alterar o botão Go para significar “inicie uma nova operação assíncrona e adicione-a à lista de operações”. Isso significa que agora o botão Go é síncrono, na verdade. Eu adicionei um simples DelegateCommand (síncrono) à solução, e agora o ViewModel e o XAML podem ser atualizados, conforme mostram a Figura 13 e a Figura 14.

Figura 13 ViewModel para múltiplos comandos

public sealed class CountUrlBytesViewModel
{
  public CountUrlBytesViewModel(MainWindowViewModel parent, string url,
    IAsyncCommand command)
  {
    LoadingMessage = "Loading (" + url + ")...";
    Command = command;
    RemoveCommand = new DelegateCommand(() => parent.Operations.Remove(this));
  }
  public string LoadingMessage { get; private set; }
  public IAsyncCommand Command { get; private set; }
  public ICommand RemoveCommand { get; private set; }
}
public sealed class MainWindowViewModel : INotifyPropertyChanged
{
  public MainWindowViewModel()
  {
    Url = "http://www.example.com/";
    Operations = new ObservableCollection<CountUrlBytesViewModel>();
    CountUrlBytesCommand = new DelegateCommand(() =>
    {
      var countBytes = new AsyncCommand<int>(token =>
        MyService.DownloadAndCountBytesAsync(
        Url, token));
      countBytes.Execute(null);
      Operations.Add(new CountUrlBytesViewModel(this, Url, countBytes));
    });
  }
  public string Url { get; set; } // Raises PropertyChanged
  public ObservableCollection<CountUrlBytesViewModel> Operations
    { get; private set; }
  public ICommand CountUrlBytesCommand { get; private set; }
}

Figura 14 XAML para múltiplos comandos

<Grid>
  <TextBox Text="{Binding Url}" />
  <Button Command="{Binding CountUrlBytesCommand}" Content="Go" />
  <ItemsControl ItemsSource="{Binding Operations}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <Grid>
          <!--Busy indicator-->
          <Label Content="{Binding LoadingMessage}"
            Visibility="{Binding Command.Execution.IsNotCompleted,
            Converter={StaticResource BooleanToVisibilityConverter}}" />
          <!--Results-->
          <Label Content="{Binding Command.Execution.Result}"
            Visibility="{Binding Command.Execution.IsSuccessfullyCompleted,
            Converter={StaticResource BooleanToVisibilityConverter}}" />
          <!--Error details-->
          <Label Content="{Binding Command.Execution.ErrorMessage}"
            Visibility="{Binding Command.Execution.IsFaulted,
            Converter={StaticResource BooleanToVisibilityConverter}}"
            Foreground="Red" />
          <!--Canceled-->
          <Label Content="Canceled"
            Visibility="{Binding Command.Execution.IsCanceled,
            Converter={StaticResource BooleanToVisibilityConverter}}"
            Foreground="Blue" />
          <Button Command="{Binding Command.CancelCommand}" Content="Cancel" />
          <Button Command="{Binding RemoveCommand}" Content="X" />
        </Grid>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</Grid>

Esse código é equivalente ao projeto AsyncCommandsWithQueue, no código de exemplo. Quando o usuário clica no botão Go, um novo AsyncCommand é criado e encapsulado por um ViewModel filho (CountUrlBytesViewModel). Essa instância filho do ViewModel é então adicionada à lista de operações. Tudo associado àquela operação específica (os diversos rótulos e o botão Cancel) é exibido em um modelo de dados para a fila de serviço. E também adicionei um simples botão “X”, que removerá o item da fila.

Essa é a uma fila de trabalho bem básica, e eu fiz algumas suposições sobre o design. Por exemplo, quando uma operação é removida da fila, ela não é automaticamente cancelada. Quando você começar a trabalhar com múltiplas operações assíncronas, recomendo que você se faça ao menos estas perguntas adicionais:

  1. como o usuário vai saber qual notificação ou item de trabalho é para qual operação? (Por exemplo, o indicador de ocupado nesse exemplo de fila de trabalho contém a URL que está baixando.)
  2. o usuário precisa saber de todos os resultados? (Por exemplo, pode ser aceitável notificar o usuário apenas dos erros, ou automaticamente remover operações bem-sucedidas da fila.)

Conclusão

Não há uma solução universal para comandos assíncronos que se encaixe nas necessidades de todos — ainda. A comunidade de desenvolvimento ainda está explorando padrões de interfaces de usuário assíncronas. Meu objetivo neste artigo é mostrar como pensar sobre comandos assíncronos no contexto de um aplicativo MVVM, especialmente considerando questões de experiência de usuário que devem ser respondidas quando a IU se torna assíncrona. Mas lembre-se que os padrões e os códigos de exemplo neste artigo são apenas padrões, e devem ser adaptados às necessidades do aplicativo.

Particularmente, lembre-se que não há uma história perfeita no que diz respeito a múltiplas operações assíncronas. Há lados negativos tanto na abordagem de filas de trabalho quanto de notificações, e me parece que uma experiência de usuário universal ainda não foi desenvolvida. À medida que mais IUs se tornam assíncronas, muito mais cabeças começarão a pensar nesse problema, e uma descoberta revolucionária pode estar bem próxima. Pense um pouco problema, caro leitor. Talvez seja você o descobridor de uma nova UX.

Enquanto isso, você ainda precisa entregar. Neste artigo, eu iniciei com a mais básica implementação do ICommand assíncrono e fui gradualmente adicionando recursos até chegar em algo adequado para a maior parte dos aplicativos modernos. O resultado também é completamente testável em nível de unidade; como o método async void ICommand.Execute chama apenas o método IAsyncCommand.ExecuteAsync que retorna Task, você pode usar o ExecuteAsync diretamente em seus testes de unidade.

No meu último artigo, desenvolvi o NotifyTaskCompletion<T>, um invólucro vinculador de dados ao redor do Task<T>. Neste, eu mostrei como desenvolver um tipo de AsyncCommand<T>, uma implementação assíncrona do ICommand. No próximo, falarei sobre serviços assíncronos. Não se esqueça que padrões MVVM assíncronos ainda são coisa nova; não tenha medo de desviar do que eles propõem e inovar em suas próprias soluções.

Stephen Cleary é marido, pai e programador que mora no norte de Michigan. Ele trabalha com multithreading e programação assíncrona há 16 anos e tem usado o suporte assíncrono no Microsoft .NET Framework desde o primeiro CTP. Seu site, incluindo seu blog, é stephencleary.com.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: James McCaffrey e Stephen Toub