Compartir a través de


Programación asincrónica

Patrones para aplicaciones MVVM asincrónicas: comandos

Stephen Cleary

Descargar el código de ejemplo

Este es el segundo artículo de una serie sobre la combinación de async y await con el patrón Model-View-ViewModel (MVVM) establecido. La vez pasada mostré cómo realizar el enlace a datos a una operación asincrónica y desarrollé un tipo clave llamado NotifyTaskCompletion<TResult> que actuaba como un Task<TResult> apto para el enlace de datos (ver msdn.microsoft.com/magazine/dn605875). Ahora lo convertiré en ICommand, una interfaz de .NET que emplean las aplicaciones MVVM para definir una operación de usuario (que frecuentemente se enlaza por datos a un botón), y consideraré las implicaciones de crear un ICommand asincrónico.

De seguro que estos patrones no se ajustan perfectamente a todas las situaciones, así que puede adaptarlos con toda libertad para sus propias necesidades. De hecho, todo este artículo se presenta como una serie de mejoras para un tipo de comando asincrónico. Al final de las iteraciones, terminaremos con una aplicación como la que se aprecia en la figura 1. Esta se parece a la aplicación que desarrollé en el último artículo, pero esta vez le entrego al usuario un comando que este puede ejecutar. Cuando el usuario hace clic en el botón Go, la URL se lee desde el cuadro de texto y la aplicación cuenta el número de bytes que hay en esa URL (después de una demora artificial). Mientras la operación se encuentra en curso, el usuario no puede iniciar otra, pero puede cancelarla.

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 Una aplicación que puede ejecutar un comando

Luego mostraré cómo usar un método muy similar para crear una cantidad arbitraria de operaciones. En la figura 2 vemos la aplicación modificada para que el botón Go represente la adición de una operación a una colección de operaciones.

An Application Executing Multiple Commands
Figura 2 Una aplicación que ejecuta varios comandos

Haré varias simplificaciones durante el desarrollo de esta aplicación, para concentrarme en los comandos asincrónicos en vez de los detalles de la implementación. Primero que nada, no voy a usar parámetros de ejecución de comandos. No he usado parámetros en las aplicaciones casi nunca en la vida diaria; pero si usted los necesita, los patrones que se encuentran en este artículo se pueden extender fácilmente para incluirlos. Segundo, no implemento ICommand.CanExecuteChanged por mi propia cuenta. Los eventos estándar parecidos a campos generan pérdidas de memoria en algunas aplicaciones MVVM (ver bit.ly/1bROnVj). Para no enredar demasiado el código, uso CommandManager que viene integrado en Windows Presentation Foundation (WPF) para implementar CanExecuteChanged.

También empleo un “nivel de servicios” que, por ahora, no es más que un único método estático, tal como se aprecia en la figura 3. Es esencialmente el mismo servicio del último artículo, pero ampliado para permitir la operación de cancelación. En el siguiente artículo abordaré el diseño correcto de un servicio asincrónico, pero por ahora basta con el servicio simplificado.

Figura 3 El nivel de servicios

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 asincrónicos

Antes de comenzar, eche una mirada rápida a la interfaz ICommand:

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

Haga caso omiso de CanExecuteChanged y los parámetros, y piense un poco sobre cómo funcionaría un comando asincrónico con esta interfaz. El método CanExecute debe ser sincrónico; el único miembro que puede ser asincrónico es Execute. El método Execute se diseñó para las implementaciones sincrónicas, así que devuelve void. Tal como mencioné en un artículo previo, “Procedimientos recomendados para la programación asincrónica” (msdn.microsoft.com/magazine/jj991977), hay que evitar los métodos async void, a menos que sean controladores de eventos (o el equivalente lógico de un controlador de eventos). Las implementaciones de ICommand.Execute tienen la lógica de un controlador de eventos y, por lo tanto, pueden ser async void.

Sin embargo, lo mejor es minimizar el código dentro de los métodos async void y exponer un método async Task en su lugar, que contenga la lógica propiamente tal. De este modo, el código se presta mejor para las pruebas. Aclarado este punto, propongo la siguiente una interfaz de comando asincrónica y el código de la figura 4 como la clase base:

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

Figura 4 Tipo base para los comandos asincrónicos

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();
  }
}

La clase base se hace cargo de dos cosas: despacha la implementación de CanExecuteChanged a la clase CommandManager e implementa el método async void ICommand.Execute al llamar el método IAsyncCommand.ExecuteAsync. Espera el resultado para asegurarse de que cualquier excepción en la lógica del comando asincrónico se derive correctamente al bucle principal del subproceso de la interfaz de usuario.

