【テクニカル・上級編】実務中級者向け:VB.NETにおける「EventHandler(Of TEventArgs)」を活用したカスタムイベントの発行:型安全な独自イベントで疎結合なコンポーネント間通信を実現する設計術 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NETを掌握する極限の知見:EventHandler(Of TEventArgs)による型安全なカスタムイベント設計術

レガシーなVB6やVBAの `WithEvents` と `RaiseEvent` の呪縛を引きずったまま、現代の.NETエコシステムでコードを書く愚を犯していないだろうか。
「イベントを飛ばすために `Object` 型をキャストし、何が入っているか分からないデータを `EventArgs` に詰め込んでパッシングする」――そのような前時代的な実装は、巨大化した業務アプリケーションにおいて不可視のバグの温床となり、メモリリークや型安全性の崩壊を招く。

本稿では、Windows Formsを中心とした実務開発において、親フォームとバックグラウンドワーカー(あるいは独立したコンポーネント群)の間を完全に疎結合化しつつ、型安全性と圧倒的なパフォーマンスを担保する `EventHandler(Of TEventArgs)` を用いたカスタムイベント設計の極意を授ける。

1. なぜ標準の `EventArgs` では不十分なのか

VB.NET(.NET Framework / .NET Core)におけるイベント駆動アーキテクチャの基本は `System.EventHandler` である。しかし、引数なし、あるいは単なる `EventArgs.Empty` を渡すだけのシグネチャでは、業務アプリケーションで頻発する「進捗率」「処理メッセージ」「動的なデータペイロード」を伝搬させることはできない。

かといって、独自の `EventArgs` クラスを作るたびにボイラープレート(定型コード)を書くのはエレガントではない。ここで投入すべきが、ジェネリック型制約をフル活用した `EventHandler(Of TEventArgs)` である。

これを用いることで、以下のメリットがもたらされる。

  • 完全な型安全性(Type Safety): コンパイル時に型のミスマッチを完全に排除し、不毛な `DirectCast` や `CType` を駆逐する。
  • イミュータブルなデータ設計: 伝搬させるペイロードクラスを読み取り専用(ReadOnly)で設計しやすくし、予期せぬデータ書き換えを防ぐ。
  • メモリ効率の最適化: 不要なボックス化(Boxing)を発生させず、マネージドヒープへの負荷を最小限に抑える。

2. 実践:進捗と例外を安全に伝搬するカスタムイベントの実装

ここでは、重いデータ処理や外部API連携を行う独立したワーカースポーネント(エンジン層)と、それを統括する親フォーム(UI層)を想定する。

まずは、イベントで伝搬させるペイロード(データ構造)を定義する。ここではパフォーマンスと堅牢性を考慮し、`Inherits EventArgs` を冠したイミュータブルなクラスとする。

Option Strict On
Option Explicit On

Imports System

Namespace Enterprise.Components

”’

”’ 処理進捗状況を安全に伝搬するためのカスタムEventArgs
”’

Public NotInheritable Class ProcessProgressEventArgs
Inherits EventArgs

‘ データの改ざんを防ぐため、ReadOnlyプロパティとして公開
Public ReadOnly Property CurrentStep As Integer
Public ReadOnly Property TotalSteps As Integer
Public ReadOnly Property StatusMessage As String
Public ReadOnly Property ProgressPercentage As Double

Public Sub New(currentStep As Integer, totalSteps As Integer, statusMessage As String)
Me.CurrentStep = currentStep
Me.TotalSteps = totalSteps
Me.StatusMessage = statusMessage

If totalSteps > 0 Then
Me.ProgressPercentage = (CDbl(currentStep) / CDbl(totalSteps)) 100.0
Else
Me.ProgressPercentage = 0.0
End If
End Sub

End Class

End Namespace

ワーカースポーネント(発行側)の構築

