次の方法で共有


イベントの概要

イベントは、コード内で応答できるアクション ("ハンドル") です。 イベントは通常、マウスのクリックやキーの押下などのユーザー アクションによって生成されますが、プログラム コードやシステムによって生成することもできます。

イベント ドリブン アプリケーションは、イベントに応答してコードを実行します。 各フォームとコントロールは、応答できるイベントの定義済みのセットを公開します。 これらのイベントのいずれかが発生し、関連するイベント ハンドラーがある場合は、ハンドラーが呼び出され、コードが実行されます。

オブジェクトによって発生するイベントの種類は異なりますが、多くの種類がほとんどのコントロールに共通しています。 たとえば、ほとんどのオブジェクトには、ユーザーがクリックしたときに発生する Click イベントがあります。

多くのイベントは、他のイベントと共に発生します。 たとえば、DoubleClick イベントが発生する過程で、MouseDownMouseUp、および Click イベントが発生します。

イベントを発生および使用する方法の一般的な情報については、「 .NET でのイベントの処理と発生」を参照してください。

代理人とその役割

デリゲートは、イベント処理メカニズムを構築するために .NET 内で一般的に使用されるクラスです。 デリゲートは、Visual C++ やその他のオブジェクト指向言語でよく使用される関数ポインターとほぼ同じです。 ただし、関数ポインターとは異なり、デリゲートはオブジェクト指向、型セーフ、セキュリティで保護されています。 また、関数ポインターに特定の関数への参照のみが含まれている場合、デリゲートはオブジェクトへの参照と、オブジェクト内の 1 つ以上のメソッドへの参照で構成されます。

このイベント モデルでは、デリゲート を使用して、イベントを処理するために使用されるメソッドにイベントをバインドします。 デリゲートを使用すると、ハンドラー メソッドを指定することで、他のクラスでイベント通知を登録できます。 イベントが発生すると、デリゲートはバインドされたメソッドを呼び出します。 デリゲートを定義する方法の詳細については、「イベントの処理と発生」を参照してください。

デリゲートは、1 つのメソッドまたはマルチキャストと呼ばれる複数のメソッドにバインドできます。 イベントのデリゲートを作成するときは、通常、マルチキャスト イベントを作成します。 まれな例外として、イベントごとに論理的に複数回繰り返されない特定のプロシージャ (ダイアログ ボックスの表示など) が発生するイベントが考えられます。 マルチキャスト デリゲートを作成する方法については、「デリゲート (マルチキャスト デリゲート)を結合する方法」を参照してください。

マルチキャスト デリゲートは、それにバインドされているメソッドの呼び出しリストを保持します。 マルチキャスト デリゲートは、呼び出しリストにメソッドを追加する Combine メソッドと、それを削除する Remove メソッドをサポートしています。

アプリケーションがイベントを記録すると、コントロールはそのイベントのデリゲートを呼び出すことによってイベントを発生させます。 デリゲートは順番にバインドされたメソッドを呼び出します。 最も一般的なケース (マルチキャスト デリゲート) では、デリゲートは呼び出しリスト内のバインドされた各メソッドを順番に呼び出し、1 対多の通知を提供します。 この戦略は、コントロールがイベント通知のターゲット オブジェクトの一覧を保持する必要がないことを意味します。デリゲートはすべての登録と通知を処理します。

デリゲートを使用すると、複数のイベントを同じメソッドにバインドして、多対 1 の通知を行うこともできます。 たとえば、ボタン クリック イベントと menu-command-click イベントの両方で同じデリゲートを呼び出し、1 つのメソッドを呼び出してこれらの個別のイベントを同じ方法で処理できます。

デリゲートで使用されるバインディング メカニズムは動的です。デリゲートは、実行時に、イベント ハンドラーのシグネチャと一致する任意のメソッドにバインドできます。 この機能を使用すると、条件に応じてバインドされたメソッドを設定または変更したり、イベント ハンドラーをコントロールに動的にアタッチしたりできます。

Windows フォームのイベント

