Events e Delegates em C#: Conceitos e Exemplos Práticos

C# 18 de Jan de 2025

No desenvolvimento de software, especialmente em linguagens como C#, é comum trabalhar com Events, Delegates e EventArgs. Esses conceitos são fundamentais para a implementação de padrões de notificação e comunicação entre componentes. Neste artigo, vamos entender cada um desses componentes e como eles se interagem.

O que são Events?

Eventos (Events) são como notificações que um objeto envia para avisar outro quando algo acontece. Quando um evento é disparado, ele avisa todos os "ouvintes" (métodos ou funções) que estão registrados para esse evento, para que eles possam fazer algo em resposta.

O que são EventArgs?

EventArgs é a classe que contém os dados que são enviados junto com um evento. Quando um evento ocorre, ele pode carregar informações adicionais que os assinantes podem utilizar para reagir de maneira específica. Em outras palavras, EventArgs encapsula os dados necessários para fornecer mais contexto sobre o evento.

O que é um Delegate?

Um Delegate é um tipo de objeto que serve como uma referência para um método. Ele age como um ponteiro para uma função. Quando um evento é disparado, o Delegate delega a responsabilidade de tratar esse evento para os métodos (ou manipuladores de evento) associados a ele. Em resumo, o Delegate age como uma "pipeline", direcionando o evento para os métodos que o assinam.

Além disso, a palavra-chave delegate em C# é uma construção que estende a classe MulticastDelegate. Isso significa que um Delegate pode associar múltiplos métodos a um único evento, permitindo que vários manipuladores de evento respondam ao mesmo evento.

Implementação do Delegate e Event

Neste exemplo abaixo, estamos utilizando o WindowsForm e nele criaremos um case de atualização de progresso usando events e delegates.

Veja como funciona: um delegate é como um "ponteiro" para uma função. Para que ele funcione corretamente, sua assinatura precisa ser idêntica à do método que será vinculado a ele. Já no event, especificamos que o tipo será o delegate que criamos:

public delegate void ProgressHandler(int progress);

public event ProgressHandler OnProgressChanged;
//             Delegate        Nome do Evento

Depois disso, precisamos associar um método com a mesma assinatura do delegate ao event. Isso significa que, sempre que o evento for disparado, esse método será responsável por executar a ação necessária. Confira o código completo abaixo:


public partial class Form1 : Form
{
    public delegate void ProgressHandler(int progress);

    public event ProgressHandler OnProgressChanged;

    public Form1()
    {
        InitializeComponent();

        OnProgressChanged += UpdateProgress; // Associa o método ao evento
    }

    private void UpdateProgress(int progress)
    {
        if (progressBar.InvokeRequired)
        {
            progressBar.Invoke(new Action(() => progressBar.Value = progress));
            lblStatus.Invoke(new Action(() => lblStatus.Text = $"Status: {progress}% completado"));
        }
        else
        {
            progressBar.Value = progress;
            lblStatus.Text = $"Status: {progress}% completado";
        }
    }

    private void Form1_Load(object sender, EventArgs e)
    {

    }

    private void button1_Click(object sender, EventArgs e)
    {
        button1.Enabled = false;

        progressBar.Value = 0;
        lblStatus.Text = "Status: Começou o processo";

        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += (s, args) =>
        {
            for (int i = 1; i <= 10; i++)
            {
                Thread.Sleep(500);
                OnProgressChanged?.Invoke(i * 10);
            }
        };
        worker.RunWorkerCompleted += (s, args) =>
        {
            button1.Invoke(new Action(() => button1.Enabled = true));

            lblStatus.Text = "Status: Processo completado!";
        };
        worker.RunWorkerAsync();
    }
}

Funcionamento:

  1. Para evitar que a interface do usuário fique travada durante o processo, usamos a classe BackgroundWorker. Essa classe permite executar tarefas em uma thread separada, deixando a interface sem travamento.
  2. No método DoWork(), o evento OnProgressChanged é disparado. Isso ativa o método vinculado, UpdateProgress().
  3. No UpdateProgress(), usamos o método Invoke() para atualizar os componentes visuais, como a barra de progresso (progressBar) e o rótulo de status (lblStatus). Isso é necessário porque estamos lidando com uma thread diferente da principal, e alterações diretas nos componentes visuais só podem ser feitas a partir da thread principal ou a thread onde foi criado o componente.

Com isso, conseguimos atualizar a barra de progresso e o texto de status de forma fluida, sem travar o restante da interface.

0:00
/0:40

Resumindo a história

  • O tipo de evento é determinado pelo delegate.
  • O delegate funciona como um ponteiro que referencia uma função específica.
  • O método responsável pelo processamento deve ser associado ao evento.

Expressão Lambda com delegates

Events e Delegates também suportam expressão lambda.

//Delegate
public delegate int Add(int a, int b);
Add addOperation = (a, b) => a + b;
int result = addOperation(3, 3);

//Event
public delegate int Add(int a, int b);
public event Add OnAdd = (a, b) => a + b;

EventHandler e EventHandler<TEventArgs>

EventHandler e EventHandler<TEventArgs> são dois delegates predefinidos pelo C# com o objetivo de padronizar a criação de delegates. E quais são as diferenças neles?

  • EventHandler é usado para eventos que não precisam passar informações extras além do fato de que o evento ocorreu. A sua assinatura é a seguinte:
