【VB.NET設計術】`EventHandler(Of TEventArgs)`で実現する!型安全なカスタムイベント駆動アーキテクチャ
業務システムの開発現場において、Windows Formsの標準イベント(`Button.Click`など)の枠を超え、「裏で動いている非同期処理の進捗を親画面に伝えたい」「独自に定義した業務データを安全に子画面から親画面へバトンタッチしたい」という要件に直面したことはないだろうか。
ここで、安易に `Public Event MyEvent As Action(Of String)` のようなデリゲートを乱発したり、パブリック変数を泥臭く参照し合ったりするコードを書くようでは、シニアエンジニアとしての資格はない。そんなスパゲッティコードは、数ヶ月後の保守フェーズで必ずチームを崩壊させる。
今回は、VB.NETが持つ強力かつ型安全なメカニズムである `EventHandler(Of TEventArgs)` を活用したカスタムイベント発行の極意 を伝授する。疎結合でバグの起きない、プロダクションクオリティのコンポーネント間通信の設計思想をここでマスターしてほしい。
—
1. なぜ「生デリゲート」や「AddHandlerの野良定義」ではダメなのか?
VB.NET(.NET Framework / .NET Core)におけるイベントの歴史は古い。かつては `Delegate Sub MyEventHandler(sender As Object, e As MyEventArgs)` とわざわざデリゲート型を自前で宣言するのが定番だった。
しかし、.NETの進化の恩恵を受け忘れてはならない。現在、Microsoftが推奨し、我々プロフェッショナルが採用すべき標準イディオムはこれだ。
.net
‘ 【アンチパターン】もう古い!無駄にデリゲート型を宣言するスタイル
Public Delegate Sub ProgressChangedEventHandler(sender As Object, e As ProgressEventArgs)
Public Event ProgressChanged As ProgressChangedEventHandler
‘ 【モダンな正解】EventHandler(Of T) によるスマートな定義
Public Event ProgressChanged As EventHandler(Of ProgressEventArgs)
なぜ `EventHandler(Of TEventArgs)` なのか?
1. ボイラープレート(定型コード)の削減: 無駄なデリゲート宣言を排除し、コードのノイズを極限まで減らせる。
2. フレームワーク規約との完全な一致: .NETの標準ライブラリ(`EventArgs` 継承パターン)と完全にシームレスに統合され、他の開発者にとっても直感的な可読性を維持できる。
3. 型安全性(Type Safety)の担保: `Object` 型のキャスト地獄から解放され、コンパイル時に厳密な型の整合性が保証される。
—
2. 【実務設計】堅牢なカスタムイベント実装の4大原則
現場で使えるコードを書く前に、設計レイヤーでの鉄則を確認する。
1. `EventArgs` は必ず `Inherits EventArgs` し、不変(Immutable)に設計せよ
イベント引数として渡すデータは、イベントを受け取った側で勝手に書き換えられないよう、プロパティは原則 `ReadOnly`(読み取り専用)にすべし。
2. イベント発火ラッパーメソッド `OnXxx` を用意せよ
VB.NETではイベントを外部から直接 `RaiseEvent` することはできないが、クラス内部で安全に発火させるためのProtectedな仮想メソッド(`Protected Overridable Sub OnProgressChanged`)を用意するのが .NETのベストプラクティスだ。
3. マルチスレッドセーフティを意識せよ
バックグラウンドスレッド(データベース接続や重いファイルIO)からUIスレッドへイベントを飛ばす場合、呼び出し元のコンテキストに配慮した設計が求められる。
—
3. 【コピペOK】ファイル一括処理・進捗通知のプロダクションコード
百聞は一見に如かず。ここでは、「大量のCSVファイルを非同期でインポートしつつ、リアルタイムに進捗率と現在のファイル名を親フォーム(UI)に通知する」という、業務システムでありがちなシーンを想定した完全な実装例を示す。
ステップ1: 型安全なカスタムイベント引数クラスの定義
まず、親へ渡すデータを包むカプセルを作成する。
.net
Imports System
”’
”’
Public Class FileProcessEventArgs
Inherits EventArgs
‘ 読み取り専用プロパティでデータの安全性を担保
Public ReadOnly Property CurrentFileName As String
Public ReadOnly Property ProgressPercentage As Integer
Public ReadOnly Property Message As String
Public Sub New(fileName As String, percentage As Integer, message As String)
Me.CurrentFileName = fileName
Me.ProgressPercentage = percentage
Me.Message = message
End Sub
End Class
ステップ2: ビジネスロジック・コンポーネント(イベント発行側)
次に、実際にファイル処理を行うWorkerクラスを定義する。このクラスはUI(Windows Forms)の存在を一切知る必要がない(=完全な疎結合)。
.net
Imports System.IO
Imports System.Threading.Tasks
”’
”’
Public Class FileProcessor
‘ EventHandler(Of T) を用いた型安全なカスタムイベントの宣言
Public Event ProgressChanged As EventHandler(Of FileProcessEventArgs)
Public Event ProcessCompleted As EventHandler(Of EventArgs)
”’
”’
Protected Overridable Sub OnProgressChanged(e As FileProcessEventArgs)
‘ イベントが購読されている(Nullでない)場合のみ安全に発火
RaiseEvent ProgressChanged(Me, e)
End Sub
Protected Overridable Sub OnProcessCompleted(e As EventArgs)
RaiseEvent ProcessCompleted(Me, e)
End Sub
”’
”’
Public Async Function ExecuteAsync(targetDirectory As String) As Task
If Not Directory.Exists(targetDirectory) Then
Throw New DirectoryNotFoundException($”指定されたディレクトリが存在しません: {targetDirectory}”)
End If
Dim files = Directory.GetFiles(targetDirectory, “.csv”)
Dim totalFiles = files.Length
If totalFiles = 0 Then Exit Function
For i As Integer = 0 To totalFiles – 1
Dim filePath = files(i)
Dim fileName = Path.GetFileName(filePath)
‘ ─── ここで実際の重い処理(DB連携やファイルIO)を行う想定 ───
Await Task.Delay(500) ‘ ダミーの負荷処理
‘ ──────────────────────────────────────────────
‘ 進捗率を計算
Dim percentage As Integer = CInt(((i + 1) / totalFiles) 100)
‘ カスタムイベント引数を生成して発火
Dim args As New FileProcessEventArgs(fileName, percentage, $”{i + 1}/{totalFiles} 件目を処理中…”)
OnProgressChanged(args)
Next
‘ 完了通知
OnProcessCompleted(EventArgs.Empty)
End Function
End Class
ステップ3: 親フォーム(UI層)での購読と制御
最後に、UI側でコンポーネントをインスタンス化し、`AddHandler` でイベントをスマートにハンドリングする。
.net
Public Class MainForm
Private Async Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
‘ UIの多重押し防止
btnStart.Enabled = False
ProgressBar1.Value = 0
‘ ビジネスロジックのインスタンス化
Dim processor As New FileProcessor()
‘ 【重要】AddHandlerによるイベントの購読
AddHandler processor.ProgressChanged, AddressOf Processor_ProgressChanged
AddHandler processor.ProcessCompleted, AddressOf Processor_ProcessCompleted
Try
‘ 非同期処理の実行(UIスレッドをブロックしない)
Await processor.ExecuteAsync(“C:\ImportData”)
Catch ex As Exception
MessageBox.Show($”エラーが発生しました: {ex.Message}”, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
Finally
‘ 【鉄則】メモリリーク防衛のため、不要になったハンドラーは必ず解除する
RemoveHandler processor.ProgressChanged, AddressOf Processor_ProgressChanged
RemoveHandler processor.ProcessCompleted, AddressOf Processor_ProcessCompleted
btnStart.Enabled = True
End Try
End Sub
”’
”’
Private Sub Processor_ProgressChanged(sender As Object, e As FileProcessEventArgs)
‘ スレッドセーフにUIを更新(必要に応じて InvokeRequired の考慮)
If Me.InvokeRequired Then
Me.Invoke(Sub() Processor_ProgressChanged(sender, e))
Return
End If
‘ UIコントロールへの反映
ProgressBar1.Value = e.ProgressPercentage
lblStatus.Text = $”{e.CurrentFileName} – {e.Message}”
End Sub
”’
”’
Private Sub Processor_ProcessCompleted(sender As Object, e As EventArgs)
If Me.InvokeRequired Then
Me.Invoke(Sub() Processor_ProcessCompleted(sender, e))
Return
End If
MessageBox.Show(“すべてのファイルのインポートが完了しました。”, “完了”, MessageBoxButtons.OK, MessageBoxIcon.Information)
lblStatus.Text = “待機中”
End Sub
End Class
—
4. プロが教える「現場の地雷原」と回避策
上記のコードは美しく堅牢だが、実務でこれを運用する際には、さらに次の2点を頭に入れておかなければならない。
① メモリリーク(Memory Leak)の恐怖
`AddHandler` を使ったら、必ず対応する `RemoveHandler` を呼ぶ、あるいはオブジェクトの寿命を厳密に管理すること。特に、長生きする親オブジェクト(MainFormなど)が、短命な子オブジェクトのイベントを購読し続けたまま `RemoveHandler` を忘れると、GC(ガベージコレクション)が働かなくなり、深刻なメモリリークを引き起こす。
※設計に自信がない場合は、弱参照イベントパターン(Weak Event Pattern)の導入を検討せよ。
② データベース・ファイル連携時のトランザクション境界
今回のサンプルではイベント内で進捗を通知しているが、「データベースのトランザクション管理」と「UIへの進捗通知」の責務を混同してはならない。
DBのコミット/ロールバック単位はビジネスロジック側(`FileProcessor`)のトランザクションスコープ内に閉じ込め、UI側はあくまで「結果の通知を受ける鏡」に徹するべきだ。UIの描画遅延が原因でDBのコネクションを長時間占有するような設計は、絶対に避けること。
—
5. まとめ
VB.NETにおける `EventHandler(Of TEventArgs)` を用いたカスタムイベント設計は、単なる「書き方のテクニック」ではない。
- UI層とビジネスロジック層を完全に切り離す(関心の分離)
- コンパイル時の型チェックの恩恵を最大限に受ける
- 保守性が高く、チーム開発で破綻しないコードベースを作る
このアーキテクチャの引き出しを持っていれば、どれほど複雑な業務要件のシステム改修であっても、怯むことなく优雅(エレガント)に対応できるはずだ。明日からのコーディングで、ぜひこの設計術を取り入れてみてほしい。
