Partager via


Cet article a fait l'objet d'une traduction automatique.

Programmation asynchrone

Modèles pour les applications MVVM asynchrones : Commandes

Stephen Cleary

Télécharger l'exemple de code

C'est le deuxième article d'une série sur la combinaison async et attendent établies sur le modèle modèle-vue-ViewModel MVVM. La dernière fois, j'ai montré comment de données lier à une opération asynchrone, et j'ai développé un type de clé appelé NotifyTaskCompletion < TResult > qui a agi comme une données liaison facile tâche < TResult > (voir msdn.microsoft.com/magazine/dn605875). Maintenant, je vais tourner à ICommand, une interface .NET utilisée par les applications MVVM pour définir une opération de l'utilisateur (qui est souvent lié aux données d'un bouton), et je vais envisager les incidences d'un ICommand asynchrone.

Les patrons ici ne peut-être pas parfaitement tous les scénarios, alors n'hésitez pas à les ajuster à vos besoins. En effet, cet article complet est présenté comme une série d'améliorations sur un type de commande asynchrone. À la fin de ces itérations, vous vous retrouverez avec une application comme ce qui est montré dans Figure 1. Ceci est similaire à l'application développée dans mon dernier article, mais cette fois j'ai fournir à l'utilisateur avec une commande réelle d'exécution. Lorsque l'utilisateur clique sur le bouton Go, l'URL est lue à partir de la zone de texte et l'application comptera le nombre d'octets à cette URL (après un délai artificiel). Alors que l'opération est en cours, l'utilisateur ne peut pas commencer un autre, mais il peut annuler l'opération.

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
La figure 1, une Application qui peut exécuter une commande

Ensuite, je vais montrer comment une approche très similaire peut servir à créer un nombre illimité d'opérations. La figure 2 illustre l'application modifiée de sorte que le bouton Go représente l'ajout d'une opération à un ensemble d'opérations.

An Application Executing Multiple Commands
Figure 2 une Application exécute plusieurs commandes

Il y a un couple de simplifications, que je vais faire lors de l'élaboration de la présente demande, de mettre l'accent sur les commandes asynchrones au lieu de détails d'implémentation. Tout d'abord, je ne l'emploierai paramètres d'exécution de commande. J'ai presque jamais besoin d'utiliser des paramètres dans les applications réelles ; mais si vous en avez besoin, les patrons dans cet article peuvent facilement être étendus pour les inclure. Deuxièmement, je n'implémentent ICommand.CanExecuteChanged moi-même. Un événement de type champ standard provoquera une fuite de mémoire sur certaines plates-formes MVVM (voir bit.ly/1bROnVj). Pour simplifier le code, j'ai utiliser le gestionnaire de commandes intégré Windows Presentation Foundation (WPF) pour implémenter CanExecuteChanged.

Je suis également en utilisant un simplifié « couche de service », qui pour l'instant est juste une seule méthode statique, comme le montre Figure 3. C'est essentiellement le même service que dans mon dernier article, mais étendu à charge l'annulation. Le prochain article portera sur la conception de service asynchrone adéquat, mais pour l'instant ce service simplifié va faire.

Figure 3 la couche de Service

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

Commandes asynchrones

Avant de commencer, prendre un coup de œil à l'interface ICommand :

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

Ignorer CanExecuteChanged et les paramètres et penser pour un peu comment une commande asynchrone travaillerait avec cette interface. La méthode CanExecute doit être synchrone ; le seul membre qui peut être asynchrone est Execute. La méthode Execute a été conçue pour les implémentations synchrones, donc il retourne void. Comme je le disais dans un précédent article, « Les meilleures pratiques en programmation asynchrone » (msdn.microsoft.com/magazine/jj991977), async méthodes void devraient être évitées sauf si ils ont des gestionnaires d'événements (ou l'équivalent logique­alent de gestionnaires d'événements). Les implémentations de ICommand.Execute sont logiquement des gestionnaires d'événements et, ainsi, peut-être async Sub.

Toutefois, il est mieux pour réduire au minimum le code dans une méthode void async et exposer un méthode spéciale d'async plutôt qui contient la logique réelle. Cette pratique rend le code plus testable. Dans cette optique, je propose ce qui suit comme une interface de commande asynchrone et le code dans Figure 4 comme classe de base :

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

Type de Base de la figure 4 pour les commandes asynchrones

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 classe de base s'occupe de deux choses : Il punts la mise en œuvre de CanExecuteChanged hors de la classe CommandManager ; et il implémente la méthode async de ICommand.Execute void en appelant la méthode IAsyncCommand.ExecuteAsync. Il attend le résultat afin de s'assurer que toutes les exceptions dans la logique de commande asynchrone passera correctement à la boucle principale du thread de l'interface utilisateur.