次に、このイベントを発行するコンポーネントを実装する。ここで重要なのは、UIスレッドへの依存を断ち切り、純粋なビジネスロジックに徹することだ。イベントの発行(Raise)は、リスナーが存在するかどうかの確認とセットで行う。

Option Strict On
Option Explicit On

Imports System.Threading
Imports Enterprise.Components

Namespace Enterprise.Components

Public Class HeavyDataProcessor

‘ EventHandler(Of TEventArgs) を用いた型安全なイベント宣言
‘ イベントの多重登録やスレッド競合を防ぐため、安全なデリゲートinvocationパターンを用いる
Public Event ProgressChanged As EventHandler(Of ProcessProgressEventArgs)

”’

”’ 重い処理を実行し、進捗を外部へ通知する
”’

Public Sub ExecuteProcessing(totalItems As Integer, cancellationToken As CancellationToken)

For i As Integer = 1 To totalItems
‘ キャンセル要求のチェック
cancellationToken.ThrowIfCancellationRequested()

‘ 業務ロジックの模擬(重い処理)
Thread.Sleep(50)

‘ ペイロードの生成
Dim args As New ProcessProgressEventArgs(
currentStep:=i,
totalSteps:=totalItems,
statusMessage:=$”レコード {i} / {totalItems} を処理中…”
)

‘ イベントの発行(安全な呼び出し)
OnProgressChanged(args)
Next

End Sub

”’

”’ イベント発行子メソッド(Protected Virtualパターン)
”’

Protected Overridable Sub OnProgressChanged(e As ProcessProgressEventArgs)
‘ VB.NETでは Event 宣言を行うと自動的に同名のデリゲートフィールドが生成される
‘ マルチスレッド環境での競合を防ぎつつ安全に発火させる
RaiseEvent ProgressChanged(Me, e)
End Sub

End Class

End Namespace

3. 親フォーム(購読側)でのスレッドセーフなハンドリングとメモリ管理

バックグラウンドスレッドからイベントが発火された場合、それを直接Windows FormsのUIコントロール(ProgressBarやLabelなど)に触れさせると、Cross-thread operation not valid 例外がスローされるか、最悪の場合UIスレッドがデッドロックする。

シニアエンジニアであれば、購読側(親フォーム)で必ず `InvokeRequired` を検知し、適切にマーシャリング(UIスレッドへの処理委譲)を行うべきだ。

また、イベントの購読(`AddHandler`)は、明示的な解除(`RemoveHandler` / Dispose)を行わないと、ガベージコレクション(GC)のルートとして残り続け、深刻なメモリリーク(Memory Leak)を引き起こす。このライフサイクルの管理こそが、プロとアマを分ける境界線である。

Option Strict On
Option Explicit On

Imports System.Threading
Imports Enterprise.Components

Public Class MainForm
Inherits Form

Private WithEvents _startButton As Button
Private _progressBar As ProgressBar
Private _statusLabel As Label

‘ ワーカーインスタンス
Private _processor As HeavyDataProcessor
Private _cts As CancellationTokenSource

Public Sub New()
MyBase.New()
InitializeComponentCustom()
End Sub

Private Sub InitializeComponentCustom()
Me.Text = “型安全カスタムイベント実演”
Me.Width = 500
Me.Height = 250

_startButton = New Button() With {.Text = “処理開始”, .Left = 20, .Top = 20, .Width = 100}
_progressBar = New ProgressBar() With {.Left = 20, .Top = 60, .Width = 440, .Height = 23}
_statusLabel = New Label() With {.Left = 20, .Top = 100, .Width = 440, .Height = 20, .Text = “待機中”}

Me.Controls.AddRange(New Control() {_startButton, _progressBar, _statusLabel})
End Sub

Private Async Sub _startButton_Click(sender As Object, e As EventArgs) Handles _startButton.Click
_startButton.Enabled = False
_progressBar.Value = 0
_cts = New CancellationTokenSource()

