paint-brush
Uma rede de segurança simples para manipuladores de eventos assíncronosby@devleader
750
750

Uma rede de segurança simples para manipuladores de eventos assíncronos

Dev Leader8m2023/02/14
Read on Terminal Reader

Async void é a única exceção que parece permitir para a temida configuração async EventHandlers. Neste artigo, apresentarei outra solução que você pode experimentar em seu próprio código. Abordaremos os prós e os contras da minha perspectiva com relação a como ele pode ser usado para que você decida se faz sentido.
featured image - Uma rede de segurança simples para manipuladores de eventos assíncronos
Dev Leader HackerNoon profile picture

Quando discutimos EventHandlers assíncronos, a primeira coisa que vem à mente de muitos de nós é que é a única exceção que parece permitir a temida configuração do vazio assíncrono .


Quando escrevi sobre isso antes, fiquei empolgado por estar explorando uma solução que envolvia realmente permitir que o vazio assíncrono existisse (sem querer arrancar o resto do meu cabelo).


Para mim, isso foi muito mais sobre alguns truques inteligentes que podemos usar para superar EventHandlers assíncronos do que fornecer soluções para evitar o problema completamente.


Com isso dito, porém, houve muita tração no artigo, pelo qual sou muito grato, e algumas pessoas expressaram opiniões de que preferem resolver EventHandlers assíncronos de uma maneira diferente.


Eu pensei que este era um ótimo ponto, então eu queria criar uma abordagem alternativa que não corrija o vazio assíncrono, mas permite que você o evite (viu o que eu fiz lá?) inteiramente ao resolver alguns dos desafios com EventHandlers assíncronos.


Neste artigo, apresentarei outra solução que você pode experimentar em seu próprio código. Abordaremos os prós e os contras da minha perspectiva com relação a como ele pode ser usado para que você decida se faz sentido para o seu caso de uso.


Você também pode encontrar algum código interativo no violino .NET à direita por aqui . Caso contrário, você pode verifique o código no GitHub se você gostaria de cloná-lo localmente para experimentá-lo.

Um vídeo complementar!

Clique aqui para conferir o vídeo!

O problema

O problema que enfrentamos com EventHandlers assíncronos é que a assinatura para eventos que podemos assinar em C# por padrão é algo como isto:


 void TheObject_TheEvent(object sender, EventArgs e);


E você notará que, ao colocar void na frente dessa assinatura, somos forçados a usar void em nossos próprios manipuladores para assinar o evento.


Isso significa que, se você quiser que seu manipulador execute o código async/await, você precisará aguardar dentro do seu método void… O que introduz o grande e assustador padrão void assíncrono que nos dizem para evitar como a praga.


E porque? Porque o async void interrompe a capacidade de as exceções surgirem corretamente e pode causar uma tonelada de dores de cabeça como resultado.


o artigo anterior abordou isso, permitindo-nos ser criativos no lado da invocação das coisas, mas…


  • Podemos precisar de suporte para isso em objetos para os quais não controlamos a invocação de eventos (ou seja, você está se conectando ao evento de clique de um botão em sua estrutura de interface do usuário favorita)


  • Algumas pessoas veem o uso do contexto dentro dessa solução como um hack (também não discordo disso).


  • … Especificamente, com manipuladores de eventos, temos alguns outros truques mais simples que podemos fazer para oferecer suporte a EventHandlers assíncronos!


Na minha opinião, simples é melhor... então, se você leu meu artigo anterior sobre async void e seu objetivo era apenas lidar com EventHandlers, isso deve ajudar.

Resolvendo EventHandlers assíncronos com Try/Catch

Com base nas condições declaradas anteriormente, o tratamento de exceção é interrompido no limite do vazio assíncrono. Se você tiver uma exceção que precisa borbulhar cruzando esse limite, você terá um momento divertido.


E por diversão, quero dizer, se você gosta de depurar por que as coisas não estão funcionando e não tem uma indicação clara do que está acontecendo, então você realmente se divertirá.


