次の方法で共有


非同期プログラミング

非同期 MVVM アプリケーションのパターン: コマンド

Stephen Cleary

コード サンプルのダウンロード

この記事は、確立されたモデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) パターンと async/await の組み合わせに関する連載の 2 回目です。前回は、非同期操作にデータ バインドする方法について説明し、データ バインドに適した Task<TResult> のように機能する重要な型、NotifyTaskCompletion<TResult> を開発しました (msdn.microsoft.com/magazine/dn605875 参照)。今回は、ICommand という、MVVM アプリケーションでユーザー操作 (多くの場合はボタンにデータ バインドしたユーザー操作) の定義に使用する .NET インターフェイスを取り上げ、非同期 ICommand を作成する意義について考察します。

この連載で使用するパターンは、すべてのシナリオに完全に適合するとは限らないので、必要に応じて自由にカスタマイズしてください。なお、この記事は全体として、非同期コマンド型に対する強化を繰り返しながら進めていきます。すべてのイテレーションが完了すると、図 1 に示すようなアプリケーションが完成します。これは、前回の記事で開発したアプリケーションに似ていますが、今回は事実上のコマンドをユーザーに提供して実行できるようにします。ユーザーが [Go] ボタンをクリックしたら、URL をテキスト ボックスから読み取り、その URL から返されたバイト数を (意図的な遅延の後で) アプリケーションで計測します。操作の進行中、ユーザーは他の計測を開始できませんが、操作のキャンセルは可能です。

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
図 1 コマンドを 1 つ実行できるアプリケーション

続いて、ほとんど同じアプローチを使用して任意の数の操作を作成する方法をお見せしましょう。図 2 は、操作のコレクションに操作を追加するよう [Go] ボタンを変更したアプリケーションを示しています。

An Application Executing Multiple Commands
図 2 複数のコマンドを実行しているアプリケーション

実装の細部ではなく非同期コマンドに注目しやすいよう、このアプリケーションの開発ではいくつかの作業を簡略化します。まず、コマンド実行パラメーターは使用しません。個人的には実際のアプリケーションでパラメーターの必要性を感じたことはほとんどありませんが、必要であれば簡単にこの記事のパターンを拡張してパラメーターを追加できます。次に、ICommand.CanExecuteChanged を自力では実装しません。フィールドに似た標準的なイベントを使用すると、一部の MVVM プラットフォームではメモリ リークが発生します (bit.ly/1bROnVj (英語) 参照)。コードを簡潔にするために、Windows Presentation Foundation (WPF) 組み込みの CommandManager を使用して、CanExecuteChanged を実装します。

また、簡易版 "サービス層" を使用します。今のところ、これは図 3 に示すような単一の静的メソッドにすぎません。本質的には前回の記事と同じサービスですが、キャンセルをサポートするように拡張しています。次回の記事では正式な非同期サービス設計を取り上げますが、今回はこの簡易版サービスを使用します。

図 3 サービス層

 

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

非同期コマンド

非同期コマンドの開発に取りかかる前に、ICommand インターフェイスについて簡単に説明しましょう。

 

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

CanExecuteChanged とパラメーターは無視し、非同期コマンドがこのインターフェイスと連携する方法について考えてみてください。CanExecute メソッドは同期メソッドである必要があります。非同期にできるメンバーは、Execute メソッドだけです。Execute メソッドは同期実装用に設計されているので、void を返します。以前の記事「非同期プログラミングのベスト プラクティス」(msdn.microsoft.com/magazine/jj991977) で述べたように、イベント ハンドラー (または論理上イベント ハンドラーに相当するもの) でない限り、async void メソッドは避ける必要があります。ICommand.Execute の実装は論理上はイベント ハンドラーなので、async void でもかまいません。

ただし、async void メソッド内のコードを最小限に抑えて、代わりに実際のロジックを含んだ async Task メソッドを公開する方が理想的です。このベスト プラクティスに従うと、コードがさらにテストしやすくなります。これを踏まえて、この記事では、非同期コマンド インターフェイスとして以下のコードを提案し、基本クラスとして図 4 のコードを提案します。

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

図 1 非同期コマンドの基本型

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

