Dela via


Översikt över händelser

En händelse är en åtgärd som du kan svara på eller "hantera" i kod. Händelser genereras vanligtvis av en användaråtgärd, till exempel genom att klicka med musen eller trycka på en nyckel, men de kan också genereras av programkod eller av systemet.

Händelsedrivna program kör kod som svar på en händelse. Varje formulär och kontroll exponerar en fördefinierad uppsättning händelser som du kan svara på. Om en av dessa händelser aktiveras och det finns en associerad händelsehanterare anropas hanteraren och kod körs.

De typer av händelser som genereras av ett objekt varierar, men många typer är gemensamma för de flesta kontroller. De flesta objekt har till exempel en Click händelse som aktiveras när en användare klickar på den.

Anmärkning

Många händelser inträffar med andra händelser. Under den DoubleClick händelse som inträffar inträffar till exempel MouseDown, MouseUpoch Click händelser.

Allmän information om hur du genererar och använder en händelse finns i Hantera och höja händelser i .NET.

Ombud och deras roll

Ombud är klasser som ofta används i .NET för att skapa mekanismer för händelsehantering. Delegater motsvarar ungefär funktionspekare och används ofta i Visual C++ och andra objektorienterade språk. Till skillnad från funktionspekare är dock delegeringar objektorienterade, typsäkra och säkra. Också, där en funktionspekare endast innehåller en referens till en viss funktion, består en delegat av en referens till ett objekt och referenser till en eller flera metoder inom objektet.

Den här händelsemodellen använder ombud för att binda händelser till de metoder som används för att hantera dem. Ombudet gör det möjligt för andra klasser att registrera sig för händelsemeddelanden genom att ange en hanteringsmetod. När händelsen inträffar, anropar delegaten den bundna metoden. Mer information om hur du definierar delegater finns i Hantering och upphöjning av händelser.

Delegater kan bindas till en enda metod eller till flera metoder, vilket kallas för multikastning. När du skapar ett ombud för en händelse skapar du vanligtvis en multicast-händelse. Ett sällsynt undantag kan vara en händelse som resulterar i en specifik procedur (till exempel att visa en dialogruta) som inte logiskt skulle upprepas flera gånger per händelse. Information om hur du skapar en multicastdelegering finns i Hur man kombinerar delegeringar (Multicastdelegeringar).

Ett multicast-ombud upprätthåller en anropslista över de metoder som är bundna till den. Multicast-ombudet stöder en Combine metod för att lägga till en metod i anropslistan, och en Remove metod för att ta bort den.

När ett program registrerar en händelse genererar kontrollen händelsen genom att anropa ombudet för händelsen. Ombudet anropar i sin tur den bundna metoden. I det vanligaste fallet (ett multicast-ombud) anropar ombudet varje bunden metod i anropslistan i sin tur, vilket ger ett en-till-många-meddelande. Den här strategin innebär att kontrollen inte behöver underhålla en lista över målobjekt för händelsemeddelanden – ombudet hanterar all registrering och alla meddelanden.

Delegater gör också att flera händelser kan bindas till samma metod, vilket tillåter en många-till-en-notifiering. Till exempel kan en knappklickshändelse och en menykommandoklickshändelse både anropa samma delegat, som sedan anropar en gemensam metod för att hantera dessa händelser likadant.

Bindningsmekanismen som används med ombud är dynamisk: ett ombud kan under körning bindas till vilken metod som helst vars signatur matchar den för händelsehanteraren. Med den här funktionen kan du konfigurera eller ändra den bundna metoden beroende på ett villkor och dynamiskt koppla en händelsehanterare till en kontroll.

Händelser i Windows-formulär

Händelser i Windows Forms deklareras med delegeringen EventHandler<TEventArgs> för hanteringsmetoder. Varje händelsehanterare innehåller två parametrar som gör att du kan hantera händelsen korrekt. I följande exempel visas en händelsehanterare för en Button-kontrolls Click-event.

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

}

Den första parametern,sender, innehåller en referens till objektet som skapade händelsen. Den andra parametern, e, skickar ett objekt som är specifikt för händelsen som hanteras. Genom att referera till objektets egenskaper (och ibland dess metoder) kan du hämta information, till exempel musplatsen för mushändelser eller data som överförs i dra och släpp-händelser.

Vanligtvis genererar varje händelse en händelsehanterare med en annan händelseobjekttyp för den andra parametern. Vissa händelsehanterare, till exempel de för MouseDown och MouseUp händelser, har samma objekttyp för sin andra parameter. För dessa typer av händelser kan du använda samma händelsehanterare för att hantera båda händelserna.

