it-swarm.com.ru

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

В моем приложении C #/XAML metro есть кнопка, которая запускает длительный процесс. Итак, как я рекомендовал, я использую 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, являются более сложными, и в разных точках выполнения могут потребоваться разные типы пользовательского ввода. Так что разбить логику на несколько методов было бы нетривиально.

135
Max

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

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;
193
dtb

Если у вас есть что-то необычное, вам нужно включить 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 похож на asyncManualResetEvent, за исключением того, что вы можете «установить» событие только один раз, и событие может иметь «результат» (в этом случае мы его не используем, поэтому мы просто устанавливаем результат в null) ,.

62
Stephen Cleary

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

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;
5
Anders Skovborg

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

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

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

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

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

Поскольку SynchronizationContext будет захвачен в обработчике событий, который вызывает GetResults (компилятор сделает это в результате использования используемого ключевого слова await, а также того факта, что SynchronizationContext.Current должен быть ненулевым Если вы находитесь в приложении пользовательского интерфейса), вы можете использовать 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 - это метод, который продолжает получать результаты в случае нажатия вашей кнопки. Если ваша кнопка нажата not, то ваш обработчик событий ничего не делает.

4
casperOne

Простой хелпер класс:

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;
3
Felix Keil

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

With Реактивные расширения (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");
    }
0
Felix Keil