Можно ли ожидать события вместо другого асинхронного метода?

В моем приложении метро C # / XAML есть кнопка, запускающая длительный процесс. Итак, как рекомендовано, я использую async / await, чтобы убедиться, что поток пользовательского интерфейса не заблокирован:

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
     await GetResults();
}

private async Task GetResults()
{ 
     // Do lot of complex stuff that takes a long time
     // (e.g. contact some web services)
  ...
}

Иногда для продолжения работы, происходящей в GetResults, требуется дополнительный ввод пользователя. Для простоты предположим, что пользователю просто нужно нажать кнопку «продолжить».

У меня вопрос: как я могу приостановить выполнение GetResults таким образом, чтобы оно ожидало события, например, щелчка другой кнопки?

Вот уродливый способ добиться того, что я ищу: обработчик событий для кнопки "продолжить" устанавливает флаг ...

private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
    _continue = true;
}

... и GetResults периодически опрашивает его:

 buttonContinue.Visibility = Visibility.Visible;
 while (!_continue) await Task.Delay(100);  // poll _continue every 100ms
 buttonContinue.Visibility = Visibility.Collapsed;

Опрос явно ужасен (занятое ожидание / трата циклов), и я ищу что-то, основанное на событиях.

Любые идеи?

Кстати, в этом упрощенном примере одним из решений было бы, конечно, разделить GetResults () на две части, вызвать первую часть с помощью кнопки запуска и вторую часть с помощью кнопки продолжения. На самом деле, все, что происходит в GetResults, более сложное, и в разных точках выполнения могут потребоваться разные типы пользовательского ввода. Поэтому разбить логику на несколько методов было бы нетривиально.


person Max♦    schedule 12.10.2012    source источник


Ответы (9)


Вы можете использовать экземпляр SemaphoreSlim Class как сигнал:

private SemaphoreSlim signal = new SemaphoreSlim(0, 1);

// set signal in event
signal.Release();

// wait for signal somewhere else
await signal.WaitAsync();

Кроме того, вы можете использовать экземпляр TaskCompletionSource ‹T› Class для создайте задачу ‹T›, которая представляет результат нажатия кнопки:

private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

// complete task in event
tcs.SetResult(true);

