without run practices method explained best await async c# microsoft-metro .net-4.5 async-await windows-store-apps

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



run method async c# (7)

Aquí hay una clase de utilidad que utilizo:

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;

En mi aplicación de metro C # / XAML, hay un botón que inicia un proceso de larga ejecución. Por lo tanto, como se recomienda, estoy usando async / await para asegurarme de que el subproceso de UI 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?

Esta es 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 votación es claramente terrible (espera ocupada / pérdida 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 Continuar. En realidad, las cosas que suceden en GetResults son más complejas y se pueden requerir diferentes tipos de entradas de usuario en diferentes puntos dentro de la ejecución. Entonces, dividir la lógica en múltiples métodos no sería trivial.


Cuando tiene algo inusual que necesita await , la respuesta más fácil es a menudo TaskCompletionSource (o alguna primitiva con async basada en TaskCompletionSource ).

En este caso, su necesidad es bastante simple, por lo que 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 ManualResetEvent async , excepto que solo se puede "configurar" el evento una vez y el evento puede tener un "resultado" (en este caso, no lo estamos usando, así que simplemente establecemos el resultado en null ) .


Idealmente, tu no . Si bien puedes bloquear el hilo asincrónico, es una pérdida de recursos y no es ideal.

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

Si ha detenido su código asíncrono mientras esperaba la entrada del usuario, entonces está desperdiciando recursos mientras el hilo está en pausa.

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

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

Porque SynchronizationContext se capturará en el controlador de eventos que llama a GetResults (el compilador hará esto como resultado del uso de la palabra clave await está utilizando, y el hecho de que SynchronizationContext.Current no sea nulo, dado que se encuentra en una aplicación de interfaz de usuario ), puedes usar 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 su botón no está presionado, su controlador de eventos no hace nada.


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 TaskCompletionSource <T> Class para crear una Task<T> 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;


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


Clase de ayuda 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;


Con Reactive Extensions (Rx.Net)

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

Puede agregar Rx con Nuget Package System.Reactive