この基本クラスには 2 つの機能があります。1 つは CanExecuteChanged の実装を CommandManager クラスに託す機能で、もう 1 つは IAsyncCommand.ExecuteAsync メソッドを呼び出して async void ICommand.Execute メソッドを実装する機能です。また、結果を待機することで、非同期コマンド ロジックの例外が UI スレッドのメイン ループに対して適切に発生するようにします。

かなり複雑な処理ですが、これらの型にはそれぞれ目的があります。IAsyncCommand は、あらゆる非同期 ICommand 実装に使用でき、ビューモデルから公開してビューや単体テストで使用することを目的としています。AsyncCommandBase は、すべての非同期 ICommand に共通する一般的な定型コードを一部処理します。

このような土台が完成したら、効果的な非同期コマンドの開発に取りかかることができます。戻り値のない同期操作の標準的なデリゲート型は、Action です。これに相当する非同期の型は、Func<Task> です。図 5 に、デリゲートに基づく AsyncCommand の最初のイテレーションを示します。

図 5 非同期コマンドの最初のイテレーション

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

この時点の UI には、URL 用のテキスト ボックス、HTTP 要求を開始するボタン、および結果を表示するラベルしかありません。XAML とビューモデルの基本部分は単純です。以下に、MainWindow.xaml を示します (Margin などの配置属性は省略しています)。

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

MainWindowViewModel.cs は、図 6 に示すとおりです。

図 6 最初の 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
}

アプリケーション (サンプル コードの AsyncCommands1) を実行すると、まだ荒削りな動作が 4 つあることがわかります。1 つ目は、ボタンをクリックする前でも、常にラベルに結果が表示されていることです。2 つ目は、ボタンをクリックした後に、操作が進行中であることを示すビジー状態インジケーターが表示されないことです。3 つ目は、HTTP 要求に失敗した場合、例外が UI のメイン ループに渡り、アプリケーションがクラッシュすることです。4 つ目は、ユーザーが複数の要求を行った場合、アプリケーションでは結果を区別できないことです。サーバーの応答時間が異なれば、後の要求の結果で前の要求の結果を上書きすることも考えられます。

このように問題は山積みです。しかし、設計をやり直す前に、発生している問題の種類について少し考えてみましょう。UI を非同期にすると、考慮が必要な UI の状態が増えます。少なくとも、以下の質問を自分自身に問いかけることをお勧めします。

  1. UI にはどのようにエラーが表示されるか (この質問に対する回答が既に同期 UI に組み込まれているとよいでしょう)。
  2. 操作の進行中に、UI がどのような状態になっている必要があるか (たとえば、ビジー状態インジケーターを使用して即時フィードバックを提供するかどうか)。
  3. 操作の進行中に、ユーザーはどのような制約を受けるか (たとえば、ボタンが無効になるかどうか)。
  4. 操作の進行中に、ユーザーが使用できるコマンドがあるかどうか (たとえば、操作をキャンセルできるかどうか)。
  5. ユーザーが複数の操作を開始できる場合、各操作の完了やエラー詳細を UI にどのように表示するか (たとえば、UI で "コマンド キュー" スタイルや通知ポップアップを使用するかどうか)。

データ バインドを使用して非同期コマンドの完了を処理する

AsyncCommand の最初のイテレーションにおける問題の多くは、結果の処理方法に関連しています。本当に必要なのは、より洗練された方法でアプリケーションが応答できるよう、Task<T> をラップしてデータ バインド機能を提供する型です。偶然にも、前回の記事で開発した NotifyTaskCompletion<T> 型は、このニーズをほぼ完全に満たします。NotifyTaskCompletion<T> 型にメンバーを 1 つ追加して、AsyncCommand のロジックをいくらか簡略化しましょう。追加するメンバーは、操作の完了を表しながらも例外を伝達しない (結果を返さない) TaskCompletion プロパティです。NotifyTaskCompletion<T> の変更点は以下のとおりです。

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

AsyncCommand の 2 回目のイテレーションでは、NotifyTaskCompletion を使用して実際の操作を表します。このようにすると、XAML でその操作の結果とエラー メッセージに直接データ バインドできます。また、データ バインドを使用して、操作の進行中に適切なメッセージを表示できます。新しい AsyncCommand には実際の操作を表すプロパティを追加しています (図 7 参照)。

図 7 非同期コマンドの 2 回目のイテレーション

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