// wait for task somewhere else
await tcs.Task;
person dtb    schedule 12.10.2012
comment
Я бы использовал ManualResetEvent. Есть ли преимущество в использовании SemaphoreSlim, или вы могли бы использовать любой из них? - person Daniel Hilgarth; 12.10.2012
comment
@DanielHilgarth ManualResetEvent(Slim), похоже, не поддерживает WaitAsync(). - person svick; 12.10.2012
comment
@svick: Хорошее замечание. Однако, поскольку GetResult уже async, вы можете без проблем заблокировать этот метод, не так ли? - person Daniel Hilgarth; 12.10.2012
comment
@DanielHilgarth Нет, ты не мог. async не означает «выполняется в другом потоке» или что-то в этом роде. Это просто означает «вы можете использовать await в этом методе». И в этом случае блокировка внутри GetResults() фактически заблокирует поток пользовательского интерфейса. - person svick; 12.10.2012
comment
@svick: Я согласен, async не создает новый поток автоматически. Но в сочетании с await это так, не так ли? Итак, в этом конкретном примере он не блокирует поток пользовательского интерфейса, не так ли? - person Daniel Hilgarth; 12.10.2012
comment
@svick Блокировка внутри GetResults() фактически заблокирует поток пользовательского интерфейса. - Это неправда. Блокировка в GetResults не блокирует поток пользовательского интерфейса, потому что до тех пор, пока GetResults не вернется, он будет работать асинхронно. Фоновый поток / задача будет заблокирован, но не поток пользовательского интерфейса. Поток пользовательского интерфейса был оставлен в момент вызова GetResults. Если ожидаемый объект от GetResult на самом деле не переходит в другой поток (а это почти всегда так, или он ожидает завершения некоторого ввода-вывода), он не будет блокироваться. - person casperOne; 12.10.2012
comment
@Gabe await сам по себе не гарантирует, что будет создан другой поток, но он заставляет все остальное после оператора запускаться как продолжение на Task или в ожидании того, что вы вызываете await. Чаще всего это какая-то асинхронная операция, которая может быть завершением ввода-вывода или чем-то, что выполняется в другом потоке. - person casperOne; 12.10.2012
comment
@casperOne Но в коде вопроса нет фоновой задачи. GetResults() вызывается непосредственно из обработчика событий, что означает, что он запускается в контексте пользовательского интерфейса. Он не заблокировал бы поток пользовательского интерфейса, если бы где-то было что-то вроде Task.Run() или ConfigureAwait(false), но там такого нет. - person svick; 12.10.2012
comment
@svick GetResults возвращает ожидаемый Task. Это означает, что Task выполняется, а ожидающий метод фактически завершает работу. Затем создается продолжение, которое продолжает оставшийся код, когда то, что вы используете await, завершается (с использованием SynchronizationContext, если он есть, и которому не сказано не использовать его). Task выполняется асинхронно, а затем оставшийся код в обработчике событий маршалируется обратно в поток пользовательского интерфейса в контексте синхронизации, когда Task завершается. - person casperOne; 12.10.2012
comment
@casperOne Конечно, но «выполняется асинхронно» не означает «выполняется в другом потоке». GetResults() все еще находится в контексте пользовательского интерфейса. - person svick; 12.10.2012
comment
@svick Нет, GetResults продолжает в контексте пользовательского интерфейса. Это большая разница. Большинство методов на Task, которые создают Task экземпляры, не захватывают контекст синхронизации. Это ждет от них, что делает. фактическая задача, выполняющая фоновую работу, вообще не захватывает этот контекст. - person casperOne; 12.10.2012
comment
@casperOne Я хочу сказать, что в этом вопросе нет кода, который явно создает Task, поэтому весь код будет выполняться в потоке пользовательского интерфейса. - person svick; 12.10.2012
comment
@svick Нет, но если у вас есть async, тогда вам нужно иметь await, если вы не делаете что-то способом вне нормы, этот await будет на Task, который основан на другом потоке, или ожидая завершения ввода-вывода, который продолжится в другом потоке. В любом случае ожидание снимается с потока пользовательского интерфейса, а затем возобновляется в потоке пользовательского интерфейса, когда выполняется эта конкретная операция (независимо от того, на скольких уровнях она скрыта). Вы не блокируете все время. - person casperOne; 12.10.2012
comment
Для тех, кто хочет узнать больше, см. Здесь: chat.stackoverflow.com/rooms/17937 - @svick и я в основном неправильно поняли друг друга, но говорили одно и то же. - person casperOne; 12.10.2012
comment
+1. Мне пришлось это найти, так что на всякий случай, если другие заинтересуются: SemaphoreSlim.WaitAsync не просто помещает Wait в поток пула потоков. SemaphoreSlim имеет правильную очередь из Task, которые используются для реализации WaitAsync. - person Stephen Cleary; 13.10.2012
comment
TaskCompletionSource ‹T› + await .Task + .SetResult () оказался идеальным решением для моего сценария - спасибо! :-) - person Max♦; 17.10.2012
comment
Использование TaskCompletionSource<T>, await tcs.Task, tcs.SetResult() оказалось самым простым способом ожидания события. На самом деле, намного проще, чем асинхронный шаблон на основе событий продемонстрированный в MSDN. - person Alex Essilfie; 21.08.2015
comment
Много путаницы между параллелизмом и асинхронизмом. У вас может быть идеально асинхронная среда, работающая в одном-единственном потоке. Посмотрите на javascript в браузерах. Он работает только в одном потоке, но код в основном асинхронный (со всеми обратными вызовами, а теперь и с обещаниями). Асинхронная среда может быть основана на цикле событий. Все обрабатывается в одном потоке, но в разные моменты времени. Асинхронная операция обычно последовательна сама по себе, а не параллельна. Сделай это, когда закончишь делать то, ... и т. Д. Параллелизм проявляется тогда, когда вы также определяете, как и где выполнять работу! - person Thanasis Ioannidis; 16.10.2019

Когда у вас есть необычная вещь, которую вам нужно await, самый простой ответ - это часто TaskCompletionSource (или какой-нибудь примитив с поддержкой async на основе TaskCompletionSource).

