paint-brush
Un filet de sécurité simple pour les gestionnaires d'événements asynchronespar@devleader
818 lectures
818 lectures

Un filet de sécurité simple pour les gestionnaires d'événements asynchrones

par Dev Leader8m2023/02/14
Read on Terminal Reader

Trop long; Pour lire

Async void est la seule exception que nous semblons autoriser pour la configuration redoutée des EventHandlers asynchrones. Dans cet article, je vais présenter une autre solution que vous pouvez essayer dans votre propre code. Nous aborderons les avantages et les inconvénients de mon point de vue en ce qui concerne la façon dont il peut être utilisé afin que vous puissiez décider si cela a du sens.
featured image - Un filet de sécurité simple pour les gestionnaires d'événements asynchrones
Dev Leader HackerNoon profile picture

Lorsque nous discutons des gestionnaires d'événements asynchrones, la première chose qui vient à l'esprit pour beaucoup d'entre nous est que c'est la seule exception que nous semblons autoriser la redoutable configuration du vide asynchrone .


Quand j'avais écrit à ce sujet auparavant, j'étais ravi d'explorer une solution qui impliquait de permettre au vide asynchrone d'exister (sans vouloir arracher le reste de mes cheveux).


Pour moi, il s'agissait bien plus de quelques astuces astucieuses que nous pouvons utiliser pour surmonter les EventHandlers asynchrones que de fournir des solutions pour éviter complètement le problème.


Cela dit, l'article a suscité beaucoup d'intérêt, ce dont je suis très reconnaissant, et certaines personnes ont exprimé leur opinion selon laquelle elles préféreraient résoudre les EventHandlers asynchrones d'une manière différente.


J'ai pensé que c'était un bon point, alors je voulais proposer une approche alternative qui ne corrige pas le vide asynchrone, mais qui vous permet de l'éviter (vous voyez ce que j'ai fait là-bas ?) tout en résolvant certains des défis avec des gestionnaires d'événements asynchrones.


Dans cet article, je vais présenter une autre solution que vous pouvez essayer dans votre propre code. Nous aborderons les avantages et les inconvénients de mon point de vue en ce qui concerne la façon dont il peut être utilisé afin que vous puissiez décider si cela a du sens pour votre cas d'utilisation.


Vous pouvez également trouver du code interactif sur le violon .NET à droite par ici . Sinon, vous pouvez vérifier le code sur GitHub si vous souhaitez le cloner localement pour l'essayer.

Une vidéo d'accompagnement !

Cliquez ici pour voir la vidéo !

Le problème

Le problème auquel nous sommes confrontés avec les gestionnaires d'événements asynchrones est que la signature des événements auxquels nous pouvons nous abonner en C# ressemble par défaut à ceci :


 void TheObject_TheEvent(object sender, EventArgs e);


Et, vous remarquerez qu'en ayant void au début de cette signature, nous sommes obligés d'utiliser void dans nos propres gestionnaires afin de souscrire à l'événement.


Cela signifie que si vous voulez que votre gestionnaire exécute du code async/wait, vous devrez attendre dans votre méthode void… Ce qui introduit le grand motif void async effrayant qu'on nous dit d'éviter comme la peste.


Et pourquoi? Parce que le vide asynchrone empêche les exceptions de se propager correctement et peut en conséquence causer une tonne de maux de tête.


L'article précédent a résolu ce problème en nous permettant de faire preuve de créativité du côté de l'invocation des choses, mais…


  • Nous pourrions avoir besoin d'une prise en charge pour cela sur les objets pour lesquels nous ne contrôlons pas l'invocation d'événements (c'est-à-dire que vous vous connectez à l'événement de clic d'un bouton dans votre framework d'interface utilisateur préféré)


  • Certaines personnes voient l'utilisation du contexte à l'intérieur de cette solution comme un hack (je ne suis pas en désaccord avec cela non plus).


  • … Plus précisément, avec les gestionnaires d'événements, nous avons d'autres astuces plus simples que nous pouvons appliquer pour prendre en charge les gestionnaires d'événements asynchrones !


À mon avis, la simplicité est préférable… donc si vous avez lu mon article précédent sur le vide asynchrone et que votre objectif était vraiment de gérer les EventHandlers, cela devrait vous aider.

Résolution des gestionnaires d'événements asynchrones avec Try/Catch

Sur la base des conditions énoncées précédemment, la gestion des exceptions échoue au-delà de la limite du vide asynchrone. Si vous avez une exception qui doit franchir cette frontière, vous passerez un bon moment.


Et par plaisir, je veux dire si vous aimez déboguer pourquoi les choses ne fonctionnent pas et que vous n'avez pas d'indication claire sur ce qui ne va pas, alors vous passerez vraiment un bon moment.