Esto es bastante complejo, pero cada uno de estos tipos tiene una finalidad. IAsyncCommand se puede usar para cualquier implementación asincrónica de ICommand y tiene la finalidad de ser expuesta desde los modelos de vista y consumido por la vista y las pruebas unitarias. AsyncCommandBase se encarga de parte del código repetitivo que es común a todos los ICommands asincrónicos.

Ahora que tengo lista la obra preliminar, estoy preparado para comenzar a desarrollar un comando asincrónico eficaz. El tipo delegado estándar para una operación sincrónica que no devuelve ningún valor es Action. El equivalente asincrónico es Func<Task>. En la figura 5 vemos mi primera iteración de un AsyncCommand basado en un delegado.

Figura 5 Primer intento de un comando asincrónico

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();
  }
}

A estas alturas, la interfaz de usuario solo tiene un cuadro de texto para la URL, un botón para iniciar la solicitud HTTP y una etiqueta para los resultados. El XAML y las partes esenciales del modelo de vista son muy sencillos. Este es Main­Window.xaml (se omitieron los atributos de posición como, por ejemplo, Margin):

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

MainWindowViewModel.cs se muestra en la figura 6.

Figura 6 El primer 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
}

Si ejecuta la aplicación (AsyncCommands1 en el código de ejemplo adjunto), verá que hay cuatro casos que tienen un comportamiento poco elegante. Primero, la etiqueta siempre muestra un resultado, incluso antes de que hagamos clic en el botón. Segundo, después de hacer clic no aparece ningún indicador de ocupado para señalar que la operación está en curso. Tercero, si la solicitud HTTP genera un error, la excepción se pasa al bucle principal de la interfaz de usuario, lo que produce que la aplicación se bloquee. Cuarto, si el usuario realiza varias solicitudes no puede distinguir entre los diferentes resultados: los resultados de una solicitud anterior podrían sobrescribir los resultados de una solicitud posterior, si el servidor presenta tiempos de respuesta desiguales.

¡Esto es un montón de problemas! Pero antes de comenzar a iterar el diseño, reflexionemos un momento sobre los tipos de problemas que mencionamos. Cuando una interfaz de usuario se convierte en asincrónica, nos vemos obligados a pensar en estados adicionales de la interfaz de usuario. Yo le recomiendo que se haga al menos las siguientes preguntas:

  1. ¿Cómo se presentarán los errores en la interfaz de usuario? (¡Espero que su interfaz de usuario ya tenga una respuesta a esta pregunta!)
  2. ¿Cómo debe verse la interfaz de usuario mientras la operación está en curso? (Por ejemplo, ¿entrega una respuesta inmediata mediante un indicador de ocupado?)
  3. ¿De qué manera se restringe al usuario mientras la operación está en curso? (¿Se deshabilitan los botones, por ejemplo?)
  4. ¿El usuario tiene comandos adicionales disponibles mientras la operación está en curso? (Por ejemplo, ¿puede cancelar la operación?)
  5. Si el usuario puede iniciar varias operaciones, ¿cómo proporciona la interfaz de usuario la información sobre la finalización o sobre los posibles errores para cada una? (Por ejemplo, ¿la interfaz de usuario emplea un estilo de “cola de comandos” o notificaciones emergentes?)

Control de la finalización de los comandos asincrónicos mediante enlace de datos

La mayoría de los problemas en la primera iteración de Async­Command tienen que ver con cómo se procesan los resultados. Lo que necesitamos realmente es algún tipo que encapsule un Task<T> y entregue ciertas funciones de enlace de datos para que la aplicación pueda responder de manera más elegante. Da la casualidad que el tipo NotifyTaskCompletion<T> que desarrollé en el último artículo cumple con estos requisitos casi a la perfección. Le agregaré un miembro para simplificar parte de la lógica de Async­Command: una propiedad TaskCompletion que representa la finalización de la operación pero que no propaga las excepciones (ni devuelve un resultado). Estas son las modificaciones en NotifyTaskCompletion<T>:

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

La siguiente iteración de AsyncCommand usa NotifyTaskCompletion para representar la operación misma. Al hacer esto, el XAML puede enlazar por datos directamente al resultado y al mensaje de error de esa operación y también puede usar el enlace de datos para mostrar un mensaje adecuado mientras la operación está en curso. El nuevo AsyncCommand ahora tiene una propiedad que representa la operación misma, tal como se aprecia en la figura 7.

Figura 7 Segundo intento de un comando asincrónico

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

