Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Событие — это действие, на которое можно реагировать или "обрабатывать" в коде. События обычно создаются действием пользователя, например щелчком мыши или нажатием клавиши, но их также можно создать с помощью кода программы или системы.
Приложения, управляемые событиями, выполняют код в ответ на событие. Каждая форма и элемент управления предоставляют предопределенный набор событий, на которые можно реагировать. Если возникает одно из этих событий и есть связанный обработчик событий, обработчик вызывается и выполняется код.
Типы событий, создаваемые объектом, различаются, но многие типы являются общими для большинства элементов управления. Например, большинство объектов имеют Click событие, которое возникает, когда пользователь щелкает его.
Замечание
Многие события происходят наряду с другими событиями. Например, в ходе события DoubleClick происходят события MouseDown, MouseUpи Click.
Общие сведения о том, как создавать и использовать событие, см. в разделе "Обработка и создание событий в .NET".
Делегаты и их роль
Делегаты — это классы, часто используемые в .NET для создания механизмов обработки событий. Делегаты примерно приравниваются к указателям функций, часто используемым в Visual C++ и других объектно-ориентированных языках. В отличие от указателей функций, делегаты обладают объектно-ориентированностью, типобезопасностью и безопасностью. Кроме того, где указатель функции содержит только ссылку на определенную функцию, делегат состоит из ссылки на объект и ссылки на один или несколько методов в объекте.
Эта модель событий использует делегатов для привязки событий к методам, используемым для их обработки. Делегат позволяет другим классам регистрировать уведомления о событиях, указывая метод обработчика. При возникновении события делегат вызывает привязанный метод. Дополнительные сведения о том, как определить делегатов, см. в разделе Обработка и генерация событий.
Делегаты могут быть привязаны к одному методу или к нескольким методам, что называется мультикастингом. При создании делегата для события обычно создается мультиметодное событие. Редкое исключение может быть событием, которое приводит к определенной процедуре (например, отображению диалогового окна), которая не будет логически повторяться несколько раз на событие. Сведения о создании многопотокового делегата см. в способах объединения делегатов (многопотоковые делегаты).
Делегат многоадресной рассылки поддерживает список вызовов методов, привязанных к нему. Делегат многоадресной рассылки поддерживает метод Combine для добавления метода в список вызовов и метода Remove для его удаления.
Когда приложение записывает событие, элемент управления вызывает событие путем вызова делегата для этого события. Делегат в свою очередь вызывает привязанный метод. В наиболее распространённом случае (многоадресный делегат) делегат вызывает каждый связанный метод в списке вызовов по очереди, что обеспечивает уведомление "от одного ко многим". Эта стратегия означает, что элемент управления не должен поддерживать список целевых объектов для уведомления о событиях, поскольку делегат берет на себя обработку всех регистраций и рассылку уведомлений.
Делегаты также позволяют привязывать несколько событий к одному методу, обеспечивая множественное уведомление. Например, событие нажатия кнопки и событие нажатия меню может вызывать один и тот же делегат, который затем вызывает один метод для обработки этих отдельных событий так же.
Механизм привязки, используемый с делегатами, является динамическим: делегат может быть привязан во время выполнения к любому методу, подпись которого соответствует обработчику событий. С помощью этой функции можно настроить или изменить привязанный метод в зависимости от условия и динамически подключить обработчик событий к элементу управления.
События в Windows Forms
События в Windows Forms объявляются делегатом EventHandler<TEventArgs> для методов обработчика. Каждый обработчик событий предоставляет два параметра, которые позволяют правильно обрабатывать событие. В следующем примере показан обработчик событий для события Button элемента управления 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)
{
}
Первый параметр,sender, предоставляет ссылку на объект, который вызвал событие. Второй параметр, e, передает объект, характерный для обрабатываемого события. Обращаясь к свойствам объекта (а иногда и к его методам), можно получить информацию о таких вещах, как расположение мыши для событий мыши или данных, передаваемых в событиях drag-and-drop.
Обычно каждое событие создает обработчик событий с другим типом объекта события для второго параметра. Некоторые обработчики событий, например для событий MouseDown и MouseUp, имеют тот же тип объекта для второго параметра. Для этих типов событий можно использовать один и тот же обработчик событий для обработки обоих событий.
Вы также можете использовать один и тот же обработчик событий для обработки одного и того же события для различных элементов управления. Например, если у вас есть группа RadioButton элементов управления в форме, можно создать один обработчик событий для Click каждого события RadioButton. Дополнительные сведения см. в разделе "Обработка события элемента управления".
Асинхронные обработчики событий
Современные приложения часто должны выполнять асинхронные операции в ответ на действия пользователя, такие как скачивание данных из веб-службы или доступ к файлам. Обработчики событий Windows Forms можно объявить как async методы для поддержки этих сценариев, но существуют важные рекомендации, чтобы избежать распространенных ошибок.
Базовый шаблон обработчика событий async
Обработчики событий можно объявить с модификатором async (Async в Visual Basic) и использовать await (Await в Visual Basic) для асинхронных операций. Так как обработчики событий должны возвращать void (или объявляться как Sub в Visual Basic), они являются одним из редких допустимых вариантов использования async void (или Async Sub в 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
Это важно
Хотя async void не рекомендуется, это необходимо для обработчиков событий (и кода, подобного обработчикам событий, например Control.OnClick), так как они не могут возвращать Task. Всегда упаковывайте ожидаемые операции в try-catch блоки для правильной обработки исключений, как показано в предыдущем примере.
Распространенные ловушки и взаимоблокировки
Предупреждение
Никогда не используйте такие блокирующие вызовы, как .Wait(), .Result или .GetAwaiter().GetResult() в обработчиках событий или в любом коде пользовательского интерфейса. Эти шаблоны могут привести к взаимоблокировкам.
Следующий код демонстрирует распространенный антипаттерн, который приводит к взаимоблокировкам:
// 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
Это приводит к взаимоблокировкам по следующим причинам:
- Поток пользовательского интерфейса вызывает асинхронный метод и блокирует ожидание результата.
- Асинхронный метод захватывает поток пользовательского интерфейса
SynchronizationContext. - После завершения асинхронной операции он пытается продолжить работу в захваченном потоке пользовательского интерфейса.
- Поток пользовательского интерфейса заблокирован до завершения операции.
- Взаимоблокировка возникает, потому что ни одна операция не может продолжаться.
Операции между потоками
Если необходимо обновить элементы управления пользовательского интерфейса из фоновых потоков в асинхронных операциях, используйте соответствующие методы маршалинга. Понимание разницы между блокирующими и неблокирующими подходами имеет решающее значение для адаптивных приложений.
В .NET 9 была введена функция Control.InvokeAsync, которая обеспечивает асинхронное маршалирование в поток пользовательского интерфейса. В отличие от Control.Invoke, который отправляет (блокирует вызывающий поток), Control.InvokeAsyncпостит (не блокируя) в очередь сообщений потока пользовательского интерфейса. Дополнительные сведения о том, как выполнять потокобезопасные вызовы для элементов управления, см. в Control.InvokeAsync.
Основные преимущества InvokeAsync:
- Неблокировка: возвращается немедленно, позволяя вызывающему потоку продолжаться.
-
Поддерживает асинхронный режим: Возвращает объект
Task, который может быть ожидаем. - Распространение исключений: правильно распространяет исключения обратно в вызывающий код.
-
Поддержка отмены: поддерживается
CancellationTokenдля отмены операций.
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
Для действительно асинхронных операций, которые должны выполняться в потоке пользовательского интерфейса:
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
Подсказка
.NET 9 содержит предупреждения анализатора (WFO2001), помогающие определить, когда асинхронные методы неправильно передаются синхронным перегрузкам InvokeAsync. Это помогает предотвратить поведение в стиле "fire-and-forget" (запустил и забыл).
Замечание
Если вы используете Visual Basic, предыдущий фрагмент кода использовал метод расширения для преобразования ValueTask в объект Task. Код метода расширения доступен на сайте GitHub.
Лучшие практики
- Последовательно используйте async/await: не смешивайте асинхронные шаблоны с блокирующими вызовами.
-
Обработка исключений. Всегда упаковывать асинхронные операции в блоки try-catch в
async voidобработчиках событий. - Предоставление отзывов пользователей: обновите пользовательский интерфейс, чтобы отобразить ход выполнения операции или состояние.
- Отключить элементы управления во время операций: запретить пользователям запускать несколько операций.
- Используйте CancellationToken: поддержка отмены операций для длительных задач.
- Рассмотрите возможность ConfigureAwait(false): используйте в коде библиотеки, чтобы избежать записи контекста пользовательского интерфейса, когда это не требуется.
Связанный контент
.NET Desktop feedback