Então, qual é a maneira mais fácil de corrigir isso?


Vamos evitar que as exceções ultrapassem esse limite em primeiro lugar, usando uma ferramenta simples à qual temos acesso: 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 }


Conforme observado no código acima, se você colocar um bloco try/catch em TODO o corpo do seu manipulador de eventos, poderá impedir que qualquer exceção borbulhe além desse limite vazio assíncrono. Superficialmente, é bastante simples e não requer nada sofisticado para implementá-lo.


Prós:

  • Extremamente simples. Sem mecanismos complexos para entender.


  • Não são necessários pacotes.


  • Você não precisa ser o proprietário da classe que gera o evento para que isso funcione. Isso significa que essa abordagem funcionará para todos os objetos geradores de eventos existentes, incluindo componentes WinForms e WPF UI.


Contras:

  • Você precisa se lembrar de fazer isso... em todos os lugares.


  • É possível que, à medida que seu código evolua com o tempo, alguém acidentalmente escreva lógica fora do try-catch do manipulador de eventos que pode lançar exceções


Com isso dito, esta solução é realmente simples, mas acho que podemos fazer um pouco melhor.

Uma abordagem (ligeiramente) mais sofisticada para melhorar os EventHandlers assíncronos

Uma melhoria que acho que podemos fazer sobre a solução proposta inicialmente é que podemos tornar um pouco mais explícito que temos um EventHandler assíncrono que deve estar protegido contra exceções borbulhantes.


Essa abordagem também impedirá que o desvio de código ao longo do tempo cause a execução de códigos problemáticos fora do manipulador de eventos. No entanto, não abordará o fato de que você precisa se lembrar de adicionar isso manualmente!


Vamos verificar o código:

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


O código acima literalmente usa exatamente a mesma abordagem para impedir que exceções cruzem o limite do vazio assíncrono. Simplesmente tentamos capturar o corpo do manipulador de eventos, mas agora o agrupamos em um método explicitamente dedicado para reutilização.


Veja como seria aplicá-lo:


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


Podemos ver que agora temos um delegado com uma assinatura de tarefa assíncrona para trabalhar, e qualquer coisa que colocarmos dentro dela, temos certeza de que terá um try/catch dentro do método auxiliar que vimos anteriormente.


Aqui está uma captura de tela mostrando o retorno de chamada do manipulador de erros capturando corretamente a exceção:


Saída do programa de exemplo para EventHandlers assíncronos

Prós:


  • Ainda muito simples. A função wrapper é *ligeiramente* mais complexa, mas ainda muito básica.


  • Não são necessários pacotes.


  • Você não precisa ser o proprietário da classe que gera o evento para que isso funcione. Isso significa que essa abordagem funcionará para todos os objetos geradores de eventos existentes, incluindo componentes WinForms e WPF UI.


  • A intenção é mais óbvia para trabalhar com EventHandlers assíncronos por causa da sintaxe ao conectar o manipulador ao evento.


  • O desvio de código que eventualmente lança mais exceções ainda será agrupado dentro do try/catch


Contras:


  • Você ainda precisa se lembrar de ligar essa coisa!

Considerações finais sobre EventHandlers assíncronos

Enquanto originalmente eu me propus a explorar maneiras interessantes de lidar com async void , o feedback do leitor foi válido porque os exemplos se concentraram em EventHandlers assíncronos e certamente deve haver uma maneira mais simples.


Neste artigo, exploramos o que eu poderia argumentar ser a maneira mais simples de fazer com que seus EventHandlers assíncronos se comportem adequadamente, e a solução refinada (na minha opinião) tem apenas a desvantagem de que você precisa se lembrar de usá-la.


Um comentarista sugeriu que alguém poderia explorar Programação Orientada a Aspectos (AoP) para injetar esse tipo de comportamento em seu aplicativo para que você não precise se lembrar de fazê-lo.


Existem algumas estruturas AoP em tempo de compilação, mas vou deixar isso como um exercício para você como leitor (porque também é um exercício para eu acompanhar).