В этом случае вам нужно довольно просто, поэтому вы можете просто использовать TaskCompletionSource напрямую:

private TaskCompletionSource<object> continueClicked;

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
  // Note: You probably want to disable this button while "in progress" so the
  //  user can't click it twice.
  await GetResults();
  // And re-enable the button here, possibly in a finally block.
}

private async Task GetResults()
{ 
  // Do lot of complex stuff that takes a long time
  // (e.g. contact some web services)

  // Wait for the user to click Continue.
  continueClicked = new TaskCompletionSource<object>();
  buttonContinue.Visibility = Visibility.Visible;
  await continueClicked.Task;
  buttonContinue.Visibility = Visibility.Collapsed;

  // More work...
}

private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
  if (continueClicked != null)
    continueClicked.TrySetResult(null);
}

Логически TaskCompletionSource похож на async ManualResetEvent, за исключением того, что вы можете «установить» событие только один раз, и событие может иметь «результат» (в данном случае мы его не используем, поэтому мы просто устанавливаем результат равным null) .

person Stephen Cleary    schedule 12.10.2012
comment
Поскольку я анализирую await a event как практически такую ​​же ситуацию, как «обернуть EAP в задачу», я определенно предпочитаю этот подход. IMHO, это определенно более простой / понятный код. - person James Manning; 12.10.2012

Вот служебный класс, который я использую:

public class AsyncEventListener
{
    private readonly Func<bool> _predicate;

    public AsyncEventListener() : this(() => true)
    {

    }

    public AsyncEventListener(Func<bool> predicate)
    {
        _predicate = predicate;
        Successfully = new Task(() => { });
    }

    public void Listen(object sender, EventArgs eventArgs)
    {
        if (!Successfully.IsCompleted && _predicate.Invoke())
        {
            Successfully.RunSynchronously();
        }
    }

    public Task Successfully { get; }
}

И вот как я его использую:

var itChanged = new AsyncEventListener();
someObject.PropertyChanged += itChanged.Listen;

// ... make it change ...

await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;
person Anders Skovborg    schedule 02.08.2016
comment
Я не знаю, как это работает. Как метод Listen асинхронно выполняет мой пользовательский обработчик? Разве new Task(() => { }); не будет завершено мгновенно? - person nawfal; 06.12.2019

Простой вспомогательный класс:

public class EventAwaiter<TEventArgs>
{
    private readonly TaskCompletionSource<TEventArgs> _eventArrived = new TaskCompletionSource<TEventArgs>();

    private readonly Action<EventHandler<TEventArgs>> _unsubscribe;

    public EventAwaiter(Action<EventHandler<TEventArgs>> subscribe, Action<EventHandler<TEventArgs>> unsubscribe)
    {
        subscribe(Subscription);
        _unsubscribe = unsubscribe;
    }

    public Task<TEventArgs> Task => _eventArrived.Task;

    private EventHandler<TEventArgs> Subscription => (s, e) =>
        {
            _eventArrived.TrySetResult(e);
            _unsubscribe(Subscription);
        };
}

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

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
                            h => example.YourEvent += h,
                            h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;
person Felix Keil    schedule 08.02.2017
comment
Как бы вы очистили подписку на example.YourEvent? - person Denis P; 09.03.2018
comment
@DenisP, возможно, передать событие в конструктор для EventAwaiter? - person CJBrew; 19.03.2018
comment
@DenisP Я улучшил версию и провел небольшой тест. - person Felix Keil; 19.03.2018
comment
Я также мог видеть добавление IDisposable, в зависимости от обстоятельств. Кроме того, чтобы избежать необходимости вводить событие дважды, мы также можем использовать Reflection для передачи имени события, так что использование будет еще проще. В остальном шаблон мне нравится, спасибо. - person Denis P; 21.03.2018

В идеале нет. Хотя вы, безусловно, можете заблокировать асинхронный поток, это пустая трата ресурсов и не идеально.

Рассмотрим канонический пример, когда пользователь идет на обед, а кнопка ожидает нажатия.

Если вы остановили свой асинхронный код, ожидая ввода от пользователя, то он просто тратит ресурсы, пока этот поток приостановлен.