AsyncCommand.ExecuteAsync で、Task ではなく TaskCompletion を使用していることに注意してください。今回は、(Task プロパティの待機時に発生する) 例外を UI メイン ループに伝達しません。代わりにTaskCompletion を返して、データ バインドで例外を処理します。また、プロジェクトに単純な NullToVisibilityConverter を追加して、ボタンをクリックするまではビジー状態インジケーター、結果、およびエラー メッセージが表示されないようにしています。図 8 に、新しいビューモデルのコードを示します。

図 8 2 回目の 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; }
}

また、新しい XAML コードを図 9 に示します。

図 9 2 回目の 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>

このコードは、サンプル コードの AsyncCommands2 プロジェクトに対応しています。このコードでは、最初のソリューションで指摘した問題がすべて解決しています。つまり、最初の操作を開始するまでラベルが表示されず、ユーザーにフィードバックを提供するビジー状態インジケーターがすぐに表示され、例外がキャッチされてデータ バインド経由で UI が更新され、複数の要求が相互干渉しなくなっています。要求を開始するたびに新しい NotifyTaskCompletion ラッパーが作成され、ラッパーには固有の独立した Result などのプロパティが含まれています。NotifyTaskCompletion は、非同期操作のデータ バインド可能な抽象化として機能します。このため複数の要求を実行でき、必ず最新の要求に UI がバインドされます。しかし実際のシナリオの多くでは、適切な解決策とは複数の要求を無効にすることです。つまり、進行中の操作がある場合はコマンドで CanExecute から false を返す必要があります。これは、AsyncCommand を少し変更するだけで簡単に対処できます (図 10 参照)。

図 10 複数要求の無効化

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

このコードは、サンプル コードの AsyncCommands3 プロジェクトに対応しています。操作の進行中は、ボタンが無効になります。

キャンセルを追加する

非同期操作は、所要時間が一定ではありません。たとえば、HTTP 要求は通常は応答が非常に速く、ユーザーが反応できないほど速いこともあります。しかし、ネットワークが低速な場合やサーバーの負荷が高い場合は、同じ HTTP 要求でもかなりの待ち時間が発生することがあります。非同期 UI を設計する際は、このようなシナリオも想定して設計します。現在のソリューションは、既にビジー状態インジケーターを実装しています。また、非同期 UI を設計する際はユーザーに追加のオプションを提供することもでき、その代表格がキャンセルです。

キャンセル要求は緊急の行動なので、キャンセル自体は必ず同期操作になります。キャンセルの最も厄介な点は、実行可能条件です。キャンセルは、非同期コマンドの進行中にのみ実行できるようにする必要があります。図 11 のように AsyncCommand を変更すると、入れ子のキャンセル コマンドを提供して、非同期コマンドの開始と終了のタイミングをそのキャンセル コマンドに通知できます。

図 11 キャンセルの追加

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

UI に [Cancel] ボタン (と [Canceled] ラベル) を追加することは簡単です (図 12 参照)。

図 12 [Cancel] ボタンの追加

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

ここでアプリケーション (サンプル コードの AsyncCommands4) を実行すると、最初は [Cancel] ボタンが無効になっています。[Go] ボタンをクリックすると有効になり、操作が (成功、失敗、キャンセルのどれであっても) 完了するまで有効のままになります。これで、非同期操作用の UI がほぼ完成しました。

単純な作業キュー

ここまでは、一度に 1 つだけの操作を実行する UI を説明してきました。多くの場合はそれだけで十分ですが、場合によっては複数の非同期操作を開始できるようにする必要があります。個人的な見解ですが、開発者コミュニティはまだ複数の非同期操作を扱う優れた UX を生み出していません。一般的なアプローチは、作業キューの使用と通知システムの使用の 2 つですが、いずれも最適とは言えません。

作業キューは、コレクションに含まれるすべての非同期操作を表示します。このようにすると、ユーザーに最大限の可視性と制御力を提供できますが、通常は複雑すぎて一般的なエンド ユーザーには対処できません。通知システムは、実行中の操作を表示せず、操作にエラーが発生した場合に (ときには正常に完了した場合にも) ポップアップを表示します。通知システムの方がユーザーにとって使いやすくなりますが、作業キューのような完全な可視性や制御力はありません (たとえば、通知ベースのシステムにキャンセルを組み込むことは困難です)。複数の非同期操作に最適な UX は、まだ見つかっていません。