Observe que AsyncCommand.ExecuteAsync usa TaskCompletion en vez de Task. No quiero propagar las excepciones al bucle principal de la interfaz de usuario, (lo que ocurriría si esperara la propiedad Task); en vez de esto, devuelvo TaskCompletion y controlo las excepciones mediante enlace de datos. También agregué un NullToVisibilityConverter simple al proyecto para que el indicador de ocupado, los resultados y los mensajes de error queden ocultos hasta que el usuario haga clic en el botón. En la figura 8 vemos el código actualizado del modelo de vista.

Figura 8 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; }
}

Y el código nuevo de XAML se muestra en la figura 9.

Figura 9 XAML del segundo MainWindow

<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>

El código ahora coincide con el proyecto AsyncCommands2 del código de ejemplo. Este código se ocupa de todos los problemas que mencioné en la solución original: las etiquetas se ocultan hasta que comience la primera operación, hay un indicador que aparece inmediatamente para mostrarle al usuario que la aplicación está ocupada, las excepciones se atajan y actualizan la interfaz de usuario mediante enlace de datos y ya no hay interferencia entre las diferentes solicitudes. Cada solicitud crea un nuevo contenedor de NotifyTaskCompletion, que tiene su propio Result y otras propiedades independientes. NotifyTaskCompletion actúa como una abstracción de una operación asincrónica y que se puede enlazar por datos. Esto permite realizar varias solicitudes, y la interfaz de usuario siempre enlaza a la última solicitud. Sin embargo, en muchas situaciones de la vida real, la solución adecuada es impedir las solicitudes múltiples. Es decir, queremos que el comando devuelva false desde CanExecute mientras hay una operación en curso. Esto se puede hacer muy fácilmente con una pequeña modificación en AsyncCommand, tal como se aprecia en la figura 10.

Figura 10 Impedir las solicitudes múltiples

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();
  }
}

El código ahora coincide con el proyecto AsyncCommands3 del código de ejemplo. El botón se desactiva mientras la operación está en curso.

Agregar la funcionalidad de cancelación

Muchas operaciones asincrónicas pueden demorar una cantidad de tiempo variable. Por ejemplo, una solicitud HTTP puede responder muy rápido en condiciones normales, incluso antes de que el usuario tenga tiempo para responder. Pero si la red está lenta o si el servidor está ocupado, la misma solicitud HTTP puede generar una demora considerable. Parte de diseñar una interfaz de usuario asincrónica es estar preparado para esa situación y adaptar el diseño para esta misma. La solución actual ya tiene un indicador de ocupado. Cuando diseñamos una interfaz de usuario asincrónica, también podemos optar por darle al usuario más alternativas, y la cancelación es una opción común.

La cancelación misma es siempre una operación sincrónica: el hecho de solicitar la cancelación es inmediata. La parte más difícil de la cancelación es en qué momento se puede ejecutar; debería ser solo cuando hay un comando asincrónico en curso. Las modificaciones en AsyncCommand de la figura 11 entregan un comando de cancelación anidado y notifican a ese comando de cancelación en qué momento comienza y termina el comando asincrónico.

Figura 11 Adición de la funcionalidad de cancelación

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();
    }
  }
}

Agregar un botón de cancelación, (y la etiqueta correspondiente) a la interfaz de usuario es muy simple, como se puede apreciar en la figura 12.

Figura 12 Adición de un botón para cancelar

<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>

Ahora si ejecuta la aplicación (AsyncCommands4 en el código de ejemplo), verá que el botón de cancelación está deshabilitado al principio. Se habilita al hacer clic en el botón Go y permanece así hasta el final de la operación (ya sea una devolución correcta, con error o si se cancela). Ahora tenemos una interfaz de usuario razonablemente completa para una operación asincrónica.

Una cola de trabajo sencilla

Hasta este punto, me he enfocado en una interfaz de usuario que sirve para una sola operación a la vez. Esto es todo lo que necesitamos en muchas situaciones, pero a veces necesitamos iniciar varias operaciones asincrónicas a la vez. Yo creo que como comunidad no hemos encontrado todavía una experiencia de usuario realmente buena para procesar múltiples operaciones asincrónicas. Hay dos métodos comunes que emplean una cola de trabajo o un sistema de notificación y ambos distan de ser ideales.

Una cola de trabajo muestra todas las operaciones asincrónicas en una colección; esto le entrega al usuario la máxima visibilidad y control, pero generalmente es demasiado complejo para el usuario final típico. Un sistema de notificación oculta las operaciones mientras que estas se ejecutan y emerge si cualquiera de ellas genera un error (y posiblemente si se terminan correctamente). Un sistema de notificación es más amigable pero no proporciona la visibilidad completa y el poder de una cola de trabajo (por ejemplo, resulta difícil integrar la cancelación en un sistema basado en notificaciones). Todavía no he encontrado una experiencia de usuario ideal para el caso de varias operaciones asincrónicas.

