Partager via


Vue d’ensemble des événements

Un événement est une action à laquelle vous pouvez répondre ou « gérer » dans le code. Les événements sont généralement générés par une action utilisateur, comme cliquer sur la souris ou appuyer sur une touche, mais ils peuvent également être générés par le code du programme ou par le système.

Les applications pilotées par les événements exécutent du code en réponse à un événement. Chaque formulaire et contrôle expose un ensemble prédéfini d’événements auxquels vous pouvez répondre. Si l’un de ces événements est déclenché et qu’il existe un gestionnaire d’événements associé, le gestionnaire est appelé et le code est exécuté.

Les types d’événements déclenchés par un objet varient, mais de nombreux types sont communs à la plupart des contrôles. Par exemple, la plupart des objets ont un Click événement déclenché lorsqu’un utilisateur clique dessus.

Remarque

De nombreux événements se produisent avec d’autres événements. Par exemple, au cours de l’événement DoubleClick se produisant, les événements MouseDown, MouseUpet Click se produisent.

Pour obtenir des informations générales sur la façon de déclencher et de consommer un événement, consultez Gestion et déclenchement d’événements dans .NET.

Délégués et leur rôle

Les délégués sont des classes couramment utilisées dans .NET pour générer des mécanismes de gestion des événements. Les délégués correspondent approximativement aux pointeurs de fonction, couramment utilisés dans Visual C++ et d’autres langages orientés objet. Toutefois à la différence des pointeurs de fonction, les délégués sont orientés objet, de type sécurisé et sûrs. En outre, lorsqu’un pointeur de fonction contient uniquement une référence à une fonction particulière, un délégué se compose d’une référence à un objet et de références à une ou plusieurs méthodes au sein de l’objet.

Ce modèle d'événement utilise les délégués pour lier les événements aux méthodes servant à les gérer. Le délégué permet à d’autres classes de s’inscrire à la notification d’événement en spécifiant une méthode de gestionnaire. Quand l'événement se produit, le délégué appelle la méthode liée. Pour plus d’informations sur la définition des délégués, consultez Gestion et déclenchement d’événements.

Les délégués peuvent être liés à une seule méthode ou à plusieurs méthodes, appelées multidiffusion. Lors de la création d’un délégué pour un événement, vous créez généralement un événement de multidiffusion. Une exception rare peut être un événement qui entraîne une procédure spécifique (par exemple, l’affichage d’une boîte de dialogue) qui ne se répète pas logiquement plusieurs fois par événement. Pour plus d’informations sur la création d’un délégué multidiffusion, consultez Comment combiner des délégués (délégués multidiffusion).

Un délégué de multidiffusion gère une liste d’appels des méthodes liées à celui-ci. Le délégué de multidiffusion prend en charge une méthode Combine pour ajouter une méthode à la liste d’appel et une méthode Remove pour la supprimer.

Lorsqu’une application enregistre un événement, le contrôle déclenche l’événement en appelant le délégué pour cet événement. Le délégué appelle à son tour la méthode liée. Dans le cas le plus courant (délégué multidiffusion), le délégué appelle à tour de rôle chaque méthode liée de la liste d'appel, ce qui produit une notification un à plusieurs. Cette stratégie signifie que le contrôle n’a pas besoin de conserver une liste d’objets cibles pour la notification d’événement : le délégué gère toutes les inscriptions et toutes les notifications.

Les délégués permettent également de lier plusieurs événements à la même méthode et par conséquent de produire une notification plusieurs-à-un. Par exemple, un événement de clic sur un bouton et un événement de clic sur une commande de menu peuvent tous deux faire appel au même délégué, qui appelle ensuite une méthode unique pour gérer ces événements distincts de manière identique.

Le mécanisme de liaison utilisé avec les délégués est dynamique : un délégué peut être lié au moment de l’exécution à n’importe quelle méthode dont la signature correspond à celle du gestionnaire d’événements. Avec cette fonctionnalité, vous pouvez configurer ou modifier la méthode liée en fonction d’une condition et attacher dynamiquement un gestionnaire d’événements à un contrôle.

Événements dans Windows Forms

Les événements dans Windows Forms sont déclarés avec le délégué pour les méthodes de gestionnaire EventHandler<TEventArgs>. Chaque gestionnaire d’événements fournit deux paramètres qui vous permettent de gérer l’événement correctement. L’exemple suivant montre un gestionnaire d’événements pour l’événement d’un contrôle ButtonClick.

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)
{

}

Le premier paramètre fournitsender une référence à l’objet qui a déclenché l’événement. Le deuxième paramètre, transmet eun objet spécifique à l’événement qui est géré. En référençant les propriétés de l’objet (et, parfois, ses méthodes), vous pouvez obtenir des informations telles que l’emplacement de la souris pour les événements de souris ou les données transférées dans des événements glisser-déplacer.

En règle générale, chaque événement produit un gestionnaire d’événements avec un type d’objet événement différent pour le deuxième paramètre. Certains gestionnaires d’événements, tels que ceux pour les événements MouseDown et MouseUp, ont le même type d’objet pour leur deuxième paramètre. Pour ces types d’événements, vous pouvez utiliser le même gestionnaire d’événements pour gérer les deux événements.