public delegate void EventHandler(object sender, EventArgs event);

Onde o parâmetro sender representa quem acionou o evento e o parâmetro event representa os dados. Por padrão não é enviado nada ao método responsável por lidar com a ação do evento.

Esse delegate é bastante utilizado na parte de UI do .NET.

  • EventHandler<TEventArgs> é um delegate genérico que permite passar dados adicionais associados ao evento. A sua assinatura é a seguinte:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;

Usamos quando precisamos passar dados adicionais para o método que vai lidar com o evento. Para realizarmos isso precisamos herdar de EventArgs conforme código abaixo:

public class MeusArgumentosPersonalizado: EventArgs
{
    public string Mensagem { get; }

    public MeusArgumentosPersonalizado(string mensagem)
    {
        Mensagem = mensagem;
    }
}

/*_____________________*/

public static event EventHandler<MeusArgumentosPersonalizado> MeuEvento;

Action, Action<T> e Func<T, TResult>

Todos esses são delegates predefinidos do C# que elimina a necessidade de termos que ficar criando delegates novos.

Action e Action<T>

A Action é um delegate que pode receber nenhum ou até 16 parâmetros, mas não retorna nenhum valor. Abaixo vemos o uso do Action<T> no método comum em listas chamado ForEach().

Action Chaining (encadeamento de funções)

Como sabemos, as actions são responsáveis por executar funções que não retornam valores. Essa característica permite encadear métodos para realizar operações cujo retorno não é relevante, mas cujas ações têm impacto, como, por exemplo, em validações ou transformações de um valor. Veja por exemplo o código abaixo:

public partial class ActionChaining : Form
{
    public ActionChaining()
    {
        InitializeComponent();
        DemostrativeOfChaining();
    }

    static void DemostrativeOfChaining()
    {
        Action<string> textProcessor = ConvertToUpperCase;
        textProcessor += AppendSemicolons;
        textProcessor += PrintToConsole;

        textProcessor("C# é demais");
    }

    static void ConvertToUpperCase(string input)
    {
        input = input.ToUpper();
        Debug.WriteLine($"Uppercase: {input}");
    }

    static void AppendSemicolons(string input)
    {
        input = $"{input};;;";
        Debug.WriteLine($"Texto com ponto e vírgula: {input}");
    }

    static void PrintToConsole(string input)
    {
        Debug.WriteLine($"Fim: {input}");
    }
}

OBS: Os métodos vinculados são executados sequencialmente.

Func<T, TResult>

A Func<T, TResult> é um delegate que podemos informar nenhum à 16 parâmetros e sempre o último parâmetro será o tipo do retorno. Diferente da Action, a Func possibilita o retorno de algum valor. Abaixo vemos o uso do Func no método comum em listas chamado Where().

Padrão mediator com delegates e events

O padrão mediator é padrão comportamental que reduz a dependência entre objetos. Ele atua como um centralizador que coordena a comunicação entre os objetos. Com isso, os objetos não se comunicam uns com os outros e sim por meio do mediador.

Nosso exemplo será simples, mas que irá exemplificar o uso desses componentes. Iremos simular um controle de luz e de temperatura e a label será alterada conforme ativarmos os eventos.

Vamos criar nossa classe mediator com um evento onde o delegate é uma Action e mais dois métodos que irá controlar nossa luz e a temperatura.

public class SmartHomeMediator
{
    public event Action<string> OnDeviceStatusChanged;

    public void ToggleLight(bool isOn)
    {
        string status = isOn ? "Luz ligada" : "Luz desligada";
        OnDeviceStatusChanged?.Invoke(status);
    }

    public void SetTemperature(int temperature)
    {
        string status = $"Temperatura ajustada para {temperature}°C";
        OnDeviceStatusChanged?.Invoke(status);
    }
}

Já no meu form, criaremos a instância da nossa classe mediator e adicionamos um método para o evento. Lembrando que em uma escala maior, o correto seria ter esse mediator em uma injeção de dependência como singleton para que toda a aplicação tenha uma única instância desse mediador.

public partial class MediatorEx : Form
{
    private SmartHomeMediator _mediator;

    public MediatorEx()
    {
        InitializeComponent();

        _mediator = new SmartHomeMediator();

        _mediator.OnDeviceStatusChanged += UpdateStatus;
    }

    private void lightButton_Click(object sender, EventArgs e)
    {
        bool isLightOn = lightButton.Text == "Ligar Luz";
        _mediator.ToggleLight(isLightOn);

        lightButton.Text = isLightOn ? "Desligar Luz" : "Ligar Luz";
    }

    private void tempButton_Click(object sender, EventArgs e)
    {
        Random random = new();
        int temperature = random.Next(18, 31);
        _mediator.SetTemperature(temperature);
    }

    private void UpdateStatus(string status)
    {
        statusLabel.Text = status;
    }
}

Referências

Func<TResult> Delegate (System)
Encapsulates a method that has no parameters and returns a value of the type specified by the TResult parameter.
Action<T1,T2> Delegate (System)
Encapsulates a method that has two parameters and does not return a value.

Marcadores