paint-brush
Ein einfaches Sicherheitsnetz für asynchrone EventHandlerby@devleader
750
750

Ein einfaches Sicherheitsnetz für asynchrone EventHandler

Dev Leader8m2023/02/14
Read on Terminal Reader
Read this story w/o Javascript

Async void ist die einzige Ausnahme, die wir offenbar für das gefürchtete asynchrone EventHandlers-Setup zulassen. In diesem Artikel stelle ich eine weitere Lösung vor, die Sie in Ihrem eigenen Code ausprobieren können. Wir werden die Vor- und Nachteile aus meiner Sicht in Bezug auf die Verwendungsmöglichkeit besprechen, damit Sie entscheiden können, ob es sinnvoll ist.
featured image - Ein einfaches Sicherheitsnetz für asynchrone EventHandler
Dev Leader HackerNoon profile picture

Wenn wir über asynchrone EventHandler sprechen, fällt vielen von uns als Erstes ein, dass dies die einzige Ausnahme ist, die wir scheinbar zulassen das gefürchtete asynchrone Void-Setup .


Als ich zuvor darüber geschrieben hatte, war ich begeistert, dass ich nach einer Lösung suchte, die darin bestand, die asynchrone Lücke tatsächlich existieren zu lassen (ohne mir den Rest der Haare ausreißen zu wollen).


Für mich ging es hier viel mehr um einige clevere Tricks, mit denen wir asynchrone EventHandler überwinden können, als darum, Lösungen bereitzustellen, um das Problem vollständig zu vermeiden.


Trotzdem fand der Artikel viel Anklang, wofür ich sehr dankbar bin, und einige Leute äußerten die Meinung, dass sie asynchrone EventHandler lieber auf andere Weise lösen würden.


Ich dachte, das wäre ein toller Punkt, deshalb wollte ich einen alternativen Ansatz entwickeln, der Async Void nicht behebt, sondern es Ihnen ermöglicht, ihn vollständig zu vermeiden (sehen Sie, was ich dort gemacht habe?), und gleichzeitig einige der Herausforderungen zu lösen mit asynchronen EventHandlern.


In diesem Artikel stelle ich eine weitere Lösung vor, die Sie in Ihrem eigenen Code ausprobieren können. Wir gehen auf die Vor- und Nachteile aus meiner Sicht hinsichtlich der Einsatzmöglichkeiten ein, damit Sie entscheiden können, ob es für Ihren Anwendungsfall sinnvoll ist.


Sie können auch interaktiven Code auf .NET fiddle right finden hier drüben . Ansonsten können Sie Schauen Sie sich den Code auf GitHub an Wenn Sie es lokal klonen möchten, um es auszuprobieren.

Ein Begleitvideo!

klicken Sie hier um das Video anzuschauen!

Das Problem

Das Problem, mit dem wir bei asynchronen EventHandlern konfrontiert sind, besteht darin, dass die Signatur für Ereignisse, die wir in C# standardmäßig abonnieren können, etwa so aussieht:


 void TheObject_TheEvent(object sender, EventArgs e);


Und Sie werden feststellen, dass wir durch die Angabe von „void“ am Anfang dieser Signatur gezwungen sind, „void“ in unseren eigenen Handlern zu verwenden, um das Ereignis zu abonnieren.


Das bedeutet, dass Sie, wenn Sie möchten, dass Ihr Handler jemals asynchronen/wartenden Code ausführt, in Ihrer void-Methode „wait“ verwenden müssen … Das führt zu dem großen, beängstigenden asynchronen Void-Muster, das wir wie die Pest vermeiden sollen.


Und warum? Weil async void die Fähigkeit von Ausnahmen beeinträchtigt, richtig aufzusteigen, und dadurch eine Menge Kopfschmerzen verursachen kann.


