Share via


Il presente articolo è stato tradotto automaticamente.

Programmazione asincrona

Modelli per applicazionI MVVM asincrone: comandi

Stephen Cleary

Scaricare il codice di esempio

Questo è il secondo articolo di una serie sulla combinazione async e attendono con il pattern Model-View-ViewModel (MVVM) stabilito. L'ultima volta, ho mostrato come dati associare a un'operazione asincrona, e sviluppato un tipo di chiave chiamato NotifyTaskCompletion < TResult > che ha agito come un dati associazione-amichevole Task < TResult > (vedere msdn.microsoft.com/magazine/dn605875). Ora girerò a ICommand, un'interfaccia .NET utilizzata dalle applicazioni MVVM per definire un'operazione dell'utente (che è spesso associato ai dati di un pulsante), e io considerano le implicazioni di rendere un oggetto ICommand asincrono.

I modelli qui può non perfettamente ogni scenario, quindi sentitevi liberi di sintonizzarsi alle vostre esigenze. Infatti, questo intero articolo è presentato come una serie di miglioramenti su un tipo di comando asincrono. Alla fine di queste iterazioni, vi ritroverete con un'applicazione come quello mostrato nella Figura 1. Questo è simile all'applicazione sviluppata nel mio ultimo articolo, ma questa volta forniscono all'utente un effettivo comando da eseguire. Quando l'utente fa clic sul pulsante Go, l'URL viene letto da textbox e l'applicazione conterà il numero di byte a quell'URL (dopo un ritardo artificiale). Mentre è in corso l'operazione, l'utente non può iniziare un altro, ma egli può annullare l'operazione.

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 un'applicazione che può eseguire un solo comando

Poi ti mostrerò come un approccio molto simile può essere utilizzato per creare qualsiasi numero di operazioni. Figura 2 illustra l'applicazione modificato quindi il pulsante Go rappresenta l'aggiunta di un'operazione a un insieme di operazioni.

An Application Executing Multiple Commands
Nella figura 2, un'applicazione per l'esecuzione di comandi multipli

Ci sono un paio di semplificazioni che ho intenzione di fare durante lo sviluppo di questa applicazione, a mantenere l'attenzione sui comandi asincroni invece dettagli di implementazione. In primo luogo, non voglio usare i parametri di esecuzione del comando. Ho quasi mai avuto bisogno utilizzare parametri nelle applicazioni reali; ma se bisogno di loro, i modelli in questo articolo possono essere facilmente esteso per includerli. In secondo luogo, non implementare ICommand.CanExecuteChanged me stesso. Un evento di tipo campo standard sarà una perdita di memoria su alcune piattaforme MVVM (vedere bit.ly/1bROnVj). Per mantenere il codice semplice, utilizzare CommandManager incorporato Windows Presentation Foundation (WPF) per implementare CanExecuteChanged.

Sto anche utilizzando una semplificata "livello di servizio," che per ora è solo un unico metodo statico, come mostrato Figura 3. È essenzialmente lo stesso servizio, come nel mio ultimo articolo, ma esteso per supportare la cancellazione. Il prossimo articolo tratterà con disegno adeguato servizio asincrono, ma per ora questo servizio semplificato farà.

Figura 3 il livello di servizio

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

Comandi asincroni

Prima di iniziare, una rapida occhiata all'interfaccia ICommand:

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

Ignorare CanExecuteChanged e i parametri e pensare per un po ' come un comando asincrono funzionerebbe con questa interfaccia. Il metodo CanExecute deve essere sincrono; l'unico membro che può essere asincrona è Execute. Il metodo Execute è stato progettato per implementazioni sincrone, pertanto restituisce void. Come accennato in un precedente articolo, "Best Practices in programmazione asincrona" (msdn.microsoft.com/magazine/jj991977), metodi void async dovrebbero essere evitati a meno che siano i gestori eventi (o la logica equiv­momy9 di gestori eventi). Le implementazioni di ICommand sono logicamente i gestori eventi e, quindi, può essere async nulle.

Tuttavia, è meglio per ridurre al minimo il codice all'interno di un metodo void async ed esporre un metodo async invece che contiene la logica effettiva. Questa pratica rende il codice più verificabili. Con questo in mente, vi propongo il seguente come un'interfaccia di comando asincrono e il codice in Figura 4 come classe base:

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

Figura 4 tipo di Base per i comandi asincroni

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 base si prende cura di due cose: Esso punt l'implementazione CanExecuteChanged fuori alla classe CommandManager; e implementa il metodo asincrono void ICommand chiamando il metodo IAsyncCommand.ExecuteAsync. Attende il risultato per garantire che eventuali eccezioni nella logica di comando asincrono verranno generati correttamente al ciclo principale del thread dell'interfaccia utente.