Тем не менее, лучше, если в вашей асинхронной операции вы установите состояние, которое вам нужно поддерживать, до точки, когда кнопка включена, и вы «ждете» щелчка. На этом этапе ваш GetResults метод останавливается.

Затем при нажатии кнопки в зависимости от сохраненного состояния вы запускаете другую асинхронную задачу, чтобы продолжить работу.

Поскольку SynchronizationContext будет записан в обработчике событий, вызывает GetResults (компилятор сделает это в результате использования ключевого слова await и того факта, что SynchronizationContext.Current не должен иметь значение NULL, если вы находитесь в приложении пользовательского интерфейса), вы можете использовать _ 5 _ / _ 6_ вот так:

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
     await GetResults();

     // Show dialog/UI element.  This code has been marshaled
     // back to the UI thread because the SynchronizationContext
     // was captured behind the scenes when
     // await was called on the previous line.
     ...

     // Check continue, if true, then continue with another async task.
     if (_continue) await ContinueToGetResultsAsync();
}

private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
    _continue = true;
}

private async Task GetResults()
{ 
     // Do lot of complex stuff that takes a long time
     // (e.g. contact some web services)
  ...
}

ContinueToGetResultsAsync - это метод, который продолжает получать результаты даже при нажатии кнопки. Если ваша кнопка не нажата, ваш обработчик событий ничего не делает.

person casperOne    schedule 12.10.2012
comment
Какой асинхронный поток? Нет кода, который не запускался бы в потоке пользовательского интерфейса, как в исходном вопросе, так и в вашем ответе. - person svick; 12.10.2012
comment
@svick Неправда. GetResults возвращает Task. await просто говорит запустить задачу, а когда задача будет выполнена, продолжить выполнение кода после этого. При наличии контекста синхронизации вызов маршалируется обратно в поток пользовательского интерфейса, как это зафиксировано в await. await не то же самое, что Task.Wait(), ни в малейшей степени. - person casperOne; 12.10.2012
comment
Я ничего не сказал о Wait(). Но код в GetResults() будет выполняться здесь в потоке пользовательского интерфейса, другого потока нет. Другими словами, да, await в основном выполняет задачу, как вы говорите, но здесь эта задача также выполняется в потоке пользовательского интерфейса. - person svick; 12.10.2012
comment
@svick Нет причин делать предположение, что задача выполняется в потоке пользовательского интерфейса, почему вы делаете это предположение? Это возможно, но маловероятно. И вызов - это два отдельных вызова пользовательского интерфейса, технически один до await, а затем код после await, блокировки нет. Остальной код маршалируется обратно в продолжение и планируется через SynchronizationContext. - person casperOne; 12.10.2012
comment
Я не делаю никаких предположений, я просто смотрю на код как есть. И как бы то ни было, код внутри GetResults() будет выполняться в потоке пользовательского интерфейса. Я думаю, это вы делаете предположение, что GetResults() будет содержать что-то вроде Task.Run(). Но я говорю не об этом, я говорю о коде прямо в GetResults(). - person svick; 12.10.2012
comment
@svick На основе чего? Части его будут выполняться, но в какой-то момент должно быть что-то, на котором он ожидает, что будет основано на Task, а затем на этом код будет выполняться асинхронно, а все остальное будет продолжено в потоке пользовательского интерфейса, но в вызове продолжения. Эти точки до и после асинхронной задачи будут в потоке пользовательского интерфейса, но будет фоновая операция (если вы не сделаете что-то вроде синхронного запуска или какого-либо другого пограничного случая), которого нет, и именно там блокировка разбита. . - person casperOne; 12.10.2012
comment
@svick У вас не может быть async без await, так что именно вы ждете, когда не выполняется в каком-то другом потоке или ожидает завершения ввода-вывода? - person casperOne; 12.10.2012
comment
Для тех, кто хочет узнать больше, см. Здесь: chat.stackoverflow.com/rooms/17937 - @svick и я в основном неправильно понимали друг друга, но говорили одно и то же. - person casperOne; 12.10.2012

Стивен Тауб опубликовал этот AsyncManualResetEvent класс в своем блоге .

public class AsyncManualResetEvent 
{ 
    private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();

    public Task WaitAsync() { return m_tcs.Task; } 

