Compartilhar via


Visão geral de eventos

Um evento é uma ação à qual você pode responder ou "manipular", no código. Os eventos geralmente são gerados por uma ação do usuário, como clicar no mouse ou pressionar uma chave, mas também podem ser gerados pelo código do programa ou pelo sistema.

Aplicativos controlados por eventos executam código em resposta a um evento. Cada formulário e controle expõe um conjunto predefinido de eventos aos quais você pode responder. Se um desses eventos for gerado e houver um manipulador de eventos associado, o manipulador será invocado e o código será executado.

Os tipos de eventos gerados por um objeto variam, mas muitos tipos são comuns à maioria dos controles. Por exemplo, a maioria dos objetos tem um Click evento gerado quando um usuário clica nele.

Observação

Muitos eventos acontecem juntamente com outros eventos. Por exemplo, durante a ocorrência do evento DoubleClick, os eventos MouseDown, MouseUpe Click ocorrem.

Para obter informações gerais sobre como gerar e consumir um evento, consulte Manipulando e gerando eventos no .NET.

Delegados e sua função

Os delegados são classes comumente usadas no .NET para criar mecanismos de manipulação de eventos. Delegados equivalem aproximadamente a ponteiros de função, geralmente usados no Visual C++ e em outras linguagens orientadas a objetos. No entanto, diferente dos ponteiros de função, as classes delegate são orientadas a objeto, fortemente tipadas e seguras. Além disso, quando um ponteiro de função contém apenas uma referência a uma função específica, um delegado consiste em uma referência a um objeto e faz referência a um ou mais métodos dentro do objeto.

Esse modelo de evento usa delegados para associar eventos aos métodos que são usados ​​para lidar com eles. O delegado permite que outras classes se registrem para notificação de evento especificando um método de manipulador. Quando o evento ocorre, o delegado chama o método vinculado. Para obter mais informações sobre como definir delegados, consulte Manipulação e geração de eventos.

Os delegados podem ser associados a um único método ou a vários métodos, conhecidos como multicasting. Ao criar um delegado para um evento, você normalmente cria um evento multicast. Uma exceção rara pode ser um evento que resulta em um procedimento específico (como exibir uma caixa de diálogo) que não repetiria logicamente várias vezes por evento. Para obter informações sobre como criar um delegado multicast, consulte Como combinar delegados (Delegados Multicast).

Um delegado multicast mantém uma lista de invocação dos métodos associados a ele. O delegado multicast dá suporte a um método Combine para adicionar um método à lista de invocação e a um método Remove para removê-lo.

Quando um aplicativo registra um evento, o controle aciona o evento invocando o delegado para esse evento. A classe delegate chama o método vinculado. No caso mais comum (uma classe delegate multicast) a classe chama cada método associado da lista de invocação, que fornece uma notificação de um para muitos. Essa estratégia significa que o controle não precisa manter uma lista de objetos de destino para notificação de evento— o delegado manipula todo o registro e a notificação.

Essas classes também permitem que vários eventos sejam associados ao mesmo método, permitindo uma notificação muitos para um. Por exemplo, um evento de clique no botão e um evento de clique em comando de menu podem invocar o mesmo delegado, que então chama um único método para lidar com esses eventos separados da mesma maneira.

O mecanismo de associação usado com delegados é dinâmico: um delegado pode ser associado em tempo de execução a qualquer método cuja assinatura corresponda à do manipulador de eventos. Com esse recurso, você pode configurar ou alterar o método associado dependendo de uma condição e anexar dinamicamente um manipulador de eventos a um controle.

Eventos no Windows Forms

Os eventos em Windows Forms são declarados com o EventHandler<TEventArgs> delegado para métodos manipuladores. Cada manipulador de eventos fornece dois parâmetros que permitem manipular o evento corretamente. O exemplo a seguir mostra um manipulador de eventos para um Button controle do evento Click.

Private Sub button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles button1.Click

End Sub
private void button1_Click(object sender, System.EventArgs e)
{

}

O primeiro parâmetrosender fornece uma referência ao objeto que gerou o evento. O segundo parâmetro, e, passa um objeto específico para o evento que está sendo gerenciado. Ao referenciar as propriedades do objeto (e, às vezes, seus métodos), você pode obter informações como a posição do mouse para eventos do mouse ou dados que estão sendo transferidos em eventos de arraste e solte.

Normalmente, cada evento produz um manipulador de eventos com um tipo de objeto de evento diferente para o segundo parâmetro. Alguns manipuladores de eventos, como aqueles para o MouseDown e MouseUp eventos, têm o mesmo tipo de objeto para seu segundo parâmetro. Para esses tipos de eventos, você pode usar o mesmo manipulador de eventos para lidar com ambos os eventos.

Você também pode usar o mesmo manipulador de eventos para manipular o mesmo evento para controles diferentes. Por exemplo, se você tiver um grupo de RadioButton controles em um formulário, poderá criar um único manipulador de eventos para o Click evento de cada RadioButton. Para obter mais informações, consulte Como lidar com um evento de controle.

