Un patrón reutilizable para convertir el evento en tarea

Me gustaría tener una pieza genérica de código reutilizable para envolver el patrón EAP como tarea , algo similar a lo que hace BeginXXX/EndXXX para BeginXXX/EndXXX modelo APM .

P.ej:

 private async void Form1_Load(object sender, EventArgs e) { await TaskExt.FromEvent( handler => this.webBrowser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(handler), () => this.webBrowser.Navigate("about:blank"), handler => this.webBrowser.DocumentCompleted -= new WebBrowserDocumentCompletedEventHandler(handler), CancellationToken.None); this.webBrowser.Document.InvokeScript("setTimeout", new[] { "document.body.style.backgroundColor = 'yellow'", "1" }); } 

Hasta ahora, se ve así:

 public static class TaskExt { public static async Task FromEvent( Action<EventHandler> registerEvent, Action action, Action<EventHandler> unregisterEvent, CancellationToken token) { var tcs = new TaskCompletionSource(); EventHandler handler = (sender, args) => tcs.TrySetResult(args); registerEvent(handler); try { using (token.Register(() => tcs.SetCanceled())) { action(); return await tcs.Task; } } finally { unregisterEvent(handler); } } } 

¿Es posible encontrar algo similar que, sin embargo, no me obligue a escribir WebBrowserDocumentCompletedEventHandler dos veces (para registerEvent / unregisterEvent ), sin recurrir a la reflexión?

Es posible con una clase auxiliar y una syntax fluida:

 public static class TaskExt { public static EAPTask> FromEvent() { var tcs = new TaskCompletionSource(); var handler = new EventHandler((s, e) => tcs.TrySetResult(e)); return new EAPTask>(tcs, handler); } } public sealed class EAPTask where TEventHandler : class { private readonly TaskCompletionSource _completionSource; private readonly TEventHandler _eventHandler; public EAPTask( TaskCompletionSource completionSource, TEventHandler eventHandler) { _completionSource = completionSource; _eventHandler = eventHandler; } public EAPTask WithHandlerConversion( Converter converter) where TOtherEventHandler : class { return new EAPTask( _completionSource, converter(_eventHandler)); } public async Task Start( Action subscribe, Action action, Action unsubscribe, CancellationToken cancellationToken) { subscribe(_eventHandler); try { using(cancellationToken.Register(() => _completionSource.SetCanceled())) { action(); return await _completionSource.Task; } } finally { unsubscribe(_eventHandler); } } } 

Ahora tiene un método de ayuda WithHandlerConversion , que puede inferir el parámetro de tipo desde el argumento del convertidor, lo que significa que necesita escribir WebBrowserDocumentCompletedEventHandler solo una vez. Uso:

 await TaskExt .FromEvent() .WithHandlerConversion(handler => new WebBrowserDocumentCompletedEventHandler(handler)) .Start( handler => this.webBrowser.DocumentCompleted += handler, () => this.webBrowser.Navigate(@"about:blank"), handler => this.webBrowser.DocumentCompleted -= handler, CancellationToken.None); 

Tengo una solución mucho más corta (uso racional). Primero te mostraré el uso y luego te daré el código que hace que esto suceda (úsalo libremente).
uso, por ejemplo:

 await button.EventAsync(nameof(button.Click)); 

o:

 var specialEventArgs = await busniessObject.EventAsync(nameof(busniessObject.CustomerCreated)); 

o para Eventos que necesitan ser activados de alguna manera:

 var serviceResult = await service.EventAsync(()=> service.Start, nameof(service.Completed)); 