Aclarado esto, el código de ejemplo de este punto se puede extender para funcionar con múltiples operaciones sin demasiados problemas. En el código existente, el botón Go y el botón Cancel están relacionados conceptualmente con una sola operación asincrónica. La interfaz de usuario nueva cambiará el botón Go para significar “iniciar una nueva operación asincrónica y agregarla al listado de las operaciones”. Lo que significa esto es que el botón Go mismo es ahora sincrónico. Agregué un DelegateCommand simple (sincrónico) a la solución y ahora el ViewModel y XAML se pueden actualizar, tal como se aprecia en la figura 13 y la figura 14.

Figura 13 ViewModel para múltiples 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últiples 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>

Este código es equivalente al proyecto AsyncCommandsWithQueue en el código de ejemplo. Cuando el usuario hace clic en el botón Go, se crea un nuevo AsyncCommand y este se encapsula dentro de un ViewModel secundario (CountUrlBytesViewModel). Esta instancia secundaria del modelo de vista se agrega luego a la lista de operaciones. Todo lo que se asocia con esta operación puntual (las diferentes etiquetas y el botón Cancel) se muestra en una plantilla de datos para la cola de trabajo. También agregué un simple botón “X” que eliminará el elemento de la cola.

Esta es una cola de trabajo bastante básica, e hice algunas suposiciones acerca del diseño. Por ejemplo, al eliminar una operación de la cola, esta no se cancela automáticamente. Cuando comience a trabajar con operaciones asincrónicas múltiples, le recomiendo que se haga al menos las siguientes preguntas adicionales:

  1. ¿Cómo sabe el usuario qué notificación o elemento de trabajo es para qué operación? (Por ejemplo, el indicador de ocupado en esta cola de trabajo contiene la URL que está descargando).
  2. ¿El usuario tiene que ver todos los resultados? (Por ejemplo, puede ser aceptable notificar al usuario solo de los errores o eliminar automáticamente las operaciones que finalizaron correctamente de la cola de trabajo).

En resumen

No hay ninguna solución universal para un comando asincrónico que satisfaga las necesidades de todos… aún. La comunidad de desarrolladores está explorando todavía los patrones para las interfaces de usuario asincrónicas. Mi objetivo en este artículo es mostrarle cómo pensar sobre los comandos asincrónicos dentro del contexto de las aplicaciones MVVM, especialmente teniendo en cuenta los problemas de la experiencia de usuario que hay que abordar cuando la interfaz de usuario se hace asincrónica. Pero recuerde que los patrones de este artículo y el código de ejemplo son solo patrones y se deben adaptar a las necesidades de cada aplicación.

En concreto, no hay ningún relato perfecto para las operaciones asincrónicas múltiples. Tanto las colas de trabajo como las notificaciones tienen desventajas, y me parece que falta encontrar todavía una experiencia de usuario universal. A medida que más y más interfaces de usuario se conviertan en asincrónicas, habrá cada vez más mentes dedicadas a este problema y es posible que nos espere alguna idea revolucionaria a la vuelta de la esquina. Reflexione sobre este problema, estimado lector. Quizás logre descubrir una nueva experiencia de usuario.

En el intertanto, tiene que seguir publicando código. En este artículo comencé con la implementación asincrónica de ICommand más básica y agregué gradualmente nuevas funciones hasta que terminé con algo bastante adecuado para la mayoría de las aplicaciones modernas. El resultado también se presta para las pruebas unitarias automatizadas; como el método async void ICommand.Execute solo llama el método IAsyncCommand.ExecuteAsync que devuelve Task, podemos usar ExecuteAsync directamente en las pruebas.

En mi último artículo desarrollé NotifyTaskCompletion<T>, un contenedor de enlace de datos en torno a Task<T>. En este, mostré cómo desarrollar un tipo de AsyncCommand<T>, una implementación asincrónica de ICommand. En el siguiente artículo veré los servicios asincrónicos. Tenga en mente que los patrones MVVM asincrónicos son bastante nuevos; desvíese de estos sin miedo y encuentre sus propias soluciones innovadoras.

Stephen Cleary es esposo, padre, programador y vive en el norte de Michigan. Trabaja en la programación multithreading y asincrónica desde hace 16 años y ha usado las funciones asincrónicas de Microsoft .NET Framework desde la primera CTP. Su página principal y su blog se encuentran en stephencleary.com.

Gracias a los siguientes expertos técnicos de Microsoft por su ayuda en la revisión de este artículo: James McCaffrey y Stephen Toub