Manipuladores de eventos assíncronos

Os aplicativos modernos geralmente precisam executar operações assíncronas em resposta a ações do usuário, como baixar dados de um serviço Web ou acessar arquivos. Os manipuladores de eventos do Windows Forms podem ser declarados como async métodos para dar suporte a esses cenários, mas há considerações importantes para evitar armadilhas comuns.

Padrão básico do manipulador de eventos assíncrono

Os manipuladores de eventos podem ser declarados com o asyncAsync modificador (no Visual Basic) e o uso await (Awaitno Visual Basic) para operações assíncronas. Como os manipuladores de eventos devem retornar void (ou ser declarados como um Sub no Visual Basic), eles são um dos raros usos aceitáveis ( async void ou Async Sub no Visual Basic):

private async void downloadButton_Click(object sender, EventArgs e)
{
    downloadButton.Enabled = false;
    statusLabel.Text = "Downloading...";
    
    try
    {
        using var httpClient = new HttpClient();
        string content = await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");
        
        // Update UI with the result
        loggingTextBox.Text = content;
        statusLabel.Text = "Download complete";
    }
    catch (Exception ex)
    {
        statusLabel.Text = $"Error: {ex.Message}";
    }
    finally
    {
        downloadButton.Enabled = true;
    }
}
Private Async Sub downloadButton_Click(sender As Object, e As EventArgs) Handles downloadButton.Click
    downloadButton.Enabled = False
    statusLabel.Text = "Downloading..."

    Try
        Using httpClient As New HttpClient()
            Dim content As String = Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")

            ' Update UI with the result
            loggingTextBox.Text = content
            statusLabel.Text = "Download complete"
        End Using
    Catch ex As Exception
        statusLabel.Text = $"Error: {ex.Message}"
    Finally
        downloadButton.Enabled = True
    End Try
End Sub

Importante

Embora async void seja desencorajado, é necessário para manipuladores de eventos (e código semelhante a um manipulador de eventos, como Control.OnClick) já que eles não podem retornar Task. Sempre encapsule as operações aguardadas em try-catch blocos para lidar com exceções corretamente, conforme mostrado no exemplo anterior.

Armadilhas comuns e deadlocks

Aviso

Nunca use chamadas de bloqueio como .Wait(), .Resultou .GetAwaiter().GetResult() em manipuladores de eventos ou qualquer código de interface do usuário. Esses padrões podem causar deadlocks.

O código a seguir demonstra um antipadrão comum que causa deadlocks:

// DON'T DO THIS - causes deadlocks
private void badButton_Click(object sender, EventArgs e)
{
    try
    {
        // This blocks the UI thread and causes a deadlock
        string content = DownloadPageContentAsync().GetAwaiter().GetResult();
        loggingTextBox.Text = content;
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Error: {ex.Message}");
    }
}

private async Task<string> DownloadPageContentAsync()
{
    using var httpClient = new HttpClient();
    await Task.Delay(2000); // Simulate delay
    return await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");
}
' DON'T DO THIS - causes deadlocks
Private Sub badButton_Click(sender As Object, e As EventArgs) Handles badButton.Click
    Try
        ' This blocks the UI thread and causes a deadlock
        Dim content As String = DownloadPageContentAsync().GetAwaiter().GetResult()
        loggingTextBox.Text = content
    Catch ex As Exception
        MessageBox.Show($"Error: {ex.Message}")
    End Try
End Sub

Private Async Function DownloadPageContentAsync() As Task(Of String)
    Using httpClient As New HttpClient()
        Return Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")
    End Using
End Function

Isso causa um deadlock pelos seguintes motivos:

  • O thread de interface do usuário chama o método assíncrono e bloqueia a espera pelo resultado.
  • O método assíncrono captura o thread de interface do usuário SynchronizationContext.
  • Quando a operação assíncrona é concluída, ela tenta continuar no thread de interface do usuário capturado.
  • O thread da interface do usuário está bloqueado aguardando a conclusão da operação.
  • O deadlock ocorre porque nenhuma operação pode continuar.

Operações entre threads

Quando precisar atualizar os controles de interface do usuário de threads em segundo plano em operações assíncronas, use as técnicas de marshaling apropriadas. Entender a diferença entre abordagens de bloqueio e não bloqueio é crucial para aplicativos responsivos.

O .NET 9 foi introduzido Control.InvokeAsync, que fornece marshaling assíncrono para o thread da interface do usuário. Ao contrário do Control.Invoke que envia (bloqueia o thread de chamada), Control.InvokeAsyncpostagens (sem bloqueio) para a fila de mensagens do thread de interface do usuário. Para obter mais informações sobre Control.InvokeAsync, consulte Como fazer chamadas thread-safe para controles.

Principais vantagens de InvokeAsync:

  • Não bloqueio: retorna imediatamente, permitindo que o thread de chamada continue.
  • Amigável: retorna um Task que pode ser aguardado.
  • Propagação de exceção: propaga corretamente as exceções de volta para o código de chamada.
  • Suporte a cancelamento: suporte CancellationToken para cancelamento de operação.