Questa è una discreta quantità di complessità, ma ognuno di questi tipi ha uno scopo. IAsyncCommand può essere utilizzato per qualsiasi implementazione ICommand asincrona ed è destinato a essere esposto da ViewModels e consumati dalla vista e dai test di unità. AsyncCommandBase gestisce alcuni dei comuni codice boilerplate comune a tutti i ICommands asincrona.

Con questo fondamento nel luogo, io sono pronto per iniziare a sviluppare un efficace comando asincrono. Il tipo di delegato standard per un funzionamento sincrono senza un valore restituito è azione. L'equivalente asincrono è Func < Task >. Figura 5 dimostra la mia prima iterazione di un AsyncCommand basato sul delegato.

Figura 5 il primo tentativo di un comando asincrono

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 questo punto, l'interfaccia utente ha solo una casella di testo URL, un pulsante per avviare la richiesta HTTP e un'etichetta per i risultati. XAML e le parti essenziali del ViewModel sono semplici. Ecco le principali­Window (saltando gli attributi di posizionamento come margine):

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

MainWindowViewModel.cs è mostrato Figura 6.

Figura 6 il primo 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 si esegue l'applicazione (AsyncCommands1 nel download del codice campione), Noterete quattro casi di comportamento inelegante. In primo luogo, l'etichetta Mostra sempre un risultato, anche prima che il pulsante è selezionato. In secondo luogo, non non c'è nessun indicatore di occupato dopo aver fatto clic sul pulsante per indicare che l'operazione è in corso. In terzo luogo, se HTTP richiesta guasti, l'eccezione viene passata all'anello principale dell'interfaccia utente, che causa un crash dell'applicazione. In quarto luogo, se l'utente effettua varie richieste, lei non può distinguere i risultati; è possibile che i risultati di una precedente richiesta di sovrascrivere i risultati di una successiva richiesta a causa di diversi tempi di risposta del server.