Der vorherige Artikel Wir haben dieses Problem gelöst, indem wir uns erlaubt haben, auf der Anrufungsseite kreativ zu werden, aber ...


  • Möglicherweise benötigen wir dafür Unterstützung für Objekte, für die wir den Aufruf von Ereignissen nicht steuern (z. B. Sie stellen eine Verbindung zum Klickereignis einer Schaltfläche in Ihrem bevorzugten UI-Framework her).


  • Manche Leute betrachten die Verwendung des Kontexts innerhalb dieser Lösung als Hack (ich bin auch nicht damit einverstanden).


  • … Insbesondere bei Event-Handlern haben wir noch einige andere, einfachere Tricks, mit denen wir asynchrone EventHandler unterstützen können!


Meiner Meinung nach ist einfach besser. Wenn Sie also meinen vorherigen Artikel über async void gelesen haben und Ihr Ziel eigentlich nur darin bestand, mit EventHandlern umzugehen, sollte dies hilfreich sein.

Asynchrone EventHandler mit Try/Catch lösen

Basierend auf den zuvor genannten Bedingungen bricht die Ausnahmebehandlung über die Grenze der asynchronen Lücke hinaus zusammen. Wenn Sie eine Ausnahme haben, die diese Grenze überschreiten muss, werden Sie eine lustige Zeit erleben.


Und mit Spaß meine ich, wenn Sie Spaß daran haben, zu debuggen, warum Dinge nicht funktionieren, und Sie keinen klaren Hinweis darauf haben, was kaputt ist, dann werden Sie wirklich eine tolle Zeit haben.


Was ist also der einfachste Weg, das Problem zu beheben?