Il s'agit d'une bonne dose de complexité, mais chacun de ces types a un but. IAsyncCommand peut être utilisé pour n'importe quelle implémentation d'ICommand asynchrone et est destiné à être exposé de ViewModels et consommé par la vue et de tests unitaires. AsyncCommandBase gère certains d'entre le code réutilisable commun commun à tous les ICommands asynchrones.

Avec ce terrain en place, je suis prêt à commencer à développer une commande asynchrone efficace. Le type délégué standard pour un fonctionnement synchrone sans valeur de retour est Action. L'équivalent asynchrone est Func < Task >. Figure 5 montre ma première itération d'un AsyncCommand reposant sur les délégués.

Figure 5 la première tentative à une commande asynchrone

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

À ce stade, l'interface utilisateur a uniquement une zone de texte de l'URL, une touche pour lancer la requête HTTP et une étiquette pour les résultats. Le code XAML et les parties essentielles du ViewModel sont simples. Voici les principales­Window.xaml (sauter les attributs de positionnement comme marge) :

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

MainWindowViewModel.cs s'affiche dans Figure 6.

Figure 6 le premier 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 vous exécutez l'application (AsyncCommands1 dans le téléchargement de code échantillon), vous pouvez remarquer que quatre cas de comportement inélégant. Tout d'abord, l'étiquette indique toujours un résultat, avant même que le bouton est cliqué. Deuxièmement, il n'y a aucun indicateur occupé après avoir cliqué sur le bouton pour indiquer que l'opération est en cours. Troisièmement, si le HTTP demande des failles, l'exception est passée à la boucle principale de l'interface utilisateur, provoquant un plantage de l'application. Quatrièmement, si l'utilisateur effectue plusieurs demandes, elle ne peut pas distinguer les résultats ; Il est possible pour les résultats d'une demande antérieure d'écraser les résultats d'une requête ultérieure en raison des temps de réponse de serveur.

Il s'agit de tout un tas de problèmes ! Mais avant que j'ai effectuer une itération de la conception, considérons un instant la nature des questions soulevées. Lorsqu'une interface utilisateur devient asynchrone, il vous oblige à penser à des États supplémentaires dans votre interface utilisateur. Je recommande que vous vous posez au moins ces questions :

  1. Comment l'interface utilisateur affichera des erreurs ? (J'espère que votre interface utilisateur synchrone a déjà une réponse pour celui-ci!)
  2. Comment l'interface utilisateur doit rechercher si l'opération est en cours ? (Par exemple, il fournira une rétroaction immédiate via des indicateurs bien remplies?)
  3. En quoi l'utilisateur restreint alors que l'opération est en cours ? (Sont boutons désactivés, par exemple?)
  4. L'utilisateur a-t-il les commandes supplémentaires disponibles alors que l'opération est en cours ? (Par exemple, il peut annuler l'opération?)
  5. Si l'utilisateur peut lancer plusieurs opérations, comment l'interface utilisateur fournit-il achèvement ou les détails de l'erreur pour chacun d'eux ? (Par exemple, l'interface utilisateur utilise un popups de style ou de la notification de « file d'attente de commandes »?)

Achèvement de la commande asynchrone via la liaison de données de manutention

La plupart des problèmes dans la première Async­itération de commande se rapportent à la façon dont les résultats sont gérés. Ce qui est vraiment nécessaire est une sorte de type d'emballerais une tâche < T > et fournir des fonctionnalités de liaison de données afin que l'application peut répondre avec plus d'élégance. En l'occurrence, la NotifyTaskCompletion < T > type développé dans mon dernier article correspond presque parfaitement à ces besoins. Je vais ajouter un membre à ce type qui simplifie certaines de la Async­logique de commande : une propriété TaskCompletion qui représente l'opération remplir mais ne propage d'exception (ou retourner un résultat). Voici les modifications à NotifyTaskCompletion < T > :

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

La prochaine itération de AsyncCommand utilise NotifyTaskCompletion pour représenter le fonctionnement proprement dit. Ce faisant, le code XAML peut lier des données directement sur le message d'erreur et le résultat de cette opération, et il peut aussi utiliser la liaison de données pour afficher un message approprié, alors que l'opération est en cours. Le nouveau AsyncCommand a maintenant une propriété qui représente l'opération réelle, comme le montre Figure 7.

Figure 7 la deuxième tentative à une commande asynchrone

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

Notez que AsyncCommand.ExecuteAsync utilise TaskCompletion et pas de tâche. Je ne veux pas propager des exceptions à la boucle principale de l'interface utilisateur (ce qui se passerait si elle attendait la propriété Task) ; au lieu de cela, j'ai retourner TaskCompletion et gèrent les exceptions par liaison de données. J'ai aussi ajouté un NullToVisibilityConverter simple au projet afin que le voyant busy, les résultats et les messages d'erreur sont tous cachés jusqu'à ce que le bouton est cliqué. Figure 8 montre le code mis à jour du ViewModel.

Figure 8 la deuxième 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; }
}

