다음을 통해 공유


이벤트 개요

이벤트는 코드를 통해 응답하거나 “처리”할 수 있는 작업입니다. 이벤트는 일반적으로 마우스를 클릭하거나 키를 누르는 등의 사용자 작업에 의해 생성되지만 프로그램 코드 또는 시스템에서 생성될 수도 있습니다.

이벤트 기반 애플리케이션은 이벤트에 대한 응답으로 코드를 실행합니다. 각 양식 및 컨트롤은 응답할 수 있는 미리 정의된 이벤트 집합을 노출합니다. 이러한 이벤트 중 하나가 발생하고 연결된 이벤트 처리기가 있는 경우 처리기가 호출되고 코드가 실행됩니다.

개체에 의해 발생하는 이벤트 형식은 다양하지만 대부분의 형식은 대다수 컨트롤에 공통적으로 적용됩니다. 예를 들어 대부분의 개체에는 Click 사용자가 클릭할 때 발생하는 이벤트가 있습니다.

비고

많은 이벤트가 다른 이벤트와 함께 발생합니다. 예를 들어 DoubleClick 이벤트가 발생하는 과정에서는 MouseDown, MouseUpClick 이벤트도 발생합니다.

이벤트를 발생시키고 사용하는 방법에 대한 일반적인 내용은 .NET에서 이벤트 처리 및 발생을 참조하세요.

대리자 및 해당 역할

대리자는 .NET에서 이벤트 처리 메커니즘을 빌드하는 데 흔히 사용되는 클래스입니다. 대리자는 Visual C++ 및 기타 개체 지향 언어에서 일반적으로 사용되는 함수 포인터와 거의 비슷합니다. 그러나 함수 포인터와는 달리 대리자는 개체 지향적이고 형식이 안전하며 보안이 유지됩니다. 또한 함수 포인터는 특정 함수에 대한 참조만을 포함하지만 대리자는 개체 참조와 해당 개체 내에 있는 하나 이상의 메서드에 대한 참조로 구성됩니다.

해당 이벤트 모델은 ‘대리자’를 사용하여 이벤트를 이벤트 처리에 사용되는 메서드에 바인딩합니다. 대리자는 처리기 메서드를 지정하여 다른 클래스가 이벤트 알림을 등록할 수 있도록 설정합니다. 이벤트가 발생하면 대리자가 bound 메서드를 호출합니다. 대리자를 정의하는 방법에 대한 자세한 내용은 이벤트 처리 및 발생을 참조하세요.

대리자는 단일 메서드에 바인딩될 수도 있고 여러 메서드에 바인딩(멀티캐스트)될 수도 있습니다. 이벤트 대리자를 만드는 경우 일반적으로 멀티캐스트 이벤트를 만듭니다. 논리적으로 이벤트당 여러 번 반복되지 않는 특정 프로시저(예: 대화 상자 표시)를 수행하는 이벤트가 드물지만 예외적으로 발생할 수 있습니다. 멀티캐스트 대리자를 만드는 방법에 대한 자세한 내용은 대리자를 결합하는 방법(멀티캐스트 대리자)을 참조하세요.

멀티캐스트 대리자는 바인딩된 메서드의 호출 목록을 유지 관리합니다. 호출 목록에 메서드를 추가하기 위한 Combine 메서드와 해당 메서드를 제거하기 위한 Remove 메서드를 지원합니다.

애플리케이션이 이벤트를 기록할 때 컨트롤은 해당 이벤트에 대한 대리자를 호출하여 이벤트를 발생합니다. 그러면 대리자는 bound 메서드를 호출합니다. 가장 일반적인 경우(멀티캐스트 대리자)에서는 대리자가 호출 목록에 바인딩된 각 메서드를 차례로 호출하므로 일대다 알림이 제공됩니다. 이 전략이 사용되므로 컨트롤이 이벤트 알림을 위해 대상 개체 목록을 유지하지 않아도 됩니다. 대리자가 모든 등록과 알림을 처리하기 때문입니다.

또한 대리자는 여러 이벤트를 같은 메서드에 바인딩할 수 있도록 함으로써 다대일 알림도 허용합니다. 예를 들어 단추 클릭 이벤트와 메뉴 명령 클릭 이벤트 모두가 같은 대리자를 호출할 수 있으며, 이 대리자는 단일 메서드를 호출하여 두 개별 이벤트를 같은 방식으로 처리합니다.