とは言うものの、この時点のサンプル コードは、それほど苦労しなくても複数操作シナリオをサポートするよう拡張できます。既存のコードの [Go] ボタンと [Cancel] ボタンは、どちらも概念上は 1 つの非同期操作に関連しています。新しい UI を使用すると、[Go] ボタンの意味が "新しい非同期操作を開始して、操作のリストに追加する" ことに変わります。つまり、[Go] ボタンが実際には同期的になります。単純な (同期的な) DelegateCommand をソリューションに追加したので、ViewModel と XAML を更新できるようになりました (図 13図 14 参照)。

図 13 複数コマンド用のビューモデル

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

図 14 複数コマンド用の XAML

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

 

このコードは、サンプル コードの AsyncCommandsWithQueue プロジェクトに対応しています。ユーザーが [Go] ボタンをクリックしたら、新しい AsyncCommand を作成して、子ビューモデル (CountUrlBytesViewModel) にラップします。その後、この子ビューモデル インスタンスを操作のリストに追加します。特定の操作に関連付けられているすべての要素 (さまざまなラベルと [Cancel] ボタン) は、作業キューのデータ テンプレートに表示します。また、キューから項目を削除する単純な [X] ボタンも追加しています。

これはごく基本的な作業キューであり、設計に関していくつか条件を想定しています。たとえば、操作をキューから削除しても、自動的には取り消されません。複数の非同期操作を使い始めるときは、追加で少なくとも次の質問を自分に問いかけることをお勧めします。

  1. 通知や作業項目に対する操作の対応関係をユーザーにどのように知らせるか (たとえば、この作業キュー サンプルのビジー状態インジケーターには、ダウンロード中の URL が表示されます)。
  2. ユーザーがすべての結果を知る必要があるかどうか (たとえば、ユーザーにエラーのみを通知したり、成功した操作を作業キューから自動的に削除したりしても問題ない場合もあります)。

まとめ

あらゆるニーズを満たす、非同期コマンド用の万能ソリューションはまだありません。開発者コミュニティは、非同期 UI パターンをまだ模索しています。この記事の目標は、MVVM アプリケーションの観点から非同期コマンドについての考え方を示すこと、特に、UI が非同期になった場合に対応が必要な、UX の問題について検討することでした。ただし、この記事やサンプル コードで紹介したパターンは単なるパターンなので、アプリケーションのニーズに合わせて変更する必要があることに留意してください。

とりわけ、複数の非同期操作に関する完璧なソリューションはありません。作業キューと通知のどちらにも欠点があり、万能の UX はまだ開発されていないように思われます。非同期 UI が増えるにつれて、この問題について考える人も増えるでしょう。革新的な打開策が現れるのももうすぐかもしれません。読者の皆さんも、この問題について考えてみてください。新しい UX の発見者になれる可能性もあります。

それまでの間も、開発者は開発を進める必要があります。この記事では、最も基本的な非同期 ICommand 実装を出発点として徐々に機能を追加していき、最新のアプリケーションにかなり適した実装を作り上げました。完成したコードは、完全に単体テスト可能でもあります。async void ICommand.Execute メソッドは Task を返す IAsyncCommand.ExecuteAsync メソッドのみを呼び出すので、単体テストで直接 ExecuteAsync メソッドを使用できます。

前回の記事では、 NotifyTaskCompletion<T> という、Task<T> のデータ バインド ラッパーを開発しました。今回は、AsyncCommand<T> の一種である ICommand の非同期実装を開発する方法について説明しました。次回の記事では、非同期サービスを取り上げます。なお、非同期 MVVM パターンはまだ生まれたてのパターンです。パターンの逸脱や独自ソリューションの導入をためらわないでください。

Stephen Cleary はミシガン北部在住の夫、父親兼プログラマです。彼は、マルチスレッドと非同期プログラミングに 16 年間取り組み、最初の CTP から Microsoft .NET Framework の非同期サポートを使ってきました。彼のホーム ページとブログは、stephencleary.com (英語) から利用できます。

この記事のレビューに協力してくれた技術スタッフの James McCaffrey と Stephen Toub に心より感謝いたします。