С# async/await Событие выполнения в Задаче <> объект

Я полностью новичок в С# 5 новых async/await ключевых словах, и меня интересует лучший способ реализации события прогресса.

Теперь я бы предпочел, чтобы событие Progress находилось в самой Task<>. Я знаю, что могу просто поместить событие в класс, содержащий асинхронный метод, и передать какой-либо объект состояния в обработчик события, но для меня это похоже на обходное решение, чем решение. Мне также могут потребоваться различные задачи, чтобы отключить обработчики событий в разных объектах, что звучит беспорядочно таким образом.

Есть ли способ сделать что-то похожее на следующее:

var task = scanner.PerformScanAsync();
task.ProgressUpdate += scanner_ProgressUpdate;
return await task;

Ответ 1

Проще говоря, Task не поддерживает прогресс. Однако уже существует традиционный способ сделать это, используя интерфейс IProgress<T>. Асинхронный шаблон на основе задач в основном предполагает перегрузку ваших асинхронных методов (где это имеет смысл), чтобы позволить клиентам проходить в реализации IProgess<T>. После этого ваш асинхронный метод сообщит о ходе выполнения.

API-интерфейс Windows Runtime (WinRT) имеет встроенные индикаторы прогресса в IAsyncOperationWithProgress<TResult, TProgress> и IAsyncActionWithProgress<TProgress>... поэтому, если вы на самом деле пишете для WinRT, стоит посмотреть на них, но также читайте комментарии ниже.

Ответ 2

Рекомендуемый подход описан в документации Task-based Asynchronous Pattern, которая дает каждому асинхронному методу свой собственный IProgress<T>:

public async Task PerformScanAsync(IProgress<MyScanProgress> progress)
{
  ...
  if (progress != null)
    progress.Report(new MyScanProgress(...));
}

Использование:

var progress = new Progress<MyScanProgress>();
progress.ProgressChanged += ...
PerformScanAsync(progress);

Примечания:

  • По соглашению параметр progress может быть null, если вызывающему абоненту не нужны отчеты о ходе работы, поэтому обязательно проверяйте это в своем методе async.
  • Отчет о ходе выполнения сам по себе является асинхронным, поэтому при каждом вызове вы должны создавать новый экземпляр своих аргументов (еще лучше, просто используйте неизменяемые типы для ваших событийных аргументов). Вы не должны мутировать, а затем повторно использовать те же объекты аргументов для нескольких вызовов progress.
  • Тип Progress<T> будет захватывать текущий контекст (например, контекст пользовательского интерфейса) при построении и будет поднять его событие ProgressChanged в этом контексте. Поэтому вам не нужно беспокоиться о том, чтобы перенаправить обратно в поток пользовательского интерфейса, прежде чем называть Report.

Ответ 3

Мне пришлось собрать этот ответ из нескольких сообщений, поскольку я пытался выяснить, как сделать эту работу для кода, который менее тривиален (т.е. события уведомляют об изменениях).

Предположим, у вас есть синхронный процессор элементов, который объявит номер позиции, над которым он начнет работу. Для моего примера я просто буду манипулировать содержимым кнопки "Процесс", но вы можете легко обновить индикатор выполнения и т.д.

private async void BtnProcess_Click(object sender, RoutedEventArgs e)
{       
    BtnProcess.IsEnabled = false; //prevent successive clicks
    var p = new Progress<int>();
    p.ProgressChanged += (senderOfProgressChanged, nextItem) => 
                    { BtnProcess.Content = "Processing page " + nextItem; };

    var result = await Task.Run(() =>
    {
        var processor = new SynchronousProcessor();

        processor.ItemProcessed += (senderOfItemProcessed , e1) => 
                                ((IProgress<int>) p).Report(e1.NextItem);

        var done = processor.WorkItWorkItRealGood();

        return done ;
    });

    BtnProcess.IsEnabled = true;
    BtnProcess.Content = "Process";
}

Ключевой частью этого является закрытие переменной Progress<> внутри ItemProcessed подписки. Это позволяет всем Just works ™.