Freigeben über


Übersicht über Ereignisse

Bei einem Ereignis handelt es sich um eine Aktion, auf die Sie im Code reagieren oder "behandeln" können. Ereignisse werden in der Regel von einer Benutzeraktion generiert, z. B. durch Klicken auf die Maus oder durch Drücken einer Taste, aber sie können auch durch Programmcode oder vom System generiert werden.

Ereignisgesteuerte Anwendungen führen Code als Reaktion auf ein Ereignis aus. Jedes Formular und jedes Steuerelement macht einen vordefinierten Satz von Ereignissen verfügbar, auf den Sie reagieren können. Wenn eines dieser Ereignisse ausgelöst wird und ein zugeordneter Ereignishandler vorhanden ist, wird der Handler aufgerufen und Code ausgeführt.

Die Typen von Ereignissen, die von einem Objekt ausgelöst werden, variieren, aber viele Typen sind für die meisten Steuerelemente üblich. Die meisten Objekte verfügen beispielsweise über ein Click Ereignis, das ausgelöst wird, wenn ein Benutzer darauf klickt.

Hinweis

Viele Ereignisse treten mit anderen Ereignissen auf. Beispielsweise treten im Verlauf des DoubleClick-Ereignisses die MouseDown, MouseUp und Click-Ereignisse auf.

Allgemeine Informationen zum Auslösen und Nutzen eines Ereignisses finden Sie unter Behandeln und Auslösen von Ereignissen in .NET.

Stellvertretungen und deren Rolle

Delegaten sind Klassen, die in .NET häufig zur Implementierung von Ereignisbehandlungsmechanismen verwendet werden. Stellvertretungen entsprechen ungefähr Funktionszeigern, die häufig in Visual C++ und anderen objektorientierten Sprachen verwendet werden. Im Gegensatz zu Funktionszeigern sind Delegaten objektorientiert, typsicher und sicher. Wenn ein Funktionszeiger nur einen Verweis auf eine bestimmte Funktion enthält, besteht ein Delegat aus einem Verweis auf ein Objekt und verweisen auf eine oder mehrere Methoden innerhalb des Objekts.

Dieses Ereignismodell verwendet Delegaten, um Ereignisse an die Methoden zu binden, die sie verarbeiten. Der Delegat ermöglicht es anderen Klassen, sich für ereignisbenachrichtigungen zu registrieren, indem eine Handlermethode angegeben wird. Wenn das Ereignis auftritt, ruft der Delegate die gebundene Methode auf. Weitere Informationen zum Definieren von Stellvertretungen finden Sie unter Behandeln und Auslösen von Ereignissen.

Stellvertretungen können an eine einzelne Methode oder an mehrere Methoden gebunden werden, die als Multicasting bezeichnet werden. Beim Erstellen eines Delegaten für ein Ereignis erstellen Sie in der Regel ein Multicastereignis. Eine seltene Ausnahme kann ein Ereignis sein, das zu einer bestimmten Prozedur führt (z. B. das Anzeigen eines Dialogfelds), das nicht logisch mehrmals pro Ereignis wiederholt würde. Informationen darüber, wie Sie einen Multicast-Delegaten erstellen, finden Sie unter Delegaten kombinieren (Multicast-Delegaten).

Ein Multicastdelegat verwaltet eine Aufrufliste der an sie gebundenen Methoden. Der Multicastdelegat unterstützt eine Combine Methode, um eine Methode zur Aufrufliste hinzuzufügen und eine Remove Methode, um sie zu entfernen.

Wenn eine Anwendung ein Ereignis aufzeichnet, löst das Steuerelement das Ereignis aus, indem er den Delegaten für dieses Ereignis aufruft. Der Delegat ruft wiederum die gebundene Methode auf. Im häufigsten Fall (ein Multicastdelegat) ruft der Delegat jede gebundene Methode in der Aufrufliste auf, was eine Eins-zu-viele-Benachrichtigung bereitstellt. Diese Strategie bedeutet, dass das Steuerelement keine Liste der Zielobjekte für die Ereignisbenachrichtigung verwalten muss – der Delegat behandelt alle Registrierungen und Benachrichtigungen.