Windows フォームのイベントは、ハンドラー メソッドの EventHandler<TEventArgs> デリゲートで宣言されます。 各イベント ハンドラーには、イベントを適切に処理できる 2 つのパラメーターが用意されています。 次の例は、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は、イベントを発生させたオブジェクトへの参照を提供します。 2 番目のパラメーター eは、処理されるイベントに固有のオブジェクトを渡します。 オブジェクトのプロパティ (および場合によってはそのメソッド) を参照することで、マウス イベントのマウスの位置や、ドラッグ アンド ドロップ イベントで転送されるデータなどの情報を取得できます。

通常、各イベントは、2 番目のパラメーターに対して異なるイベント オブジェクト型のイベント ハンドラーを生成します。 MouseDown イベントや MouseUp イベントなどの一部のイベント ハンドラーは、2 番目のパラメーターに同じオブジェクト型を持ちます。 これらの種類のイベントでは、同じイベント ハンドラーを使用して両方のイベントを処理できます。

同じイベント ハンドラーを使用して、異なるコントロールに対して同じイベントを処理することもできます。 たとえば、フォームにRadioButton コントロールのグループがある場合は、すべてのClickRadioButton イベントに対して 1 つのイベント ハンドラーを作成できます。 詳細については、「 コントロール イベントを処理する方法」を参照してください。

非同期イベント ハンドラー

最新のアプリケーションでは、多くの場合、Web サービスからのデータのダウンロードやファイルへのアクセスなど、ユーザーの操作に応答して非同期操作を実行する必要があります。 Windows フォーム イベント ハンドラーは、これらのシナリオをサポートするための async メソッドとして宣言できますが、一般的な落とし穴を回避するための重要な考慮事項があります。

基本的な非同期イベント ハンドラー パターン

イベント ハンドラーは、 async (Visual Basic でAsync ) 修飾子を使用して宣言し、非同期操作に await (Visual Basic のAwait ) を使用できます。 イベント ハンドラーは void を返す (または Visual Basic で Sub として宣言する) 必要があるため、 async void (または Visual Basic では Async Sub ) のまれな使用方法の 1 つです。

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

Important

async voidは推奨されませんが、イベント ハンドラー (およびイベント ハンドラーのようなコード (Control.OnClick など) には、Taskを返すことはできません。 前の例に示すように、待機中の操作を常に try-catch ブロックでラップして、例外を適切に処理します。

一般的な落とし穴とデッドロック

Warnung

イベント ハンドラーや 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 コントロールを更新する必要がある場合は、適切なマーシャリング手法を使用します。 応答性の高いアプリケーションでは、ブロッキングアプローチと非ブロッキングアプローチの違いを理解することが重要です。

.NET 9 では、 Control.InvokeAsyncが導入されました。これは、UI スレッドに非同期でわかりやすいマーシャリングを提供します。 (呼び出し元のControl.Invokeする) とは異なり、UI スレッドのメッセージ キューに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 には、非同期メソッドInvokeAsyncの同期オーバーロードに誤って渡されるタイミングを検出するのに役立つアナライザー警告 (WFO2001) が含まれています。 これは、"ファイア アンド フォーゲット" 動作を防ぐのに役立ちます。

Visual Basic を使用している場合、前のコード スニペットでは拡張メソッドを使用して ValueTaskTaskに変換しました。 拡張メソッド コードは GitHub で入手できます。

ベスト プラクティス

  • 非同期/待機を一貫して使用する: 非同期パターンとブロック呼び出しを混在させる必要はありません。
  • 例外を処理する: async void イベント ハンドラーの try-catch ブロックで非同期操作を常にラップします。
  • ユーザーフィードバックを提供する: 操作の進行状況または状態を表示するように UI を更新します。
  • 操作中にコントロールを無効にする: ユーザーが複数の操作を開始できないようにします。
  • CancellationToken を使用する: 実行時間の長いタスクの操作の取り消しをサポートします。
  • ConfigureAwait(false) を検討する: 不要な場合に UI コンテキストをキャプチャしないように、ライブラリ コードで使用します。