_processor = New HeavyDataProcessor()

‘ 【極意】EventHandler(Of T) の購読 (AddHandler)
AddHandler _processor.ProgressChanged, AddressOf Processor_ProgressChanged

Try
‘ 非同期タスクとしてワーカーを実行(UIフリーズの防止)
Awaiting Task.Run(Sub() _processor.ExecuteProcessing(100, _cts.Token))
_statusLabel.Text = “処理が正常に完了しました。”
Catch ex As OperationCanceledException
_statusLabel.Text = “処理はキャンセルされました。”
Catch ex As Exception
MessageBox.Show($”エラーが発生しました: {ex.Message}”, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
Finally
‘ 【極意】メモリリーク防止のための購読解除と破棄
RemoveHandler _processor.ProgressChanged, AddressOf Processor_ProgressChanged
_processor = Nothing
_cts.Dispose()
_startButton.Enabled = True
End Try
End Sub

”’

”’ イベントハンドラー:バックグラウンドから呼び出される
”’

Private Sub Processor_ProgressChanged(sender As Object, e As ProcessProgressEventArgs)
‘ 【極意】クロススレッドアクセスの安全なハンドリング
If Me.InvokeRequired Then
‘ UIスレッドへ処理をマーシャリング
Me.Invoke(New Action(Of Object, ProcessProgressEventArgs)(AddressOf Processor_ProgressChanged), sender, e)
Return
End Sub

‘ ここからは完全にUIスレッド上で安全に実行される
_progressBar.Value = CInt(Math.Floor(e.ProgressPercentage))
_statusLabel.Text = $”{e.StatusMessage} ({e.ProgressPercentage:F1}%)”
End Sub

”’

”’ フォーム破棄時の最終防衛ライン
”’

Protected Overrides Sub Dispose(disposing As Boolean)
If disposing Then
‘ 万が一実行中にフォームが閉じられた場合のクリーンアップ
If _cts IsNot Nothing Then
_cts.Cancel()
_cts.Dispose()
End If
If _processor IsNot Nothing Then
RemoveHandler _processor.ProgressChanged, AddressOf Processor_ProgressChanged
End If
End If
MyBase.Dispose(disposing)
End Sub

End Class

4. チーフアーキテクトからの提言:レガシーシステム保守における注意点

社内システムや長年運用されてきたVBAからのリプレイス案件では、VB.NETの持つモダンな言語仕様と、古い設計思想が混在しがちである。以下のアンチパターンに厳しく目を光らせてほしい。

1. `AddHandler` しているのに `RemoveHandler` を忘れる(マネージドメモリリークの温床)
親フォームが子コンポーネントを保持し続ける構造(あるいは逆の参照)において、イベントハンドラーを解除しないままインスタンスを破棄すると、GCはそれを回収できず、画面を開閉するたびにメモリ使用量が右肩上がりに増加する。
2. イベントハンドラー内での重い同期的ブロック
イベントを受け取った側が、その場でファイルI/Oや重いDBクエリを実行すると、発行元の処理(ワーカー)全体のパフォーマンスが劇的に低下する。イベントは「通知」であり、重い処理の同期実行場所ではない。
3. 過剰なイベント粒度
ループの1回ごとにイベントを発行すると、Windowsメッセージキューが飽和し、UIの描画パフォーマンスが崩壊する。必要に応じてスロットリング(間引き処理)を入れる設計的配慮がプロフェッショナルには求められる。

結び

`EventHandler(Of TEventArgs)` を使いこなすことは、単にコードを綺麗に書くことではない。
それは、システム全体の疎結合性を高め、保守性を劇的に向上させ、メモリのライフサイクルまでをも完全に支配下に置くという、シニアエンジニアの矜持そのものである。

レガシーな呪縛を断ち切り、型安全でモダンなVB.NETアーキテクチャをあなたの現場に実装せよ。

タイトルとURLをコピーしました