Vous pouvez également utiliser le même gestionnaire d’événements pour gérer le même événement pour différents contrôles. Par exemple, si vous avez un groupe de RadioButton contrôles sur un formulaire, vous pouvez créer un seul gestionnaire d’événements pour l’événement Click de chaque RadioButton. Pour plus d’informations, consultez Comment gérer un événement de contrôle.

Gestionnaires d’événements Async

Les applications modernes doivent souvent effectuer des opérations asynchrones en réponse à des actions utilisateur, telles que le téléchargement de données à partir d’un service web ou l’accès aux fichiers. Les gestionnaires d’événements Windows Forms peuvent être déclarés en tant que async méthodes pour prendre en charge ces scénarios, mais il existe des considérations importantes pour éviter les pièges courants.

Modèle de gestionnaire d’événements asynchrones de base

Les gestionnaires d’événements peuvent être déclarés avec le async modificateur (Async en Visual Basic) et l’utiliser await (Await en Visual Basic) pour les opérations asynchrones. Étant donné que les gestionnaires d’événements doivent retourner void (ou être déclarés en tant que Sub visual Basic), ils sont l’une des rares utilisations acceptables de async void (ou Async Sub en 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

Important

Bien qu’il async void soit déconseillé, il est nécessaire pour les gestionnaires d’événements (et le code semblable au gestionnaire d’événements, par Control.OnClickexemple) car ils ne peuvent pas retourner Task. Habillage toujours des opérations attendues dans try-catch des blocs pour gérer correctement les exceptions, comme indiqué dans l’exemple précédent.

Pièges courants et interblocages

Avertissement

N’utilisez jamais d’appels bloquants tels que .Wait(), .Resultou .GetAwaiter().GetResult() dans des gestionnaires d’événements ou n’importe quel code d’interface utilisateur. Ces modèles peuvent entraîner des interblocages.

Le code suivant illustre un anti-modèle courant qui provoque des interblocages :

// 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

Cela provoque un blocage pour les raisons suivantes :

  • Le thread d’interface utilisateur appelle la méthode asynchrone et bloque l’attente du résultat.
  • La méthode asynchrone capture le thread d’interface SynchronizationContextutilisateur .
  • Une fois l’opération asynchrone terminée, elle tente de continuer sur le thread d’interface utilisateur capturé.
  • Le thread d’interface utilisateur est bloqué en attente de la fin de l’opération.
  • L’interblocage se produit car aucune opération ne peut se poursuivre.

Opérations entre threads

Lorsque vous devez mettre à jour les contrôles d’interface utilisateur à partir de threads d’arrière-plan dans les opérations asynchrones, utilisez les techniques de marshaling appropriées. Comprendre la différence entre les approches bloquantes et non bloquantes est essentielle pour les applications réactives.

.NET 9 introduit Control.InvokeAsync, qui fournit un marshaling convivial asynchrone au thread d’interface utilisateur. Contrairement Control.Invoke aux envois (bloque le thread appelant), Control.InvokeAsyncles publications (non bloquantes) dans la file d’attente de messages du thread d’interface utilisateur. Pour plus d’informations sur Control.InvokeAsync, consultez Comment effectuer des appels thread-safe aux contrôles.

Principaux avantages d’InvokeAsync :

  • Non bloquant : retourne immédiatement, ce qui permet au thread appelant de continuer.
  • Async-friendly : retourne une Task valeur qui peut être attendue.
  • Propagation d’exception : propage correctement les exceptions vers le code appelant.
  • Prise en charge de l’annulation : prend en charge CancellationToken l’annulation de l’opération.
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

Pour les opérations véritablement asynchrones qui doivent s’exécuter sur le thread d’interface utilisateur :

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

Conseil / Astuce

.NET 9 inclut des avertissements d’analyseur (WFO2001) pour vous aider à détecter quand les méthodes asynchrones sont transmises de manière incorrecte aux surcharges synchrones de InvokeAsync. Cela permet d’éviter le comportement « fire-and-forget ».

Remarque

Si vous utilisez Visual Basic, l’extrait de code précédent a utilisé une méthode d’extension pour convertir un ValueTaskTasken . Le code de méthode d’extension est disponible sur GitHub.

Meilleures pratiques

  • Utilisez async/await de manière cohérente : ne mélangez pas de modèles asynchrones avec des appels bloquants.
  • Gérer les exceptions : encapsulez toujours les opérations asynchrones dans les blocs try-catch dans async void les gestionnaires d’événements.
  • Fournissez des commentaires sur l’utilisateur : Mettez à jour l’interface utilisateur pour afficher la progression ou l’état de l’opération.
  • Désactiver les contrôles pendant les opérations : empêcher les utilisateurs de démarrer plusieurs opérations.
  • Utilisez CancellationToken : Prise en charge de l’annulation de l’opération pour les tâches de longue durée.
  • Envisagez ConfigureAwait(false) : utilisez-le dans le code de la bibliothèque pour éviter de capturer le contexte de l’interface utilisateur si nécessaire.