Et le nouveau code XAML est affiché dans Figure 9.

Figure 9 la deuxième XAML 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>

Le code correspond maintenant le projet AsyncCommands2 dans l'exemple de code. Ce code prend soin de toutes les préoccupations que j'ai mentionné avec la solution d'origine : étiquettes sont cachés jusqu'à ce que la première opération commence ; Il y a un indicateur occupé immédiat rétro-information à l'utilisateur ; exceptions sont capturées et mettre à jour l'interface utilisateur via la liaison de données ; plusieurs demandes n'est plus interfèrent entre eux. Chaque requête crée un nouvel emballage de NotifyTaskCompletion, qui a son propre résultat indépendant et autres propriétés. NotifyTaskCompletion agit comme une abstraction de données pouvant être lié d'une opération asynchrone. Cela permet à plusieurs demandes, avec l'interface utilisateur lie toujours à la dernière demande. Toutefois, dans de nombreux scénarios réels, la solution appropriée est de désactiver les demandes multiples. Autrement dit, vous voulez que la commande pour retourner false de CanExecute, bien qu'il y a une opération en cours. C'est assez facile à faire avec une petite modification à AsyncCommand, comme le montre Figure 10.

Figure 10 désactiver plusieurs demandes

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

Maintenant, le code correspond le projet AsyncCommands3 dans l'exemple de code. Le bouton est désactivé alors que l'opération se passe.

Ajout d'annulation

Nombre d'opérations asynchrones peut prendre des quantités variables de temps. Par exemple, une requête HTTP peut normalement réagir très vite, avant que l'utilisateur peut même répondre. Toutefois, si le réseau est lent ou si le serveur est occupé, cette même requête HTTP pourrait causer un retard considérable. Partie de la conception d'une interface utilisateur asynchrone est attendu et concevoir pour ce scénario. La solution actuelle a déjà un indicateur occupé. Lorsque vous créez une interface utilisateur asynchrone, vous pouvez aussi choisir de donner à l'utilisateur plus d'options, et l'annulation est un choix commun.

Annulation elle-même est toujours une opération synchrone — l'acte de la demande d'annulation est immédiate. La partie la plus délicate de l'annulation est quand il peut être exécuté ; Il devrait être en mesure de n'exécuter qu'une fois il y a une commande asynchrone en cours. Les modifications apportées au AsyncCommand en Figure 11 fournir une commande d'annulation imbriqués et notifier cette commande d'annulation lors de la commande asynchrone commence et se termine.

Figure 11 ajout d'annulation

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

Ajout d'un bouton d'annulation (et une étiquette annulée) à l'interface utilisateur est simple, comme Figure 12 montre.

Figure 12 ajout d'un bouton Annuler

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

Maintenant, si vous exécutez l'application (AsyncCommands4 dans l'exemple de code), vous trouverez le bouton Annuler est initialement désactivé. Il est activé lorsque vous cliquez sur le bouton Go et reste activé jusqu'à ce que l'opération se termine (si avec succès, blâmé ou annulée). Vous avez maintenant une interface d'utilisateur sans doute complet pour une opération asynchrone.

Une file d'attente de travail Simple

Jusqu'à présent, j'ai mis l'accent sur une interface utilisateur pour un service à la fois. C'est tout ce qui est nécessaire dans de nombreuses situations, mais parfois vous avez besoin de la possibilité de lancer plusieurs opérations asynchrones. À mon avis, en tant que communauté nous n'avons pas imaginé un UX vraiment bon pour la manipulation de plusieurs opérations asynchrones. Deux approches communes utilisent une file d'attente de travail ou d'un système de notification, ni de ce qui est idéal.