Delegaten ermöglichen außerdem, dass mehrere Ereignisse an dieselbe Methode gebunden werden können, wodurch eine Benachrichtigung von vielen auf einen möglich ist. Beispielsweise kann ein Schaltflächenklickereignis und ein Menübefehl-Click-Ereignis denselben Delegat aufrufen, der dann eine einzelne Methode aufruft, um diese separaten Ereignisse auf die gleiche Weise zu behandeln.

Der bindungsmechanismus, der mit Delegaten verwendet wird, ist dynamisch: Ein Delegat kann zur Laufzeit an jede Methode gebunden werden, deren Signatur mit der des Ereignishandlers übereinstimmt. Mit diesem Feature können Sie die gebundene Methode je nach Bedingung einrichten oder ändern und einen Ereignishandler dynamisch an ein Steuerelement anfügen.

Ereignisse in Windows Forms

Ereignisse in Windows Forms werden mit dem EventHandler<TEventArgs> Delegat für Handlermethoden deklariert. Jeder Ereignishandler stellt zwei Parameter bereit, mit denen Sie das Ereignis ordnungsgemäß behandeln können. Das folgende Beispiel zeigt einen Ereignishandler für das Ereignis eines Button Steuerelements 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)
{

}

Der erste Parametersender stellt einen Verweis auf das Objekt bereit, das das Ereignis ausgelöst hat. Der zweite Parameter eübergibt ein Objekt, das für das Ereignis spezifisch ist, das behandelt wird. Durch Verweisen auf die Eigenschaften des Objekts (und manchmal auch deren Methoden) können Sie Informationen abrufen, z. B. die Position der Maus für Mausereignisse oder Daten, die in Drag-and-Drop-Ereignissen übertragen werden.

In der Regel erzeugt jedes Ereignis einen Ereignishandler mit einem anderen Ereignisobjekttyp für den zweiten Parameter. Einige Ereignishandler, wie die für die MouseDown- und MouseUp-Ereignisse, haben denselben Objekttyp als zweiten Parameter. Für diese Ereignistypen können Sie denselben Ereignishandler verwenden, um beide Ereignisse zu behandeln.

Sie können auch denselben Ereignishandler verwenden, um dasselbe Ereignis für verschiedene Steuerelemente zu behandeln. Wenn Sie beispielsweise über eine Gruppe von RadioButton Steuerelementen in einem Formular verfügen, können Sie einen einzelnen Ereignishandler für das Click Ereignis jedes RadioButtonFormulars erstellen. Weitere Informationen finden Sie unter Umgang mit einem Steuerelementereignis.

Asynchrone Ereignishandler

Moderne Anwendungen müssen häufig asynchrone Vorgänge als Reaktion auf Benutzeraktionen ausführen, z. B. das Herunterladen von Daten aus einem Webdienst oder den Zugriff auf Dateien. Windows Forms-Ereignishandler können als async Methoden deklariert werden, um diese Szenarien zu unterstützen, aber es gibt wichtige Überlegungen, um häufige Fallstricke zu vermeiden.

Grundlegendes asynchrones Ereignishandlermuster

