Sdílet prostřednictvím


Přehled událostí

Událost je akce, na kterou můžete v kódu odpovědět nebo na ni "zpracovat". Události se obvykle generují akcí uživatele, například kliknutím myši nebo stisknutím klávesy, ale mohou být generovány také programovým kódem nebo systémem.

Aplikace řízené událostmi spouštějí kód v reakci na událost. Každý formulář a ovládací prvek zveřejňuje předdefinovanou sadu událostí, na které můžete reagovat. Pokud je vyvolána jedna z těchto událostí a existuje přidružená obslužná rutina události, vyvolá se obslužná rutina a spustí se kód.

Typy událostí vyvolaných objektem se liší, ale většina ovládacích prvků se běžně používá u mnoha typů. Například většina objektů má Click událost, která se vyvolá, když na ni uživatel klikne.

Poznámka:

Mnoho událostí se koná zároveň s jinými událostmi. Například během události DoubleClick dochází k událostem MouseDown, MouseUpa Click.

Obecné informace o vyvolání a využívání události naleznete v tématu Zpracování a vyvolávání událostí v .NET.

Delegáti a jejich role

Delegáti jsou třídy, které se běžně používají v rozhraní .NET k vytváření mechanismů zpracování událostí. Delegáti zhruba odpovídají ukazatelům na funkce, běžně používaným v jazyce Visual C++ a dalších objektově orientovaných jazycích. Na rozdíl od ukazatelů na funkce jsou však delegáti objektově orientovaní, typově bezpeční a zabezpečení. Pokud ukazatel funkce obsahuje pouze odkaz na konkrétní funkci, delegát se skládá z odkazu na objekt a odkazy na jednu nebo více metod v rámci objektu.

Tento model událostí používá delegáty k vytvoření vazby událostí na metody, které se používají k jejich zpracování. Delegát umožňuje ostatním třídám registrovat se pro oznámení událostí zadáním obslužné metody. Když dojde k události, delegát volá vázanou metodu. Další informace o definování delegátů naleznete v tématu Zpracování a vyvolávání událostí.

Delegáti mohou být vázáni na jednu metodu nebo na více metod, které se označují jako vícesměrové vysílání. Při vytváření delegáta pro událost obvykle vytvoříte vícesměrovou událost. Výjimkou může být událost, která vede ke konkrétnímu postupu (například zobrazení dialogového okna), který by logicky neopakovala vícekrát na událost. Informace o tom, jak vytvořit delegáta vícesměrového vysílání, naleznete v tématu Jak kombinovat delegáty (vícesměrové delegáty).

Delegát vícesměrového vysílání udržuje seznam vyvolání metod, které jsou k němu vázané. Delegát typu multicast podporuje metodu Combine pro přidání metody do seznamu vyvolání a metodu Remove pro její odstranění.

Když aplikace zaznamená událost, ovládací prvek vyvolá událost vyvoláním delegáta pro danou událost. Delegát zase volá vázanou metodu. V nejběžnějším případě (delegát vícesměrového vysílání) delegát volá každou vázanou metodu v seznamu volání, která poskytuje oznámení 1:N. Tato strategie znamená, že ovládací prvek nemusí udržovat seznam cílových objektů pro oznámení události – delegát zpracovává veškerou registraci a oznámení.

Delegáti také umožňují vázání několika událostí na stejnou metodu, což umožňuje notifikaci ve stylu „mnoho na jednoho“. Například událost kliknutí na tlačítko a událost menu-command–click může vyvolat stejný delegát, který pak volá jednu metodu pro zpracování těchto samostatných událostí stejným způsobem.

Mechanismus vazby používaný s delegáty je dynamický: delegát může být za běhu vázán na libovolnou metodu, jejíž podpis odpovídá obslužné metodě události. Pomocí této funkce můžete nastavit nebo změnit vázanou metodu v závislosti na podmínce a dynamicky připojit obslužnou rutinu události k ovládacímu prvku.

Události ve Windows Forms

Události ve Windows Forms jsou deklarovány delegátem EventHandler<TEventArgs> pro metody obslužné rutiny. Každá obslužná rutina události poskytuje dva parametry, které umožňují správně zpracovat událost. Následující příklad ukazuje obslužnou rutinu události ovládacího prvku Button pro událost 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)
{

}

První parametr,sender, poskytuje odkaz na objekt, který vyvolal událost. Druhý parametr , epředá objekt specifický pro událost, která se zpracovává. Odkazováním na vlastnosti objektu (a někdy i jeho metody) můžete získat informace, například o umístění myši u událostí myši nebo o datech, která se přenášejí v událostech přetažení.

Každá událost obvykle vytvoří obslužnou rutinu události s jiným typem objektu události pro druhý parametr. Některé obslužné rutiny událostí, například obslužné rutiny MouseDown a MouseUp událostí, mají pro druhý parametr stejný typ objektu. U těchto typů událostí můžete ke zpracování obou událostí použít stejnou obslužnou rutinu události.