Du kan också använda samma händelsehanterare för att hantera samma händelse för olika kontroller. Om du till exempel har en grupp RadioButton kontroller i ett formulär kan du skapa en enskild händelsehanterare för händelsen för Click varje RadioButton. Mer information finns i Hantera en kontrollhändelse.

Asynkrona händelsehanterare

Moderna program behöver ofta utföra asynkrona åtgärder som svar på användaråtgärder, till exempel att ladda ned data från en webbtjänst eller komma åt filer. Händelsehanterare för Windows Forms kan deklareras som metoder för async att stödja dessa scenarier, men det finns viktiga överväganden för att undvika vanliga fallgropar.

Grundläggande asynkront händelsehanterarmönster

Händelsehanterare kan deklareras med async modifieraren (Async i Visual Basic) och använda await (Await i Visual Basic) för asynkrona åtgärder. Eftersom händelsehanterare måste returnera void (eller deklareras som en Sub i Visual Basic) är de en av de sällsynta acceptabla användningsområdena async void för (eller Async Sub i 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

Viktigt!

Även om async void det inte rekommenderas är det nödvändigt för händelsehanterare (och händelsehanterarliknande kod, till exempel Control.OnClick) eftersom de inte kan returnera Task. Omslut alltid väntande åtgärder i try-catch block för att hantera undantag korrekt, som du ser i föregående exempel.

Vanliga fallgropar och dödlägen

Varning

Använd aldrig blockeringsanrop som .Wait(), .Resulteller .GetAwaiter().GetResult() i händelsehanterare eller någon UI-kod. Dessa mönster kan orsaka dödlägen.

Följande kod visar ett vanligt antimönster som orsakar dödlägen:

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

Detta orsakar ett dödläge av följande skäl:

  • Användargränssnittstråden anropar metoden async och blockerar väntan på resultatet.
  • Metoden async fångar upp användargränssnittstrådens SynchronizationContext.
  • När asynkroniseringsåtgärden är klar försöker den fortsätta med den insamlade användargränssnittstråden.
  • Användargränssnittstråden blockeras i väntan på att åtgärden ska slutföras.
  • Deadlock inträffar eftersom ingen av operationerna kan fortsätta.

Korstrådsåtgärder

När du behöver uppdatera användargränssnittskontroller från bakgrundstrådar inom asynkrona åtgärder använder du lämpliga hanteringstekniker. Att förstå skillnaden mellan blockering och icke-blockerande metoder är avgörande för dynamiska program.

.NET 9 introducerade Control.InvokeAsync, som tillhandahåller asynkron marshaling till användargränssnittstråden. Till skillnad från Control.Invoke vilka skickar (blockerar den anropande tråden), Control.InvokeAsyncinlägg (icke-blockerande) till användargränssnittstrådens meddelandekö. Mer information om Control.InvokeAsyncfinns i Så här gör du trådsäkra anrop till kontroller.

Viktiga fördelar med InvokeAsync:

  • Icke-blockerande: Returnerar omedelbart, så att den anropande tråden kan fortsätta.
  • Asynkron: Returnerar en Task som kan vänta.
  • Undantagsspridning: Korrekt spridning av undantag tillbaka till den anropande koden.
  • Support för annullering: Stöd CancellationToken för åtgärdsavbokning.
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 verkligt asynkrona åtgärder som måste köras i användargränssnittstråden:

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

Tips/Råd

.NET 9 innehåller analysvarningar (WFO2001) för att identifiera när asynkrona metoder felaktigt skickas till synkrona överlagringar av InvokeAsync. Detta hjälper till att förhindra "fire-and-forget"-beteende.

Anmärkning

Om du använder Visual Basic använde det tidigare kodfragmentet en tilläggsmetod för att konvertera en ValueTask till en Task. Tilläggsmetodkoden är tillgänglig på GitHub.

Metodtips

  • Använd async/await konsekvent: Blanda inte asynkrona mönster med blockeringsanrop.
  • Hantera undantag: Omslut alltid asynkrona åtgärder i try-catch-block i async void händelsehanterare.
  • Ge användarfeedback: Uppdatera användargränssnittet för att visa åtgärdens förlopp eller status.
  • Inaktivera kontroller under åtgärder: Förhindra att användare startar flera åtgärder.
  • Använd CancellationToken: Supportåtgärden avbryts för långvariga uppgifter.
  • Överväg ConfigureAwait(false): Använd i bibliotekskod för att undvika att samla in användargränssnittskontexten när det inte behövs.