대리자에는 동적 바인딩 메커니즘이 사용되므로 런타임에 이벤트 처리기의 시그니처와 일치하는 시그니처를 가진 모든 메서드에 대리자를 바인딩할 수 있습니다. 이 기능을 사용하면 조건에 따라 bound 메서드를 설정하거나 변경하고 이벤트 처리기를 컨트롤에 동적으로 연결할 수 있습니다.

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처리 중인 이벤트와 관련된 개체를 전달합니다. 개체의 속성 및 경우에 따라 메서드를 참조하여 마우스 이벤트의 마우스 위치 또는 끌어서 놓기 이벤트에서 전송되는 데이터와 같은 정보를 얻을 수 있습니다.

일반적으로 각 이벤트는 두 번째 매개 변수에 대해 다른 이벤트 개체 형식의 이벤트 처리기를 생성합니다. MouseDownMouseUp 이벤트의 이벤트 처리기와 같은 일부 이벤트 처리기는 두 번째 매개 변수에 대해 동일한 개체 형식을 사용합니다. 이 유형의 이벤트에 대해 동일한 이벤트 처리기를 사용하여 두 이벤트를 모두 처리할 수 있습니다.

동일한 이벤트 처리기를 사용하여 다양한 컨트롤에 대한 동일한 이벤트를 처리할 수도 있습니다. 예를 들어 폼에 컨트롤 그룹이 RadioButton 있는 경우 모든 Click이벤트에 대한 RadioButton 단일 이벤트 처리기를 만들 수 있습니다. 자세한 내용은 컨트롤 이벤트를 처리하는 방법을 참조하세요.

비동기 이벤트 처리기

최신 애플리케이션은 웹 서비스에서 데이터를 다운로드하거나 파일에 액세스하는 등의 사용자 작업에 대한 응답으로 비동기 작업을 수행해야 하는 경우가 많습니다. Windows Forms 이벤트 처리기는 이러한 시나리오를 지원하는 메서드로 async 선언할 수 있지만 일반적인 문제를 방지하기 위한 중요한 고려 사항이 있습니다.

기본 비동기 이벤트 처리기 패턴

이벤트 처리기는 async (Async는 Visual Basic에서) 한정자로 선언할 수 있으며, 비동기 작업에는 await (Await는 Visual Basic에서)를 사용할 수 있습니다. 이벤트 처리기는 void를 반환해야 하거나 Visual Basic에서는 Sub로 선언되어야 하므로, 이는 드물게 허용되는 async void의 사용(또는 Visual Basic에서의 Async Sub) 중 하나입니다.

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

중요합니다

권장되지는 않지만, 이벤트 처리기(및 이벤트 처리기와 유사한 코드인 Control.OnClick)는 Task을/를 반환할 수 없으므로 async void이 필요합니다. 이전 예제와 같이 대기 중인 작업을 블록으로 try-catch 래핑하여 예외를 제대로 처리합니다.

일반적인 문제 및 교착 상태

경고

이벤트 처리기나 UI 코드에서는 .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

이렇게 하면 다음과 같은 이유로 교착 상태가 발생합니다.

  • UI 스레드는 비동기 메서드를 호출하고 결과를 기다리는 것을 차단합니다.
  • 비동기 메서드는 UI 스레드를 SynchronizationContext캡처합니다.
  • 비동기 작업이 완료되면 캡처된 UI 스레드에서 계속하려고 합니다.
  • 작업이 완료될 때까지 UI 스레드가 차단됩니다.
  • 교착 상태는 두 작업을 모두 진행할 수 없으므로 발생합니다.

스레드 간 작업

비동기 작업 내의 백그라운드 스레드에서 UI 컨트롤을 업데이트해야 하는 경우 적절한 마샬링 기술을 사용합니다. 응답성이 뛰어난 애플리케이션에서는 차단 방식과 비차단 방식의 차이를 이해하는 것이 중요합니다.

UI 스레드에 비동기 친화적인 마샬링을 제공하는 .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

UI 스레드에서 실행해야 하는 진정한 비동기 작업의 경우:

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을 사용하는 경우, 이전 코드 조각은 확장 메서드를 사용하여 ValueTaskTask로 변환했습니다. 확장 메서드 코드는 GitHub에서 사용할 수 있습니다.

모범 사례

  • 비동기/await를 일관되게 사용: 비동기 패턴을 차단 호출과 혼합하지 마세요.
  • 예외 처리: 이벤트 핸들러에서 비동기 작업을 항상 try-catch 블록으로 래핑합니다.
  • 사용자 피드백 제공: 작업 진행률 또는 상태를 표시하도록 UI를 업데이트합니다.
  • 작업 중 컨트롤 사용 안 함: 사용자가 여러 작업을 시작하지 못하도록 합니다.
  • CancellationToken 사용: 장기 실행 작업에 대한 작업 취소를 지원합니다.
  • ConfigureAwait(false)를 고려합니다. 필요하지 않은 경우 라이브러리 코드에서 UI 컨텍스트를 캡처하지 않도록 합니다.