Questo è piuttosto una sfilza di problemi! Ma prima iterare il design, considerare per un attimo i generi delle questioni sollevate. Quando un'interfaccia utente diventa asincrona, costringe a pensare a stati aggiuntivi dell'interfaccia utente. Mi raccomando che si faccia almeno queste domande:

  1. Come l'interfaccia utente visualizza Errori? (Spero che tua sincrona UI ha già una risposta per questo!)
  2. Come dovrebbe apparire l'interfaccia utente mentre è in corso l'operazione? (Per esempio, fornirà un feedback immediato tramite indicatori occupati?)
  3. Qual è l'utente limitato, mentre è in corso l'operazione? (Sono pulsanti disattivati, per esempio?)
  4. L'utente dispone eventuali comandi aggiuntivi disponibili mentre è in corso l'operazione? (Per esempio, è possibile annullare l'operazione?)
  5. Se l'utente può avviare più operazioni, come l'interfaccia utente fornisce completamento o dettagli di errore per ciascuno di essi? (Ad esempio, l'interfaccia utente utilizzerà un popup di notifica o di stile "coda di comandi"?)

Gestione asincrona di comando completamento tramite associazione dati

La maggior parte dei problemi nel primo Async­iterazione di comando si riferiscono a come vengono gestiti i risultati. Ciò che realmente serve è qualche tipo che sarebbe avvolgere un compito < T > e fornisce alcune funzionalità di associazione dati, quindi l'applicazione può rispondere più elegantemente. Come spesso accade, la NotifyTaskCompletion < T > tipo sviluppato nel mio ultimo articolo adatta quasi perfettamente queste esigenze. Ho intenzione di aggiungere un membro di questo tipo che semplifica alcuni di Async­logica di comando: una proprietà TaskCompletion che rappresenta il completamento dell'operazione ma non propagare le eccezioni (o restituire un risultato). Ecco le modifiche alla NotifyTaskCompletion < T >:

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

La prossima iterazione del AsyncCommand utilizza NotifyTaskCompletion per rappresentare l'effettivo funzionamento. Così facendo, XAML può associare dati direttamente per il messaggio di errore e il risultato di tale operazione, e può anche utilizzare l'associazione dati per visualizzare un messaggio appropriato mentre è in corso l'operazione. Il nuovo AsyncCommand ha ora una proprietà che rappresenta l'effettivo funzionamento, come indicato nella Figura 7.

Figura 7 il secondo tentativo di un comando asincrono

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

Si noti che AsyncCommand.ExecuteAsync è usando TaskCompletion e non è compito. Non voglio propagare le eccezioni per il loop principale dell'interfaccia utente (cosa che accadrebbe se attendeva la proprietà Task); invece, tornare a TaskCompletion e gestire le eccezioni di associazione dati. Ho anche aggiunto un semplice NullToVisibilityConverter al progetto affinché l'indicatore di occupato, risultati e messaggio di errore sono tutti nascosti fino a quando il pulsante è selezionato. Figura 8 Mostra il codice aggiornato di ViewModel.

Figura 8 il MainWindowViewModel secondo

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 il nuovo codice XAML viene mostrato nella Figura 9.

Figura 9 il secondo 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>

Il codice ora corrisponda al progetto di AsyncCommands2 nell'esempio di codice. Questo codice si prende cura di tutte le preoccupazioni che ho citato con la soluzione originale: le etichette sono nascosti fino alla prima operazione di avvio; c'è un indicatore di occupato immediato fornendo feedback all'utente; le eccezioni vengono catturate e aggiornare l'interfaccia utente tramite associazione dati; più richieste non più interferiscano con a vicenda. Ogni richiesta crea un wrapper di nuovo NotifyTaskCompletion, che ha un proprio risultato indipendente e altre proprietà. NotifyTaskCompletion agisce come un'astrazione associabile a dati di un'operazione asincrona. Questo permette più richieste, con l'interfaccia utente sempre vincolante per l'ultima richiesta. Tuttavia, in molti scenari reali, la soluzione appropriata è disabilitare più richieste. Che è, si desidera che il comando per restituire false da CanExecute mentre c'è un'operazione in corso. Questo è abbastanza facile da fare con una piccola modifica al AsyncCommand, come mostrato Figura 10.

Figura 10 più richieste di disattivazione

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

Ora il codice corrisponde al progetto di AsyncCommands3 nell'esempio di codice. Il pulsante è disattivato mentre è in corso l'operazione.

L'aggiunta di cancellazione

Molte operazioni asincrone possono prendere quantità variabili di tempo. Ad esempio, una richiesta HTTP normalmente possa rispondere molto rapidamente, prima che l'utente può anche rispondere. Tuttavia, se la rete è lenta o il server è occupato, quella stessa richiesta HTTP potrebbe causare un ritardo considerevole. Parte di progettare un'interfaccia asincrona è in attesa e progettando per questo scenario. La soluzione attuale ha già un indicatore di occupato. Quando si progetta un'interfaccia asincrona, si può anche scegliere di dare all'utente più opzioni e cancellazione è una scelta comune.

Cancellazione stessa è sempre un'operazione sincrona — l'atto di richiesta di cancellazione è immediata. La parte più delicata della cancellazione è quando può essere eseguito; dovrebbe essere in grado di eseguire solo quando c'è un comando asincrono in corso. Le modifiche alla AsyncCommand in Figura 11 fornire un comando di annullamento nidificati e notificare tale comando di annullamento quando il comando asincrono inizia e finisce.

Figura 11 aggiunta di cancellazione

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

L'aggiunta di un pulsante Annulla (e un'etichetta annullata) per l'interfaccia utente è molto semplice, come Figura 12 illustrato.

Figura 12 aggiungendo un pulsante Annulla

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

Ora, se si esegue l'applicazione (AsyncCommands4 nell'esempio di codice), troverete il pulsante Annulla inizialmente è disabilitato. Esso viene attivato quando si clicca sul pulsante Go e rimane attivata fino al completamento dell'operazione (se correttamente, colpevolizzata o annullato). Ora avete un UI discutibilmente completo un'operazione asincrona.

Una coda di lavoro semplice

Fino a questo punto, ho stato incentrato su un'interfaccia utente per la sola operazione alla volta. Questo è tutto ciò che è necessario in molte situazioni, ma a volte è necessario la capacità di avviare più operazioni asincrone. A mio parere, come comunità abbiamo non abbiamo escogitato un UX veramente buono per la gestione di più operazioni asincrone. Due approcci comuni utilizza una coda di lavoro o di un sistema di notifica, nessuno dei quali è l'ideale.

Una coda di lavoro vengono visualizzate tutte le operazioni asincrone in una raccolta; Questo dà la massima visibilità dell'utente e il controllo, ma di solito è troppo complesso per l'utente finale tipico di far fronte. Un sistema di notifica nasconde le operazioni mentre si sta eseguendo e pop-up se qualcuno di loro colpa (e possibilmente se che completano con successo). Un sistema di notifica è più facile da usare, ma non fornisce la piena visibilità e il potere della coda di lavoro (ad esempio, è difficile lavorare la cancellazione in un sistema basato sulla notifica). Devo ancora scoprire un'ideale UX per più operazioni asincrone.