la magia que hace que esto suceda (tenga cuidado con la syntax de C # 7.1 pero puede convertirla fácilmente a versiones en idiomas más bajos agregando algunas líneas):

 using System; using System.Threading; using System.Threading.Tasks; namespace SpacemonsterIndustries.Core { public static class EventExtensions { ///  /// Extension Method that converts a typical EventArgs Event into an awaitable Task ///  /// The type of the EventArgs (must inherit from EventArgs) /// the object that has the event /// optional Function that triggers the event /// the name of the event -> use nameof to be safe, eg nameof(button.Click)  /// an optional Cancellation Token ///  public static async Task EventAsync(this object objectWithEvent, Action trigger, string eventName, CancellationToken ct = default) where TEventArgs : EventArgs { var completionSource = new TaskCompletionSource(ct); var eventInfo = objectWithEvent.GetType().GetEvent(eventName); var delegateDef = new UniversalEventDelegate(Handler); var handlerAsDelegate = Delegate.CreateDelegate(eventInfo.EventHandlerType, delegateDef.Target, delegateDef.Method); eventInfo.AddEventHandler(objectWithEvent, handlerAsDelegate); trigger?.Invoke(); var result = await completionSource.Task; eventInfo.RemoveEventHandler(objectWithEvent, handlerAsDelegate); return result; void Handler(object sender, TEventArgs e) => completionSource.SetResult(e); } public static Task EventAsync(this object objectWithEvent, string eventName, CancellationToken ct = default) where TEventArgs : EventArgs => EventAsync(objectWithEvent, null, eventName, ct); private delegate void UniversalEventDelegate(object sender, TEventArgs e) where TEventArgs : EventArgs; } } 

La conversión de EAP a Tareas no es tan sencilla, principalmente porque tiene que manejar excepciones tanto al llamar al método de larga ejecución como al manejar el evento.

La biblioteca ParallelExtensionsExtras contiene el método de extensión EAPCommon.HandleCompletion (TaskCompletionSource tcs, AsyncCompletedEventArgs e, Func getResult, Action unregisterHandler) para facilitar la conversión. El método maneja la suscripción / desuscripción de un evento. Tampoco trata de iniciar la operación de larga ejecución

Con este método, la biblioteca implementa versiones asíncronas de SmtpClient, WebClient y PingClient.

El siguiente método muestra el patrón de uso general:

  private static Task SendTaskCore(Ping ping, object userToken, Action> sendAsync) { // Validate we're being used with a real smtpClient. The rest of the arg validation // will happen in the call to sendAsync. if (ping == null) throw new ArgumentNullException("ping"); // Create a TaskCompletionSource to represent the operation var tcs = new TaskCompletionSource(userToken); // Register a handler that will transfer completion results to the TCS Task PingCompletedEventHandler handler = null; handler = (sender, e) => EAPCommon.HandleCompletion(tcs, e, () => e.Reply, () => ping.PingCompleted -= handler); ping.PingCompleted += handler; // Try to start the async operation. If starting it fails (due to parameter validation) // unregister the handler before allowing the exception to propagate. try { sendAsync(tcs); } catch(Exception exc) { ping.PingCompleted -= handler; tcs.TrySetException(exc); } // Return the task to represent the asynchronous operation return tcs.Task; } 

La principal diferencia con respecto a tu código está aquí:

 // Register a handler that will transfer completion results to the TCS Task PingCompletedEventHandler handler = null; handler = (sender, e) => EAPCommon.HandleCompletion(tcs, e, () => e.Reply, () => ping.PingCompleted -= handler); ping.PingCompleted += handler; 

El método de extensión crea el controlador y engancha los tcs. Su código establece el controlador al objeto de origen y comienza la operación larga. El tipo de controlador real no se escapa fuera del método.

Al separar las dos preocupaciones (manejar el evento frente a comenzar la operación) es más fácil crear un método genérico.

Creo que la siguiente versión podría ser lo suficientemente satisfactoria. Tomé prestada la idea de preparar un controlador de eventos correctamente tipeado a partir de la respuesta de max , pero esta implementación no crea ningún objeto adicional explícitamente.

Como efecto colateral positivo, le permite a la persona que llama cancelar o rechazar el resultado de la operación (con una excepción), según los argumentos del evento (como AsyncCompletedEventArgs.Cancelled , AsyncCompletedEventArgs.Error ).

El TaskCompletionSource subyacente TaskCompletionSource está completamente oculto de la persona que llama (por lo que podría ser reemplazado por otra cosa, por ejemplo, un awaiter personalizado o una promesa personalizada ):

 private async void Form1_Load(object sender, EventArgs e) { await TaskExt.FromEvent( getHandler: (completeAction, cancelAction, rejectAction) => (eventSource, eventArgs) => completeAction(eventArgs), subscribe: eventHandler => this.webBrowser.DocumentCompleted += eventHandler, unsubscribe: eventHandler => this.webBrowser.DocumentCompleted -= eventHandler, initiate: (completeAction, cancelAction, rejectAction) => this.webBrowser.Navigate("about:blank"), token: CancellationToken.None); this.webBrowser.Document.InvokeScript("setTimeout", new[] { "document.body.style.backgroundColor = 'yellow'", "1" }); } 

 public static class TaskExt { public static async Task FromEvent( Func, Action, Action, TEventHandler> getHandler, Action subscribe, Action unsubscribe, Action, Action, Action> initiate, CancellationToken token = default(CancellationToken)) where TEventHandler : class { var tcs = new TaskCompletionSource(); Action complete = args => tcs.TrySetResult(args); Action cancel = () => tcs.TrySetCanceled(); Action reject = ex => tcs.TrySetException(ex); TEventHandler handler = getHandler(complete, cancel, reject); subscribe(handler); try { using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false)) { initiate(complete, cancel, reject); return await tcs.Task; } } finally { unsubscribe(handler); } } } 


De hecho, esto se puede utilizar para esperar cualquier callback, no solo controladores de eventos, por ejemplo:

 var mre = new ManualResetEvent(false); RegisteredWaitHandle rwh = null; await TaskExt.FromEvent( (complete, cancel, reject) => (state, timeout) => { if (!timeout) complete(true); else cancel(); }, callback => rwh = ThreadPool.RegisterWaitForSingleObject(mre, callback, null, 1000, true), callback => rwh.Unregister(mre), (complete, cancel, reject) => ThreadPool.QueueUserWorkItem(state => { Thread.Sleep(500); mre.Set(); }), CancellationToken.None);