    public void Set() 
    { 
        var tcs = m_tcs; 
        Task.Factory.StartNew(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), 
            tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default); 
        tcs.Task.Wait(); 
    }

    public void Reset() 
    { 
        while (true) 
        { 
            var tcs = m_tcs; 
            if (!tcs.Task.IsCompleted || 
                Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs) 
                return; 
        } 
    } 
}
person Drew Noakes    schedule 04.02.2016

С помощью реактивных расширений (Rx.Net)

var eventObservable = Observable
            .FromEventPattern<EventArgs>(
                h => example.YourEvent += h,
                h => example.YourEvent -= h);

var res = await eventObservable.FirstAsync();

Вы можете добавить Rx с помощью Nuget Package System.Reactive

Протестированный образец:

    private static event EventHandler<EventArgs> _testEvent;

    private static async Task Main()
    {
        var eventObservable = Observable
            .FromEventPattern<EventArgs>(
                h => _testEvent += h,
                h => _testEvent -= h);

        Task.Delay(5000).ContinueWith(_ => _testEvent?.Invoke(null, new EventArgs()));

        var res = await eventObservable.FirstAsync();

        Console.WriteLine("Event got fired");
    }
person Felix Keil    schedule 19.03.2018

Я использую свой собственный класс AsyncEvent для ожидаемых событий.

public delegate Task AsyncEventHandler<T>(object sender, T args) where T : EventArgs;

public class AsyncEvent : AsyncEvent<EventArgs>
{
    public AsyncEvent() : base()
    {
    }
}

public class AsyncEvent<T> where T : EventArgs
{
    private readonly HashSet<AsyncEventHandler<T>> _handlers;

    public AsyncEvent()
    {
        _handlers = new HashSet<AsyncEventHandler<T>>();
    }

    public void Add(AsyncEventHandler<T> handler)
    {
        _handlers.Add(handler);
    }

    public void Remove(AsyncEventHandler<T> handler)
    {
        _handlers.Remove(handler);
    }

    public async Task InvokeAsync(object sender, T args)
    {
        foreach (var handler in _handlers)
        {
            await handler(sender, args);
        }
    }

    public static AsyncEvent<T> operator+(AsyncEvent<T> left, AsyncEventHandler<T> right)
    {
        var result = left ?? new AsyncEvent<T>();
        result.Add(right);
        return result;
    }

    public static AsyncEvent<T> operator-(AsyncEvent<T> left, AsyncEventHandler<T> right)
    {
        left.Remove(right);
        return left;
    }
}

Чтобы объявить событие в классе, вызывающем события:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

Чтобы поднять события:

if (MyNormalEvent != null) await MyNormalEvent.InvokeAsync(this, new EventArgs());
if (MyCustomEvent != null) await MyCustomEvent.InvokeAsync(this, new ProgressEventArgs());

Чтобы подписаться на события:

MyControl.Click += async (sender, args) => {
    // await...
}

MyControl.Click += (sender, args) => {
    // synchronous code
    return Task.CompletedTask;
}
person cat_in_hat    schedule 26.07.2019
comment
Вы полностью изобрели новый механизм обработчика событий. Возможно, это то, на что в конечном итоге будут переведены делегаты в .NET, но не стоит ожидать, что люди это примут. Наличие типа возврата для самого делегата (события) может сначала отпугнуть людей. Но хорошее усилие, очень нравится, как хорошо это сделано. - person nawfal; 05.12.2019
comment
@nawfal Спасибо! С тех пор я изменил его, чтобы не возвращать делегата. Источник доступен здесь как часть Lara Web Engine, альтернатива Blazor. - person cat_in_hat; 05.12.2019

Вот небольшой набор из шести методов, которые можно использовать для преобразования событий в задачи:

/// <summary>Converts a .NET event, conforming to the standard .NET event pattern
/// based on <see cref="EventHandler"/>, to a Task.</summary>
public static Task EventToAsync(
    Action<EventHandler> addHandler,
    Action<EventHandler> removeHandler)
{
    var tcs = new TaskCompletionSource<object>();
    addHandler(Handler);
    return tcs.Task;

    void Handler(object sender, EventArgs e)
    {
        removeHandler(Handler);
        tcs.SetResult(null);
    }
}

