1

DoLoadingロード操作を実行するためにコンシューマーがサブスクライブできるイベントを公開するコントロールを作成しようとしています。便宜上、イベント ハンドラーを UI スレッドから呼び出して、コンシューマーが自由に UI を更新できるようにする必要がありますが、async/await を使用して、UI スレッドをブロックすることなく長時間実行されるタスクを実行することもできます。

このために、次のデリゲートを宣言しました。

public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e);

これにより、コンシューマーはイベントをサブスクライブできます。

public event AsyncEventHandler<bool> DoLoading;

アイデアは、消費者がそのようにイベントをサブスクライブするということです (この行は UI スレッドで実行されます):

loader.DoLoading += async (s, e) =>
            {
                for (var i = 5; i > 0; i--)
                {
                    loader.Text = i.ToString(); // UI update
                    await Task.Delay(1000); // long-running task doesn't block UI
                }
            };

適切な時点でTaskScheduler、UI スレッドの を取得し、 に保存してい_uiSchedulerます。

イベントは、次の行で適切な場合にトリガーされloaderます (これはランダムなスレッドで発生します)。

this.PerformLoadingActionAsync().ContinueWith(
            _ =>
            {
                // Other operations that must happen on UI thread
            },
            _uiScheduler);

この行は UI スレッドから呼び出されるのではなく、読み込みの完了時に UI を更新する必要があることに注意してください。そのためContinueWith、読み込みタスクの完了時に UI タスク スケジューラでコードを実行するために使用しています。

次の方法のいくつかのバリエーションを試しましたが、どれもうまくいきませんでした。

private async Task<Task> PerformLoadingActionAsync()
{
    TaskFactory uiFactory = new TaskFactory(_uiScheduler);

    // Trigger event on the UI thread and await its execution
    Task evenHandlerTask = await uiFactory.StartNew(async () => await this.OnDoLoading(_mustLoadPreviousRunningState));

    // This can be ignored for now as it completes immediately
    Task commandTask = Task.Run(() => this.ExecuteCommand());

    return Task.WhenAll(evenHandlerTask, commandTask);
}

private async Task OnDoLoading(bool mustLoadPreviousRunningState)
{
    var handler = this.DoLoading;

    if (handler != null)
    {
        await handler(this, mustLoadPreviousRunningState);
    }
}

ご覧のとおり、私は 2 つのタスクを開始してContinueWithおり、以前から 1 つのタスクを実行してすべてを完了することを期待しています。

commandTaskすぐに完了するので、当面は無視できます。私が見ているように、イベントハンドラーを呼び出すメソッドへのeventHandlerTask呼び出しを待っていて、イベントハンドラー自体を待っていることを考えると、イベントハンドラーが完了する1つだけを完了する必要があります。

ただし、実際に起こっていることはawait Task.Delay(1000)、イベント ハンドラーの行が実行されるとすぐにタスクが完了していることです。

これはなぜですか?どうすれば期待どおりの動作を得ることができますか?

4

2 に答える 2

4

まず、「非同期イベント」の設計を再考することをお勧めします。

の戻り値を使用できるのは事実ですがTask、C# イベント ハンドラーが を返す方が自然ですvoid。特に、複数のサブスクリプションがある場合、Task返される fromはイベント ハンドラーの1 つhandler(this, ...)の戻り値のみです。すべての非同期イベントが完了するのを適切に待機するには、イベントを発生させるときにwithを使用する必要があります。Delegate.GetInvocationListTask.WhenAll

既に WinRT プラットフォームを使用しているため、"遅延" を使用することをお勧めします。これは、WinRT チームが非同期イベント用に選択したソリューションであるため、クラスのコンシューマーにはなじみがあるはずです。

残念ながら、WinRT チームは、WinRT の .NET フレームワークに遅延インフラストラクチャを含めませんでした。そこで、非同期イベント ハンドラーと遅延マネージャーの作成方法に関するブログ記事を書きました。

遅延を使用すると、イベントを発生させるコードは次のようになります。

private Task OnDoLoading(bool mustLoadPreviousRunningState)
{
  var handler = this.DoLoading;
  if (handler == null)
    return;

  var args = new DoLoadingEventArgs(this, mustLoadPreviousRunningState);
  handler(args);
  return args.WaitForDeferralsAsync();
}

private Task PerformLoadingActionAsync()
{
  TaskFactory uiFactory = new TaskFactory(_uiScheduler);

  // Trigger event on the UI thread.
  var eventHandlerTask = uiFactory.StartNew(() => OnDoLoading(_mustLoadPreviousRunningState)).Unwrap();

  Task commandTask = Task.Run(() => this.ExecuteCommand());
  return Task.WhenAll(eventHandlerTask, commandTask);
}

それが解決策の私の推奨事項です。遅延の利点は、同期ハンドラーと非同期ハンドラーの両方を有効にすること、WinRT 開発者には既になじみのある手法であること、コードを追加しなくても複数のサブスクライバーを正しく処理できることです。

元のコードが機能しない理由については、コード内のすべての型に注意を払い、各タスクが何を表しているかを特定することで、これを考えることができます。次の重要な点に注意してください。

  • Task<T>から派生しTaskます。これは、警告なしでTask<Task>に変換されることを意味します。Task
  • StartNewは をasync認識しないため、 とは異なる動作をしTask.Runます。この件に関するStephen Toub の優れたブログ投稿を参照してください。

メソッドは、最後のイベント ハンドラーの完了を表すOnDoLoadingを返します。他のイベント ハンドラーからの s はすべて無視されます (前述のように、複数の非同期ハンドラーを適切にサポートするには、 または 遅延を使用する必要があります)。TaskTaskDelegate.GetInvocationList

PerformLoadingActionAsyncでは、次を見てみましょう。

Task evenHandlerTask = await uiFactory.StartNew(async () => await this.OnDoLoading(_mustLoadPreviousRunningState));

この声明には多くのことが書かれています。これは、この (少し単純な) コード行と意味的に同等です。

Task evenHandlerTask = await uiFactory.StartNew(() => OnDoLoading(_mustLoadPreviousRunningState));

OnDoLoadingOK、 UI スレッドのキューに入れます。の戻り値の型OnDoLoadingTaskであるため、 の戻り値の型はStartNewですTask<Task>Stephen Toub のブログでは、この種のラッピングの詳細について説明していますが、次のように考えることができます。「外側」のタスクは、非同期メソッドの開始OnDoLoadingを表し(で生成する必要があるまでawait)、「内側」のタスクは非同期メソッドの開始を表します。 taskは、非同期メソッドの完了を表します。OnDoLoading

次に、awaitの結果ですStartNew。これにより、「外側の」タスクがアンラップされ、格納されTaskた の完了を表す が得られます。OnDoLoadingevenHandlerTask

return Task.WhenAll(evenHandlerTask, commandTask);

これで、とのTask両方が完了したことを表す を返しています。ただし、メソッド内にいるため、実際の戻り値の型は- であり、必要なものを表すのは内部タスクです。あなたが意図したことは次のとおりだと思います。commandTaskevenHandlerTaskasyncTask<Task>

await Task.WhenAll(evenHandlerTask, commandTask);

Taskこれにより、完全な完了を表す の戻り値の型が得られます。

それがどのように呼ばれているかを見ると:

this.PerformLoadingActionAsync().ContinueWith(...)

ContinueWithは、元のコードでは外側に作用していますが、実際には 内側Taskに作用させたいと思っていました。 Task

于 2013-05-07T13:15:34.763 に答える