Ereignishandler können mit dem async Modifizierer (Asyncin Visual Basic) deklariert und (Await in Visual Basic) für asynchrone Vorgänge verwendet await werden. Da Ereignishandler zurückgegeben void werden müssen (oder als Sub in Visual Basic deklariert werden müssen), sind sie eine der seltenen zulässigen Verwendungen async void (oder Async Sub in 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

Von Bedeutung

Es async void wird zwar abgeraten, aber für Ereignishandler (und ereignishandlerähnlicher Code, z Control.OnClick. B. ) erforderlich, da sie nicht zurückgegeben Taskwerden können. Immer erwartete Vorgänge in try-catch Blöcken umschließen, um Ausnahmen ordnungsgemäß zu behandeln, wie im vorherigen Beispiel gezeigt.

Häufige Fallstricke und Deadlocks

Warnung

Verwenden Sie niemals Blockierungsaufrufe wie .Wait(), oder .Result.GetAwaiter().GetResult() in Ereignishandlern oder ui-Code. Diese Muster können Deadlocks verursachen.

Der folgende Code veranschaulicht ein gängiges Antimuster, das Deadlocks verursacht:

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

Dies führt zu einem Deadlock aus folgenden Gründen:

  • Der UI-Thread ruft die asynchrone Methode auf und blockiert das Warten auf das Ergebnis.
  • Die asynchrone Methode erfasst die UI-Threads SynchronizationContext.
  • Nach Abschluss des asynchronen Vorgangs wird versucht, den erfassten UI-Thread fortzusetzen.
  • Der UI-Thread wird blockiert, bis der Vorgang abgeschlossen ist.
  • Deadlock tritt auf, da keine operation fortgesetzt werden kann.

Threadübergreifende Vorgänge

Wenn Sie UI-Steuerelemente aus Hintergrundthreads in asynchronen Vorgängen aktualisieren müssen, verwenden Sie die entsprechenden Marshaling-Techniken. Das Verständnis des Unterschieds zwischen Blockierungs- und nicht blockierenden Ansätzen ist für reaktionsfähige Anwendungen von entscheidender Bedeutung.

.NET 9 eingeführt Control.InvokeAsync, das asynchrone Marshaling für den UI-Thread bereitstellt. Im Gegensatz dazu Control.Invoke , welche Senden (den aufrufenden Thread blockiert), Control.InvokeAsyncBeiträge (nicht blockieren) an die Nachrichtenwarteschlange des UI-Threads. Weitere Informationen finden Control.InvokeAsyncSie unter How to make thread-safe calls to controls.

Wichtige Vorteile von InvokeAsync:

  • Nicht blockierend: Gibt sofort zurück, sodass der aufrufende Thread fortgesetzt werden kann.
  • Asynchron-freundlich: Gibt ein Task , das erwartet werden kann.
  • Ausnahmeverteilung: Gibt Ausnahmen ordnungsgemäß an den aufrufenden Code weiter.
  • Abbruchunterstützung: Unterstützt CancellationToken den Abbruch des Vorgangs.
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

Für wirklich asynchrone Vorgänge, die im UI-Thread ausgeführt werden müssen:

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

Tipp

.NET 9 enthält Analysewarnungen (WFO2001), um zu erkennen, wann asynchrone Methoden fälschlicherweise an synchrone Überladungen InvokeAsyncübergeben werden. Dies trägt dazu bei, das "Feuer-und-Vergessen"-Verhalten zu verhindern.

Hinweis

Wenn Sie Visual Basic verwenden, verwendet der vorherige Codeausschnitt eine Erweiterungsmethode, um eine ValueTask in ein Task. Der Code der Erweiterungsmethode ist auf GitHub verfügbar.

Bewährte Methoden

  • Verwenden Sie async/await konsistent: Mischen Sie keine asynchronen Muster mit blockierungsaufrufen.
  • Behandeln von Ausnahmen: Schließen Sie asynchrone Vorgänge immer in Try-Catch-Blöcken in async void Ereignishandlern um.
  • Geben Sie Benutzerfeedback an: Aktualisieren Sie die Benutzeroberfläche, um den Vorgangsfortschritt oder -status anzuzeigen.
  • Deaktivieren Von Steuerelementen während vorgängen: Verhindern, dass Benutzer mehrere Vorgänge starten.
  • Verwenden Sie CancellationToken: Support operation cancellation for long-running tasks.
  • Erwägen Sie ConfigureAwait(false):Verwenden Sie in Bibliothekscode, um zu vermeiden, dass der Ui-Kontext bei Bedarf erfasst wird.