Подтвердить что ты не робот

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

В моем приложении метро С#/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, более сложный, и в разных точках исполнения могут потребоваться различные типы пользовательских ввода. Поэтому разбиение логики на несколько методов было бы нетривиальным.

4b9b3361

Ответ 1

В качестве сигнала можно использовать экземпляр 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> Класса для создания Task <T> , который представляет результат нажатия кнопки:

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

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

// wait for task somewhere else
await tcs.Task;

Ответ 2

Если у вас есть необычная вещь, вам нужно await on, самый простой ответ - это TaskCompletionSource (или некоторый примитив async -enabled на основе 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).

Ответ 3

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

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;

Ответ 4

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

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

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

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

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

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

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 - это метод, который продолжает получать результаты в случае нажатия кнопки. Если ваша кнопка не нажата, ваш обработчик событий ничего не делает.

Ответ 5

Стивен Тууб опубликовал этот 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; 
        } 
    } 
}

Ответ 6

Класс простого помощника:

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;

Ответ 7

С реактивными расширениями (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");
    }

Ответ 8

Я использую свой собственный класс 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;
}