private async void processButton_Click(object sender, EventArgs e)
{
    processButton.Enabled = false;
    
    // Start background work
    await Task.Run(async () =>
    {
        for (int i = 0; i <= 100; i += 10)
        {
            // Simulate work
            await Task.Delay(200);
            
            // Create local variable to avoid closure issues
            int currentProgress = i;
            
            // Update UI safely from background thread
            await progressBar.InvokeAsync(() =>
            {
                progressBar.Value = currentProgress;
                statusLabel.Text = $"Progress: {currentProgress}%";
            });
        }
    });
    
    processButton.Enabled = true;
}
Private Async Sub processButton_Click(sender As Object, e As EventArgs) Handles processButton.Click
    processButton.Enabled = False

    ' Start background work
    Await Task.Run(Async Function()
                       For i As Integer = 0 To 100 Step 10
                           ' Simulate work
                           Await Task.Delay(200)

                           ' Create local variable to avoid closure issues
                           Dim currentProgress As Integer = i

                           ' Update UI safely from background thread
                           Await progressBar.InvokeAsync(Sub()
                                                             progressBar.Value = currentProgress
                                                             statusLabel.Text = $"Progress: {currentProgress}%"
                                                         End Sub)
                       Next
                   End Function)

    processButton.Enabled = True
End Sub

Para operações verdadeiramente assíncronas que precisam ser executadas no thread da interface do usuário:

private async void complexButton_Click(object sender, EventArgs e)
{
    // This runs on UI thread but doesn't block it
    statusLabel.Text = "Starting complex operation...";

    // Dispatch and run on a new thread
    await Task.WhenAll(Task.Run(SomeApiCallAsync),
                       Task.Run(SomeApiCallAsync),
                       Task.Run(SomeApiCallAsync));

    // Update UI directly since we're already on UI thread
    statusLabel.Text = "Operation completed";
}

private async Task SomeApiCallAsync()
{
    using var client = new HttpClient();

    // Simulate random network delay
    await Task.Delay(Random.Shared.Next(500, 2500));

    // Do I/O asynchronously
    string result = await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");

    // Marshal back to UI thread
    await this.InvokeAsync(async (cancelToken) =>
    {
        loggingTextBox.Text += $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}";
    });

    // Do more async I/O ...
}
Private Async Sub complexButton_Click(sender As Object, e As EventArgs) Handles complexButton.Click
    'Convert the method to enable the extension method on the type
    Dim method = DirectCast(AddressOf ComplexButtonClickLogic,
                            Func(Of CancellationToken, Task))

    'Invoke the method asynchronously on the UI thread
    Await Me.InvokeAsync(method.AsValueTask())
End Sub

Private Async Function ComplexButtonClickLogic(token As CancellationToken) As Task
    ' This runs on UI thread but doesn't block it
    statusLabel.Text = "Starting complex operation..."

    ' Dispatch and run on a new thread
    Await Task.WhenAll(Task.Run(AddressOf SomeApiCallAsync),
                       Task.Run(AddressOf SomeApiCallAsync),
                       Task.Run(AddressOf SomeApiCallAsync))

    ' Update UI directly since we're already on UI thread
    statusLabel.Text = "Operation completed"
End Function

Private Async Function SomeApiCallAsync() As Task
    Using client As New HttpClient()

        ' Simulate random network delay
        Await Task.Delay(Random.Shared.Next(500, 2500))

        ' Do I/O asynchronously
        Dim result As String = Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")

        ' Marshal back to UI thread
        ' Extra work here in VB to handle ValueTask conversion
        Await Me.InvokeAsync(DirectCast(
                Async Function(cancelToken As CancellationToken) As Task
                    loggingTextBox.Text &= $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}"
                End Function,
            Func(Of CancellationToken, Task)).AsValueTask() 'Extension method to convert Task
        )

        ' Do more Async I/O ...
    End Using
End Function

Dica

O .NET 9 inclui avisos do analisador (WFO2001) para ajudar a detectar quando os métodos assíncronos são passados incorretamente para sobrecargas síncronas de InvokeAsync. Isso ajuda a evitar o comportamento de "fogo e esquecer".

Observação

Se você estiver usando o Visual Basic, o snippet de código anterior usou um método de extensão para converter um ValueTask em um Task. O código do método de extensão está disponível no GitHub.

Práticas recomendadas

  • Use assíncrono/await consistentemente: não misture padrões assíncronos com chamadas de bloqueio.
  • Manipular exceções: sempre encapsular operações assíncronas em blocos try-catch em async void manipuladores de eventos.
  • Forneça comentários do usuário: atualize a interface do usuário para mostrar o progresso ou o status da operação.
  • Desabilitar controles durante as operações: impedir que os usuários iniciem várias operações.
  • Use CancellationToken: suporte ao cancelamento da operação para tarefas de execução prolongada.
  • Considere ConfigureAwait(false): use no código da biblioteca para evitar capturar o contexto da interface do usuário quando não for necessário.