¿Es posible esperar un evento en lugar de otro método asíncrono?
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.
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;
Cuando necesita algo inusual await
, la respuesta más fácil es a menudo TaskCompletionSource
(o alguna async
primitiva habilitada según TaskCompletionSource
).
En este caso, tu necesidad es bastante simple, por lo que puedes 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 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
).
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;
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 GetResults
mé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 SynchronizationContext
capturará en el controlador de eventos que llama GetResults
(el compilador hará esto como resultado del uso de la await
palabra 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)
...
}
ContinueToGetResultsAsync
es 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.