¿Es posible esperar un evento en lugar de otro método asíncrono?

Resuelto Max asked hace 12 años • 11 respuestas

En mi aplicación metro C#/XAML, hay un botón que inicia un proceso de larga duración. Entonces, según lo recomendado, estoy usando async/await para asegurarme de que el hilo 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, lo que sucede dentro de GetResults requeriría una entrada adicional del usuario antes de poder continuar. Para simplificar, digamos que el usuario sólo 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 manera 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;

La encuesta es claramente terrible (espera ocupada / desperdicio de ciclos) y estoy buscando algo basado en eventos.

¿Algunas ideas?

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 de inicio y la segunda parte desde el botón de continuar. En realidad, lo que sucede en GetResults es más complejo y se pueden requerir diferentes tipos de entrada del usuario en diferentes puntos de la ejecución. Por lo tanto, dividir la lógica en múltiples métodos no sería trivial.

Max avatar Oct 12 '12 18:10 Max
Aceptado

Puede utilizar una instancia de la clase SemaphoreSlim como 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<T> para crear una Task<T> que represente el resultado del clic en el botón:

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

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

// wait for task somewhere else
await tcs.Task;
dtb avatar Oct 12 '2012 12:10 dtb

Cuando necesita algo inusual await, la respuesta más fácil es a menudo TaskCompletionSource(o alguna asyncprimitiva habilitada según TaskCompletionSource).

En este caso, tu necesidad es bastante simple, por lo que puedes usar TaskCompletionSourcedirectamente:

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, TaskCompletionSourcees como un async ManualResetEvent, excepto que sólo puedes "establecer" el evento una vez y el evento puede tener un "resultado" (en este caso, no lo estamos usando, por lo que simplemente configuramos el resultado en null).

Stephen Cleary avatar Oct 12 '2012 14:10 Stephen Cleary

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;
Anders Skovborg avatar Aug 02 '2016 09:08 Anders Skovborg

Lo ideal es que no lo hagas . Si bien es cierto que puedes bloquear el hilo asíncrono, eso es un desperdicio de recursos y no es lo ideal.

Considere el ejemplo canónico en el que el usuario va a almorzar mientras el botón espera que lo hagan clic.

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

Dicho esto, es mejor si en su operación asincrónica establece el estado que necesita mantener hasta el punto en que el botón esté habilitado y esté "esperando" un clic. En ese punto, su GetResultsmétodo se detiene .

Luego, cuando se hace clic en el botón, según el estado que haya almacenado, inicia otra tarea asincrónica para continuar el trabajo.

Debido a que se SynchronizationContextcapturará en el controlador de eventos que llama GetResults(el compilador hará esto como resultado del uso de la awaitpalabra clave que se está utilizando y del hecho de que SynchronizationContext.Current no debe ser nulo, dado que se encuentra en una aplicación de interfaz de usuario), usted puedo usar async/await me gusta 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)
  ...
}

ContinueToGetResultsAsynces el método que continúa obteniendo los resultados en caso de que se presione el botón. Si no se presiona el botón , entonces el controlador de eventos no hace nada.

casperOne avatar Oct 12 '2012 12:10 casperOne