Alors, quel est le moyen le plus simple de résoudre ce problème ?


Empêchez les exceptions de franchir cette limite en premier lieu en utilisant un outil simple auquel nous avons accès : 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 }


Comme indiqué dans le code ci-dessus, si vous placez un bloc try/catch autour du corps ENTIER de votre gestionnaire d'événements, vous pouvez empêcher toute exception de se propager à travers cette limite vide asynchrone. En surface, c'est assez simple et ne nécessite rien d'extraordinaire pour l'implémenter.


Avantages:

  • Extrêmement simple. Pas de mécanismes complexes à comprendre.


  • Aucun forfait requis.


  • Vous n'avez pas besoin d'être le propriétaire de la classe qui déclenche l'événement pour que cela fonctionne. Cela signifie que cette approche fonctionnera pour tous les objets de déclenchement d'événements existants, y compris les composants d'interface utilisateur WinForms et WPF.


Les inconvénients:

  • Vous devez vous rappeler de le faire… partout.


  • Il est possible qu'au fur et à mesure que votre code évolue avec le temps, quelqu'un écrive accidentellement une logique en dehors du try-catch du gestionnaire d'événements qui peut lever des exceptions


Cela dit, cette solution est vraiment simple, mais je pense que nous pouvons faire un peu mieux.

Une approche (légèrement) plus sophistiquée pour améliorer les gestionnaires d'événements asynchrones

Une amélioration que je pense que nous pouvons apporter par rapport à la solution initialement proposée est que nous pouvons rendre un peu plus explicite le fait que nous avons un EventHandler asynchrone qui devrait être à l'abri des exceptions.


Cette approche empêchera également la dérive du code au fil du temps de provoquer l'exécution de code problématique en dehors du gestionnaire d'événements. Cependant, cela n'abordera pas le fait que vous devez vous rappeler de l'ajouter manuellement !


Vérifions le code :

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


Le code ci-dessus utilise littéralement exactement la même approche pour empêcher les exceptions de franchir la limite vide asynchrone. Nous essayons simplement de comprendre le corps du gestionnaire d'événements, mais nous l'avons maintenant regroupé dans une méthode explicitement dédiée à réutiliser.


Voici à quoi cela ressemblerait pour l'appliquer:


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


Nous pouvons voir que nous avons maintenant un délégué avec une signature de tâche asynchrone avec laquelle travailler, et tout ce que nous mettons à l'intérieur de celui-ci, nous sommes assurés qu'il y aura un try/catch autour de lui dans la méthode d'assistance que nous avons vue précédemment.


Voici une capture d'écran montrant le rappel du gestionnaire d'erreurs capturant correctement l'exception :


Sortie d'un exemple de programme pour les gestionnaires d'événements asynchrones

Avantages:


  • Toujours très simple. La fonction wrapper est *légèrement* plus complexe, mais reste très basique.


  • Aucun forfait requis.


  • Vous n'avez pas besoin d'être le propriétaire de la classe qui déclenche l'événement pour que cela fonctionne. Cela signifie que cette approche fonctionnera pour tous les objets de déclenchement d'événements existants, y compris les composants d'interface utilisateur WinForms et WPF.


  • L'intention est plus évidente de travailler avec des gestionnaires d'événements asynchrones en raison de la syntaxe lors de la connexion du gestionnaire à l'événement.


  • La dérive de code qui finit par lever plus d'exceptions sera toujours enveloppée dans le try/catch


Les inconvénients:


  • Vous devez toujours vous rappeler de brancher cette chose!

Réflexions finales sur les gestionnaires d'événements asynchrones

Alors qu'à l'origine j'avais entrepris d'explorer façons intéressantes de gérer le vide asynchrone , les commentaires des lecteurs étaient valables dans la mesure où les exemples se concentraient sur les EventHandlers asynchrones, et il doit sûrement y avoir un moyen plus simple.


Dans cet article, nous avons exploré ce que je pourrais considérer comme le moyen le plus simple de faire en sorte que vos gestionnaires d'événements asynchrones se comportent correctement, et la solution raffinée (à mon avis) n'a que l'inconvénient que vous devez vous rappeler de l'utiliser.


Un commentateur avait suggéré que l'on pourrait explorer Programmation orientée aspect (AoP) pour injecter ce type de comportement dans votre application afin que vous n'ayez pas à vous rappeler de le faire.


Il existe des frameworks AoP au moment de la compilation, mais je vais laisser cela comme un exercice pour vous en tant que lecteur (car c'est aussi un exercice pour moi de suivre).