Detto questo, a questo punto il codice di esempio può essere esteso per supportare uno scenario di multiplo-operazione senza troppi problemi. Nel codice esistente, il pulsante Go e il pulsante Annulla sono entrambi concettualmente legati a una singola operazione asincrona. La nuova interfaccia utente cambierà il pulsante Go per significare "avvia un'operazione asincrona e aggiungerlo all'elenco di operazioni." Che cosa questo significa è che il pulsante Vai è ora effettivamente sincrono. Ho aggiunto un semplice DelegateCommand (sincrono) per la soluzione, e ora il ViewModel e XAML può essere aggiornato, come Figura 13 e Figura 14 mostrare.

Figura 13 ViewModel per comandi multipli

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 per comandi multipli

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

Questo codice è equivalente al progetto AsyncCommandsWithQueue nel codice di esempio. Quando l'utente fa clic sul pulsante Go, una nuova AsyncCommand è creato e avvolto in un bambino ViewModel (CountUrlBytesViewModel). Questa istanza di ViewModel bambino viene quindi aggiunto all'elenco delle operazioni. Tutto ciò che è connesso con la specifica operazione (le varie etichette e il pulsante Annulla) viene visualizzato in un modello di dati per la coda di lavoro. Ho anche aggiunto un semplice pulsante "X" che rimuove l'elemento dalla coda.

Questa è una coda di lavoro molto semplice, e ho fatto alcune ipotesi circa il disegno. Ad esempio, quando un'operazione viene rimosso dalla coda, esso non è automaticamente annullata. Quando si avvia il lavoro con più operazioni asincrone, raccomando che ti chiedi almeno queste domande aggiuntive:

  1. Come l'utente sapere quale notifica o elemento di lavoro è per quale operazione? (Ad esempio, l'indicatore di occupato in questo campione di coda di lavoro contiene l'URL è il download).
  2. L'utente deve conoscere ogni risultato? (Ad esempio, può essere accettabile per notificare all'utente solo di errori o per rimuovere automaticamente le operazioni riuscite dalla coda di lavoro).

Conclusioni

Non c'è una soluzione universale per un comando asincrono che si adatta alle esigenze di tutti — ancora. La comunità degli sviluppatori è ancora esplorando modelli UI asincroni. Lo scopo di questo articolo è di mostrare come pensare comandi asincroni nel contesto di un'applicazione MVVM, soprattutto in considerazione questioni UX che devono essere affrontate quando l'interfaccia utente diventa asincrona. Ma tenete a mente i modelli in questo articolo e codice di esempio sono pochi modelli e devono essere adattate alle esigenze dell'applicazione.

In particolare, non c'è una storia perfetta per quanto riguarda più operazioni asincrone. Ci sono svantaggi a code di lavoro sia le notifiche, e mi sembra che un universale UX deve ancora essere sviluppato. Come interfacce utente più diventano asincroni, molto di più menti saranno pensare a questo problema, e una svolta rivoluzionaria potrebbe essere proprio dietro l'angolo. Dare il problema qualche pensiero, caro lettore. Forse è lo scopritore di una nuova UX.

Nel frattempo, avete ancora a spedire. In questo articolo ho iniziato con la più semplice delle implementazioni ICommand asincrone e gradualmente aggiunto funzionalità fino a quando ho finito con qualcosa di abbastanza adatto per le applicazioni più moderne. Il risultato è anche completamente unità-verificabili; Poiché il metodo asincrono void ICommand chiama solo il metodo di restituzione di attività IAsyncCommand.ExecuteAsync, è possibile utilizzare ExecuteAsync direttamente negli unit test.

Nel mio ultimo articolo, sviluppato NotifyTaskCompletion < T >, un wrapper di associazione dati attività < T >. In questo, ho mostrato come sviluppare una sorta di AsyncCommand < T >, un'implementazione asincrona di ICommand. Nel mio prossimo articolo, rivolgo servizi asincroni. Tenete a mente che modelli MVVM asincroni sono ancora abbastanza nuovi; non abbiate paura di deviare dal loro ed innovare le proprie soluzioni.

Stephen Cleary è un programmatore che vivono nel nord del Michigan, padre e marito. Ha lavorato con multithreading e asincrona di programmazione per 16 anni e ha utilizzato il supporto asincrono in Microsoft .NET Framework poiché il primo CTP. Sua homepage, compreso il suo blog, è a stephencleary.com.

Grazie ai seguenti esperti tecnici Microsoft per la revisione di questo articolo: James McCaffrey e Stephen Toub