¿Es posible esperar un evento en lugar de otro método asincrónico?


En mi aplicación C#/XAML metro, hay un botón que inicia un proceso de larga duración. Por lo tanto, como se recomienda, estoy usando async / await para asegurarme de que el subproceso de la interfaz de usuario no se bloquee:

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)
  ...
}

Ocasionalmente, las cosas que suceden dentro de GetResults requerirían una entrada adicional del usuario antes de que pueda continuar. Para simplificar, digamos que el usuario solo tiene que hacer clic en el botón "continuar".

Mi pregunta es: ¿cómo puedo suspender la ejecución de GetResults de tal manera que espere un evento como el clic de otro botón?

Aquí hay una forma fea de lograr lo que estoy buscando: el controlador de eventos para el botón continuar" establece una bandera...

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

... y GetResults lo sondea periódicamente:

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

El sondeo es claramente terrible (espera ocupada / pérdida de ciclos) y estoy buscando algo basado en eventos.

¿Alguna idea?

Por cierto, en este ejemplo simplificado, una solución sería, por supuesto, dividir GetResults() en dos partes, invocar la primera parte desde el botón inicio y la segunda parte desde el botón continuar. En realidad, las cosas que suceden en GetResults son más complejas y se pueden requerir diferentes tipos de entrada de usuario en diferentes puntos dentro de la ejecución. Así que dividir la lógica en múltiples métodos no sería trivial.

Author: Tim Medora, 2012-10-12

7 answers

Puede usar una instancia de la Clase SemaphoreSlim como una señal:

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

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

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

Alternativamente, puede usar una instancia de la clase TaskCompletionSource para crear una tarea que represente el resultado del clic del botón:

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

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

// wait for task somewhere else
await tcs.Task;
 172
Author: dtb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-12 22:03:27

Cuando tienes algo inusual que necesitas await encendido, la respuesta más fácil es a menudo TaskCompletionSource (o algún primitivo habilitado async basado en TaskCompletionSource).

En este caso, su necesidad es bastante simple, por lo que solo puede usar TaskCompletionSource directamente:

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);
}

Lógicamente, TaskCompletionSource es como un async ManualResetEvent, excepto que solo puede " establecer "el evento una vez y el evento puede tener un" resultado " (en este caso, no lo estamos usando, por lo que solo establecemos el resultado en null).

 58
Author: Stephen Cleary,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-05 13:19:24

Lo ideal es que no. Si bien ciertamente puede bloquear el hilo asincrónico, eso es un desperdicio de recursos, y no lo ideal.

Considere el ejemplo canónico donde el usuario va a almorzar mientras el botón espera que se haga clic.

Si ha detenido su código asincrónico mientras espera la entrada del usuario, entonces solo está desperdiciando recursos mientras ese hilo está en pausa.

Dicho esto, es mejor si en su operación asíncrona, establece el estado que es necesario mantener hasta el punto en el que el botón está habilitado y usted está "esperando" en un clic. En ese punto, su GetResults método se detiene.

Luego, cuando se hace clic en el botón , en función del estado que haya almacenado, inicia otra tarea asíncrona para continuar el trabajo.

Porque el SynchronizationContext se capturará en el controlador de eventos que llama a GetResults (el compilador hará esto como resultado de usar la palabra clave await que se está utilizando, y hecho de que SynchronizationContext.Current should be non-null, given you are in a UI application), you can use async/await así:

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 es el método que continúa obteniendo los resultados en caso de que se presione el botón. Si el botón no está pulsado, entonces el controlador de eventos no hace nada.

 4
Author: casperOne,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-12 13:29:57

Aquí hay una clase de utilidad que uso:

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; }
}

Y así es como lo uso:

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

// ... make it change ...

await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;
 3
Author: Anders Skovborg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-03 06:58:38

Clase Auxiliar Simple:

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);
        };
}

Uso:

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
                            h => example.YourEvent += h,
                            h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;
 3
Author: Felix Keil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-19 14:50:19

Stephen Toub publicó esta clase AsyncManualResetEvent en su blog .

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; 
        } 
    } 
}
 2
Author: Drew Noakes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-04 21:26:25

Con Extensiones reactivas (Rx.Net)

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

var res = await eventObservable.FirstAsync();

Puede agregar Rx con el Sistema de paquetes Nuget.Reactivo

 -1
Author: Felix Keil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-19 15:11:35