Une file d'attente de travail affiche toutes les opérations asynchrones dans une collection ; Cela donne la visibilité maximale de l'utilisateur et le contrôle, mais il est généralement trop complexe pour l'utilisateur final typique faire face. Un système de notification cache les opérations tandis qu'ils courent et pop up si un d'eux faute (et, éventuellement, si elles se terminer correctement). Un système de notification est plus facile à utiliser, mais il ne fournit pas la visibilité totale et la puissance de la file d'attente de travail (par exemple, il est difficile de travailler d'annulation dans un système de notification). Je dois encore découvrir un UX idéale pour plusieurs opérations asynchrones.

Cela dit, l'exemple de code à ce stade peut être étendue pour prendre en charge un scénario plusieurs opérations sans trop d'ennui. Dans le code existant, ce bouton et le bouton Cancel sont tous deux conceptuellement liés à une seule opération asynchrone. La nouvelle interface utilisateur changera le bouton Go pour signifier « démarrer une nouvelle opération asynchrone et ajoutez-le à la liste des opérations. » Ce que cela signifie, c'est que ce bouton est maintenant réellement synchrone. J'ai ajouté un DelegateCommand (synchrone) simple à la solution, et maintenant le ViewModel et le XAML peuvent être mis à jour, comme Figure 13 et Figure 14 montrer.

Figure 13 ViewModel pour des commandes multiples

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

Figure 14 XAML pour des commandes multiples

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

Ce code est équivalent au projet AsyncCommandsWithQueue dans l'exemple de code. Lorsque l'utilisateur clique sur le bouton Go, un nouveau AsyncCommand est créé et encapsulé dans un enfant ViewModel (CountUrlBytesViewModel). Cette instance de ViewModel enfant est ensuite ajoutée à la liste des opérations. Tout associé à cette opération particulière (les différents labels et le bouton Annuler) est affiché dans un modèle de données pour la file d'attente de travail. J'ai aussi ajouté un simple bouton « X » qui supprime l'élément de la file d'attente.

Il s'agit d'une file d'attente de travail très basique, et j'ai fait certaines hypothèses au sujet de la conception. Par exemple, lorsqu'une opération est supprimée de la file d'attente, il n'est pas automatiquement annulé. Lorsque vous commencez à travailler avec plusieurs opérations asynchrones, je recommande que vous vous posez au moins ces questions supplémentaires :

  1. Comment l'utilisateur ne sais pas lequel avis ou d'élément de travail est pour quelle opération ? (Par exemple, l'indicateur occupé cet échantillon de file d'attente de travail contient l'URL c'est téléchargement).
  2. L'utilisateur a besoin de connaître chaque résultat ? (Par exemple, il peut être acceptable pour avertir l'utilisateur seulement des erreurs, ou de supprimer automatiquement les opérations réussies de la file d'attente de travail).

Synthèse

Il n'est pas une solution universelle pour une commande asynchrone qui correspond à tous les besoins — encore. La communauté des développeurs est encore à explorer des modèles d'interface utilisateur asynchrones. Mon objectif dans cet article est de montrer comment penser sur les commandes asynchrones dans le contexte d'une demande de MVVM, compte tenu notamment des questions UX qui doivent être abordées lors de l'interface utilisateur devient asynchrone. Mais garder à l'esprit les modèles dans cet article et les exemples de code sont justes motifs et devraient être adaptés aux besoins de l'application.

En particulier, il n'est pas une histoire parfaite concernant plusieurs opérations asynchrones. Il existe des inconvénients à la fois les files d'attente de travail et les notifications, et il me semble qu'un UX universels doit être développée. UIs plus fur asynchrones, esprits beaucoup plus vont penser sur ce problème, et une percée révolutionnaire pourrait être juste autour du coin. Donner le problème quelque pensée, cher lecteur. Peut-être serez-vous le découvreur d'un ux de nouveau.

En attendant, vous avez toujours à expédier. Dans cet article, j'ai commencé avec les plus élémentaires des implémentations ICommand asynchrones et ajouté des fonctionnalités progressivement jusqu'à ce que je me suis retrouvé avec quelque chose d'assez convenable pour les applications plus modernes. Le résultat est aussi pleinement unité-testable ; étant donné que la méthode async de ICommand.Execute Sub appelle uniquement la méthode IAsyncCommand.ExecuteAsync retourne des tâches, vous pouvez utiliser ExecuteAsync directement dans vos tests unitaires.

Dans mon dernier article, j'ai développé NotifyTaskCompletion < T >, un wrapper de la liaison de données autour de tâche < T >. Dans celui-ci, j'ai montré comment développer un genre de AsyncCommand < T >, une implémentation asynchrone de ICommand. Dans mon prochain article, je vais traiter les services asynchrones. Gardez à l'esprit que les modèles MVVM asynchrones sont encore tout à fait nouveau ; n'ayez pas peur de s'écarter d'eux et innover vos propres solutions.

Stephen Cleary est un mari, le père et le programmeur vivant dans le nord du Michigan. Il a travaillé avec multithreading et asynchrone de programmation pendant 16 ans et a utilisé le soutien async dans Microsoft .NET Framework depuis la première version CTP. Sa page d'accueil, y compris son blog, est à stephencleary.com.

Merci aux experts techniques Microsoft suivants d'avoir relu cet article : James McCaffrey et Stephen Toub