Um zu verhindern, dass Ausnahmen diese Grenze überhaupt überschreiten, verwenden wir ein einfaches Tool, auf das wir Zugriff haben: try/catch.


 objectThatRaisesEvent.TheEvent += async (s, e) => { // if the try catch surrounds EVERYTHING in the handler, no exception can bubble up try { await SomeTaskYouWantToAwait(); } catch (Exception ex) { // TODO: put your exception handling stuff here } // no exception can escape here if the try/catch surrounds the entire handler body }


Wie im obigen Code erwähnt, können Sie verhindern, dass Ausnahmen über diese asynchrone Lückengrenze hinweg auftauchen, wenn Sie einen Try/Catch-Block um den GESAMTEN Hauptteil Ihres Ereignishandlers platzieren. Oberflächlich betrachtet ist es ganz einfach und erfordert nichts Besonderes, um es umzusetzen.


Vorteile:

  • Extrem einfach. Keine komplexen Mechanismen zu verstehen.


  • Keine Pakete erforderlich.


  • Damit dies funktioniert, müssen Sie nicht der Besitzer der Klasse sein, die das Ereignis auslöst. Dies bedeutet, dass dieser Ansatz für alle vorhandenen ereignisauslösenden Objekte funktioniert, einschließlich WinForms- und WPF-UI-Komponenten.


Nachteile:

  • Sie müssen daran denken, dies überall zu tun.


  • Wenn sich Ihr Code im Laufe der Zeit weiterentwickelt, ist es möglich, dass jemand versehentlich Logik außerhalb des Try-Catch-Bereichs des Ereignishandlers schreibt, die Ausnahmen auslösen kann


Trotzdem ist diese Lösung wirklich einfach, aber ich denke, wir können es noch ein bisschen besser machen.

Ein (etwas) ausgefallenerer Ansatz zur Verbesserung asynchroner EventHandler

Eine Verbesserung, die wir meiner Meinung nach gegenüber der ursprünglich vorgeschlagenen Lösung vornehmen können, besteht darin, dass wir etwas deutlicher machen können, dass wir einen asynchronen EventHandler haben, der vor der Entstehung von Ausnahmen geschützt sein sollte.


Dieser Ansatz verhindert auch, dass Codedrift im Laufe der Zeit dazu führt, dass problematischer Code außerhalb des Ereignishandlers ausgeführt wird. Es wird jedoch nicht auf die Tatsache eingegangen, dass Sie daran denken müssen, dies manuell hinzuzufügen!


Schauen wir uns den Code an:

 static class EventHandlers { public static EventHandler<TArgs> TryAsync<TArgs>( Func<object, TArgs, Task> callback, Action<Exception> errorHandler) where TArgs : EventArgs => TryAsync<TArgs>( callback, ex => { errorHandler.Invoke(ex); return Task.CompletedTask; }); public static EventHandler<TArgs> TryAsync<TArgs>( Func<object, TArgs, Task> callback, Func<Exception, Task> errorHandler) where TArgs : EventArgs { return new EventHandler<TArgs>(async (object s, TArgs e) => { try { await callback.Invoke(s, e); } catch (Exception ex) { await errorHandler.Invoke(ex); } }); } }


Der obige Code verwendet im wahrsten Sinne des Wortes genau den gleichen Ansatz, um zu verhindern, dass Ausnahmen die Async-Void-Grenze überschreiten. Wir versuchen einfach, den Hauptteil des Event-Handlers zu erfassen, aber jetzt haben wir ihn in einer explizit dedizierten Methode zur Wiederverwendung gebündelt.


So würde die Anwendung aussehen:


 someEventRaisingObject.TheEvent += EventHandlers.TryAsync<EventArgs>( async (s, e) => { Console.WriteLine("Starting the event handler..."); await SomeTaskToAwait(); Console.WriteLine("Event handler completed."); }, ex => Console.WriteLine($"[TryAsync Error Callback] Our exception handler caught: {ex}"));


Wir können sehen, dass wir jetzt einen Delegaten mit einer asynchronen Task-Signatur haben, mit dem wir arbeiten können, und alles, was wir darin einfügen, wird, wie wir sicher sind, innerhalb der Hilfsmethode, die wir zuvor gesehen haben, einen Try/Catch-Umgang haben.


Hier ist ein Screenshot, der zeigt, wie der Rückruf des Fehlerhandlers die Ausnahme richtig erfasst:


Ausgabe eines Beispielprogramms für asynchrone EventHandler

Vorteile:


  • Immer noch sehr einfach. Die Wrapper-Funktion ist *etwas* komplexer, aber immer noch sehr einfach.


  • Keine Pakete erforderlich.


  • Damit dies funktioniert, müssen Sie nicht der Besitzer der Klasse sein, die das Ereignis auslöst. Dies bedeutet, dass dieser Ansatz für alle vorhandenen ereignisauslösenden Objekte funktioniert, einschließlich WinForms- und WPF-UI-Komponenten.


  • Aufgrund der Syntax beim Einbinden des Handlers in das Ereignis ist die Absicht bei der Arbeit mit asynchronen EventHandlern offensichtlicher.


  • Codedrift, der schließlich weitere Ausnahmen auslöst, wird weiterhin in den Try/Catch eingeschlossen


Nachteile:


  • Sie müssen immer noch daran denken, dieses Ding anzuschließen!

Abschließende Gedanken zu asynchronen EventHandlern

Ursprünglich wollte ich die Gegend erkunden interessante Möglichkeiten, mit asynchroner Leere umzugehen , das Feedback der Leser war insofern gültig, als sich die Beispiele auf asynchrone EventHandler konzentrierten, und es muss sicherlich einen einfacheren Weg geben.


In diesem Artikel haben wir untersucht, was meiner Meinung nach die einfachste Möglichkeit ist, dafür zu sorgen, dass sich Ihre asynchronen EventHandler ordnungsgemäß verhalten, und die verfeinerte Lösung (meiner Meinung nach) hat nur den Nachteil, dass Sie daran denken müssen, sie zu verwenden.


Ein Kommentator hatte vorgeschlagen, dass man es erkunden könnte Aspektorientierte Programmierung (AoP), um diese Art von Verhalten in Ihre Anwendung einzubinden, sodass Sie nicht daran denken müssen, es zu tun.


Es gibt einige AoP-Frameworks zur Kompilierungszeit, aber ich überlasse das als Übung für Sie als Leser (weil es auch eine Übung ist, die ich weiterverfolgen kann).