/// <summary>Converts a .NET event, conforming to the standard .NET event pattern
/// based on <see cref="EventHandler{TEventArgs}"/>, to a Task.</summary>
public static Task<TEventArgs> EventToAsync<TEventArgs>(
    Action<EventHandler<TEventArgs>> addHandler,
    Action<EventHandler<TEventArgs>> removeHandler)
{
    var tcs = new TaskCompletionSource<TEventArgs>();
    addHandler(Handler);
    return tcs.Task;

    void Handler(object sender, TEventArgs e)
    {
        removeHandler(Handler);
        tcs.SetResult(e);
    }
}

/// <summary>Converts a .NET event, conforming to the standard .NET event pattern
/// based on a supplied event delegate type, to a Task.</summary>
public static Task<TEventArgs> EventToAsync<TDelegate, TEventArgs>(
    Action<TDelegate> addHandler, Action<TDelegate> removeHandler)
{
    var tcs = new TaskCompletionSource<TEventArgs>();
    TDelegate handler = default;
    Action<object, TEventArgs> genericHandler = (sender, e) =>
    {
        removeHandler(handler);
        tcs.SetResult(e);
    };
    handler = (TDelegate)(object)genericHandler.GetType().GetMethod("Invoke")
        .CreateDelegate(typeof(TDelegate), genericHandler);
    addHandler(handler);
    return tcs.Task;
}

/// <summary>Converts a named .NET event, conforming to the standard .NET event
/// pattern based on <see cref="EventHandler"/>, to a Task.</summary>
public static Task EventToAsync(object target, string eventName)
{
    var type = target.GetType();
    var eventInfo = type.GetEvent(eventName);
    if (eventInfo == null) throw new InvalidOperationException("Event not found.");
    var tcs = new TaskCompletionSource<object>();
    EventHandler handler = default;
    handler = new EventHandler((sender, e) =>
    {
        eventInfo.RemoveEventHandler(target, handler);
        tcs.SetResult(null);
    });
    eventInfo.AddEventHandler(target, handler);
    return tcs.Task;
}

/// <summary>Converts a named .NET event, conforming to the standard .NET event
/// pattern based on <see cref="EventHandler{TEventArgs}"/>, to a Task.</summary>
public static Task<TEventArgs> EventToAsync<TEventArgs>(
    object target, string eventName)
{
    var type = target.GetType();
    var eventInfo = type.GetEvent(eventName);
    if (eventInfo == null) throw new InvalidOperationException("Event not found.");
    var tcs = new TaskCompletionSource<TEventArgs>();
    EventHandler<TEventArgs> handler = default;
    handler = new EventHandler<TEventArgs>((sender, e) =>
    {
        eventInfo.RemoveEventHandler(target, handler);
        tcs.SetResult(e);
    });
    eventInfo.AddEventHandler(target, handler);
    return tcs.Task;
}

/// <summary>Converts a generic Action-based .NET event to a Task.</summary>
public static Task<TArgument> EventActionToAsync<TArgument>(
    Action<Action<TArgument>> addHandler,
    Action<Action<TArgument>> removeHandler)
{
    var tcs = new TaskCompletionSource<TArgument>();
    addHandler(Handler);
    return tcs.Task;

    void Handler(TArgument arg)
    {
        removeHandler(Handler);
        tcs.SetResult(arg);
    }
}

Все эти методы создают Task, который завершится при следующем вызове связанного события. Эта задача никогда не может быть сброшена или отменена, она может быть выполнена только успешно.

Пример использования со стандартным событием (Progress<T>.ProgressChanged ):

var p = new Progress<int>();

//...

int result = await EventToAsync<int>(
    h => p.ProgressChanged += h, h => p.ProgressChanged -= h);

// ...or...

int result = await EventToAsync<EventHandler<int>, int>(
    h => p.ProgressChanged += h, h => p.ProgressChanged -= h);

// ...or...

int result = await EventToAsync<int>(p, "ProgressChanged");

Пример использования с нестандартным событием:

public static event Action<int> MyEvent;

//...

int result = await EventActionToAsync<int>(h => MyEvent += h, h => MyEvent -= h);

Событие отменяется по завершении задачи. Механизма отказа от подписки раньше этого не предусмотрено.

person Theodor Zoulias    schedule 16.12.2020