Ein wiederverwendbares Muster, um ein Ereignis in eine Aufgabe umzuwandeln

Ich hätte gerne einen generischen Code, der wiederverwendbar ist, um EAP-Muster als Aufgabe zu Task.Factory.FromAsync , ähnlich wie Task.Factory.FromAsync für BeginXXX/EndXXX APM-Muster .

Z.B:

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

Bisher sieht es so aus:

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

Ist es möglich, etwas Ähnliches zu finden, das jedoch nicht erfordert, dass ich zweimal WebBrowserDocumentCompletedEventHandler (für registerEvent / unregisterEvent ) unregisterEvent , ohne auf eine Reflektion zurückzugreifen?

Es ist möglich mit einer Hilfsklasse und einer fließenden Syntax:

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

Jetzt haben Sie eine WithHandlerConversion , die den Typparameter vom Konverterargument ableiten kann, was bedeutet, dass Sie WebBrowserDocumentCompletedEventHandler nur einmal schreiben müssen. Verwendung:

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

Ich habe eine (Gebrauchs weise) viel kürzere Lösung. Ich zeige Ihnen zuerst die Verwendung und dann geben Sie den Code, der dies ermöglicht (verwenden Sie es frei).
Verwendung zB:

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

oder:

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

oder für Ereignisse, die auf irgendeine Art ausgetriggers werden müssen:

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

die Magie, die dies ermöglicht (Vorsicht, es ist die C # 7.1-Syntax, aber kann leicht durch Hinzufügen von ein paar Zeilen zurück zu niedrigeren Sprachversionen konvertiert werden):

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

Die Konvertierung von EAP in Tasks ist nicht so einfach, vor allem, weil Sie sowohl beim Aufruf der Long-Running-Methode als auch bei der Behandlung des Ereignisses mit Exceptions umgehen müssen.

Die ParallelExtensionsExtras-Bibliothek enthält die Erweiterungsmethode EAPCommon.HandleCompletion (TaskCompletionSource tcs, AsyncCompletedEventArgs, Func getResult, Action unregisterHandler) , um die Konvertierung zu vereinfachen. Die Methode behandelt das Abonnieren / Abmelden von einem Ereignis. Es wird auch nicht versucht, den Langzeitbetrieb zu starten

Mit dieser Methode implementiert die Bibliothek asynchrone Versionen von SmtpClient, WebClient und PingClient.

Die folgende Methode zeigt das allgemeine Verwendungsmuster:

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

Der Hauptunterschied zu deinem Code ist hier:

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

Die Erweiterungsmethode erstellt den Handler und hakt die tcs. Ihr Code setzt den Handler auf das Quellobjekt und startet den langen Vorgang. Der tatsächliche Behandlungstyp tritt nicht außerhalb der Methode auf.

Durch die Trennung der beiden Probleme (Behandeln des Ereignisses vs. Starten der Operation) ist es einfacher, eine generische Methode zu erstellen.

Ich denke, dass die folgende Version zufriedenstellend genug sein könnte. Ich habe die Idee, einen korrekt typisierten Event-Handler aus der Antwort von max zu erstellen, ausgeliehen, aber diese Implementierung erzeugt kein zusätzliches Objekt explizit.

Als positiven Nebeneffekt ermöglicht es dem Aufrufer, das Ergebnis der Operation (mit einer Ausnahme) basierend auf den Argumenten des Ereignisses (wie AsyncCompletedEventArgs.Cancelled , AsyncCompletedEventArgs.Error ) abzubrechen oder abzulehnen.

Die zugrundeliegende TaskCompletionSource ist dem Aufrufer immer noch vollständig verborgen (also könnte sie durch etwas anderes ersetzt werden, zB einen benutzerdefinierten Warner oder eine benutzerdefinierte Versprechung ):

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


Dies kann tatsächlich dazu verwendet werden, auf jeden Callback zu warten, nicht nur auf Event-Handler, zB:

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