Stejnou obslužnou rutinu události můžete použít také ke zpracování stejné události pro různé ovládací prvky. Pokud máte například skupinu ovládacích prvků RadioButton ve formuláři, můžete pro událost Click každého RadioButton vytvořit jednu společnou obslužnou rutinu. Další informace naleznete v tématu Zpracování řídicí události.

Obslužné rutiny asynchronních událostí

Moderní aplikace často potřebují provádět asynchronní operace v reakci na akce uživatelů, jako je například stahování dat z webové služby nebo přístup k souborům. Obslužné rutiny událostí Modelu Windows Forms je možné deklarovat jako async metody pro podporu těchto scénářů, ale existují důležité aspekty, které brání běžným nástrahám.

Základní vzor obslužné rutiny asynchronní události

Obslužné rutiny událostí lze deklarovat pomocí modifikátoru async (Async v jazyce Visual Basic) a použít await (Await v jazyce Visual Basic) pro asynchronní operace. Vzhledem k tomu, že obslužné rutiny událostí musí vracet void (nebo být deklarovány jako Sub v jazyce Visual Basic), jsou jedním ze vzácných přijatelných async void použití (nebo Async Sub v jazyce 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

Důležité

I když async void se nedoporučuje, je nutné, aby obslužné rutiny událostí (a kód podobný obslužné rutině události, jako je ), protože Control.OnClicknemohou vrátit Task. Vždy zalamujte očekávané operace v try-catch blocích, aby se správně zpracovávaly výjimky, jak je znázorněno v předchozím příkladu.

Běžné nástrahy a zablokování

Výstraha

Nikdy nepoužívejte blokující volání, jako je .Wait(), .Resultnebo .GetAwaiter().GetResult() v obslužných rutinách událostí nebo v jakémkoli kódu uživatelského rozhraní. Tyto vzory můžou způsobit zablokování.

Následující kód ukazuje běžný anti-vzor, který způsobuje zablokování:

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

To způsobí zablokování z následujících důvodů:

  • Vlákno uživatelského rozhraní volá asynchronní metodu a blokuje čekání na výsledek.
  • Asynchronní metoda zachycuje vlákno SynchronizationContextuživatelského rozhraní .
  • Po dokončení asynchronní operace se pokusí pokračovat na zachycené vlákno uživatelského rozhraní.
  • Vlákno uživatelského rozhraní je blokováno čekáním na dokončení operace.
  • K vzájemnému zablokování dochází, protože žádná operace nemůže pokračovat.

Operace napříč vlákny

Pokud potřebujete aktualizovat ovládací prvky uživatelského rozhraní z vláken na pozadí v rámci asynchronních operací, použijte příslušné techniky zařazování. Pochopení rozdílu mezi blokováním a neblokujícími přístupy je zásadní pro responzivní aplikace.

Rozhraní .NET 9 zavedlo Control.InvokeAsync, což poskytuje asynchronní zařazování do vlákna uživatelského rozhraní. Na rozdíl od Control.Invoke toho, který odesílá (blokuje volající vlákno), Control.InvokeAsyncpříspěvky (neblokující) do fronty zpráv vlákna uživatelského rozhraní. Další informace o Control.InvokeAsynctom naleznete v tématu Jak provádět volání ovládacích prvků bezpečných pro přístup z více vláken.

Klíčové výhody InvokeAsync:

  • Neblokující: Vrátí okamžitě a umožní volajícímu vláknu pokračovat.
  • Asynchronní funkce: Vrátí Task hodnotu, kterou lze očekávat.
  • Šíření výjimek: Správně rozšíří výjimky zpět do volajícího kódu.
  • Podpora zrušení: Podporuje CancellationToken zrušení operace.
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

Pro skutečně asynchronní operace, které je potřeba spustit ve vlákně uživatelského rozhraní:

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

Návod

.NET 9 obsahuje upozornění analyzátoru (WFO2001), které pomáhají zjistit, kdy asynchronní metody jsou nesprávně předány InvokeAsyncsynchronním přetížením . To pomáhá zabránit "požáru a zapomenutí" chování.

Poznámka:

Pokud používáte Visual Basic, předchozí fragment kódu použil metodu rozšíření k převodu na ValueTask .Task Kód metody rozšíření je k dispozici na GitHubu.

Osvědčené postupy

  • Používejte asynchronní/await konzistentně: Nekombinujte asynchronní vzory s blokujícími voláními.
  • Zpracování výjimek: Vždy zalamujte asynchronní operace v blocích try-catch v async void obslužných rutinách událostí.
  • Poskytněte zpětnou vazbu uživatelů: Aktualizujte uživatelské rozhraní tak, aby zobrazoval průběh nebo stav operace.
  • Zakázat ovládací prvky během operací: Zabrání uživatelům v spouštění více operací.
  • Použití CancellationToken: Podpora zrušení operace pro dlouhotrvající úlohy.
  • Zvažte konfiguraci Await(false):: Použijte v kódu knihovny, abyste se vyhnuli zachycení kontextu uživatelského rozhraní, pokud není potřeba.