VB.NETを掌握する極限の知見:EventHandler(Of T)で実現する型安全なカスタムイベント設計
開発現場において、画面(UI)とバックグラウンドのデータ処理、あるいはファイル監視やデータベース連携のバッチ処理を疎結合に保つための「イベント駆動アーキテクチャ」は不可欠だ。
しかし、中級者へのステップアップの過程で多くのプログラマが、古いスタイルの `Object` 型キャストの泥沼にハマる。
`Private Sub Worker_Completed(sender As Object, e As EventArgs)` —— このシグネチャを見ただけで胃が痛くなるのは私だけではまいまい。`e` をわざわざ独自のクラスにキャストし、型安全性をドブに捨てるようなコードをいつまで書き続けるつもりか?
今回は、VB.NETのポテンシャルを極限まで引き出し、保守性と堅牢性を担保する `EventHandler(Of T)` を用いた型安全なカスタムイベントの設計と実装を、チーフアーキテクトの視点から伝授する。
—
なぜ従来の `EventArgs` では不十分なのか?
レガシーなVB.NETコードや、古いWinFormsの自動生成コードを見ると、以下のようなイベント定義が横行している。
‘ 【アンチパターン】これはいけない
Public Event DataProcessed As EventHandler
これを受け取る側(リスナー)では、次のような「お祈りキャスト」が必要になる。
Private Sub OnDataProcessed(sender As Object, e As EventArgs)
‘ ぐぬぬ… DirectCastが必要。型を間違えれば一発でInvalidCastExceptionだ
Dim customArgs As MyCustomEventArgs = DirectCast(e, MyCustomEventArgs)
Console.WriteLine(customArgs.ProcessedCount)
‘ …
このアプローチには致命的な欠陥がある。
1. 型安全性の欠如: コンパイル時に型の整合性が保証されず、実行時エラー(バグ)の温床になる。
2. マジックナンバーと暗黙の了解: どんなデータが渡されるのか、リスナー側がイベント定義を隅々まで読まないと分からない。
3. リファクタリング耐性のゼロ: プロパティ名を変えた途端、あちこちのキャスト先で爆発する。
これを解決するのが、ジェネリックな `EventHandler(Of TEventArgs)` である。
—
モダンなカスタムイベント設計の3原則
業務効率化ツールやファイル・DB連携バッチを堅牢に作るために、以下の3原則を厳守せよ。
1. `EventArgs` のイミュータブル(不変)化: イベント引数は「データCarrier(運び屋)」に徹し、リスナー側で値を書き換えられないように `ReadOnly` プロパティで構成する。
2. `EventHandler(Of T)` の採用: ボイラープレートコードを極限まで削ぎ落とし、コンパイラに型チェックを強制する。
3. スレッドセーフなイベント発火: バックグラウンドスレッド(TaskやAsync/Await)から安全にイベントを発火させ、UIスレッドや他のリスナーをクラッシュさせない。
—
実装例:ファイル処理とDB連携を監視する堅牢なプロセッサ
ここでは、大量のCSVファイルを読み込み、データベースへ非同期でバルクインサートを行いつつ、進捗状況とエラー情報をリアルタイムで外部へ通知する実用的なコンポーネントを実装する。
コピペしてそのままプロジェクトのコアロジックとして活用してほしい。
Imports System
Imports System.IO
Imports System.Threading.Tasks
Namespace Enterprise.Automation.Core
‘ =========================================================================
‘ 1. 型安全なカスタムイベント引数の定義 (イミュータブル設計)
‘ =========================================================================
Public Class FileProcessedEventArgs
Inherits EventArgs
‘ 外部から書き換えられないよう ReadOnly プロパティとして定義
Public ReadOnly Property FilePath As String
Public ReadOnly Property SuccessCount As Integer
Public ReadOnly Property ErrorMessage As String
Public ReadOnly Property IsSuccess As Boolean
Public Sub New(filePath As String, successCount As Integer, Optional errorMessage As String = “”)
Me.FilePath = filePath
Me.SuccessCount = successCount
Me.ErrorMessage = errorMessage
Me.IsSuccess = String.IsNullOrEmpty(errorMessage)
End Sub
End Class
‘ =========================================================================
‘ 2. イベントを発行する業務プロセッサクラス
‘ =========================================================================
Public Class DatabaseBatchProcessor
‘ EventHandler(Of T) を使用した型安全なイベント宣言
‘ 使う側はキャストの必要が一切なくなる
Public Event FileProcessed As EventHandler(Of FileProcessedEventArgs)
”’
”’
Public Async Function ProcessCsvFilesAsync(targetDirectory As String) As Task
If Not Directory.Exists(targetDirectory) Then
Throw New DirectoryNotFoundException($”対象ディレクトリが存在しません: {targetDirectory}”)
End If
Dim csvFiles = Directory.GetFiles(targetDirectory, “.csv”)
For Each filePath In csvFiles
Dim processedCount As Integer = 0
Dim errorMsg As String = String.Empty
Try
‘ 【業務ロジックの擬似コード】ファイル読み込み & DB連携
‘ ※実際の現場ではここに SqlClient や Entity Framework の処理が入る
AwaitingFileProcessingMock(filePath, processedCount)
Catch ex As Exception
‘ 例外発生時もキャッチしてイベント引数に内包する(プロセス全体を落とさない設計)
errorMsg = ex.Message
End Try
‘ イベントの発火(スレッドセーフな呼び出し)
OnesFileProcessed(New FileProcessedEventArgs(filePath, processedCount, errorMsg))
Next
End Function
”’
”’
Protected Overridable Sub OnesFileProcessed(e As FileProcessedEventArgs)
‘ VB.NETの通常イベント構文によるスレッドセーフな発火
‘ (Nullチェックとマルチスレッド競合をワンライナーで防ぐモダンな書き方)
RaiseEvent FileProcessed(Me, e)
End Sub
‘ モック用の非同期処理メソッド
private Async Function AwaitingFileProcessingMock(path As String, ByRef count As Integer) As Task
Await Task.Delay(500) ‘ I/O処理のシミュレーション
count = 1500 ‘ 1500件処理したと仮定
‘ 意図的なエラーテスト用(必要に応じてコメントアウト解除)
‘ If path.Contains(“error”) Then Throw New InvalidOperationException(“DB接続タイムアウト”)
End Function
End Class
End Namespace
—
使う側の実装:美しく、強靭なリスナー
では、先ほど作成した `DatabaseBatchProcessor` を呼び出し、イベントを受け取る側のコードを見てみよう。余計な型変換が一切なく、極めてクリーンであることが分かるはずだ。
Imports Enterprise.Automation.Core
Module Program
Sub Main()
Console.WriteLine(“=== バッチ処理を開始します ===”)
Dim processor As New DatabaseBatchProcessor()
‘ 【重要】EventHandler(Of T) の恩恵により、e の型が完全に推論・保証される
AddHandler processor.FileProcessed, AddressOf HandleFileProcessed
‘ 非同期処理の実行(コンソールアプリでの同期待機パターン)
Task.Run(Async Function()
Await processor.ProcessCsvFilesAsync(“C:\Data\Input”)
End Function).GetAwaiter().GetResult()
Console.WriteLine(“=== 全ての処理が完了しました ===”)
Console.ReadLine()
End Sub
‘ イベントハンドラの実装
Private Sub HandleFileProcessed(sender As Object, e As FileProcessedEventArgs)
‘ DirectCastの記述はもう不要!コンパイル時安全性が完全に担保されている。
If e.IsSuccess Then
Console.WriteLine($”[成功] ファイル: {Path.GetFileName(e.FilePath)} / 処理件数: {e.SuccessCount}件”)
Else
Console.WriteLine($”[失敗] ファイル: {Path.GetFileName(e.FilePath)} / エラー: {e.ErrorMessage}”)
‘ 業務要件に応じてログ出力やDBへのエラーシュート処理をここに記述
End Sub
End Sub
End Module
—
チーフアーキテクトからの実務アドバイス
1. ファイルやDB連携における例外のハンドリング:
プロセッサ内部でデータベースへの書き込み中に例外が発生した場合、それをそのまま上位にスローしてバッチ全体をクラッシュさせるべきではない。イベント引数 (`FileProcessedEventArgs`) に `ErrorMessage` を持たせている通り、「1ファイルの失敗はイベントで報告し、バッチ自体は次のファイルの処理へ継続する(フォールトトレラントな設計)」 が業務自動化ツールの鉄則である。
2. UIスレッドへのMarshalling(Windows Forms / WPFの場合):
バックグラウンドの `Task` からイベントが発火されるため、もしリスナー側でWindows FormsのProgressBarやTextBoxを直接操作すると `Cross-thread operation not valid` エラーが発生する。リスナー側(UI層)で `If Me.InvokeRequired Then …` を使うか、`IProgress(Of T)` パターンとの併用を検討すること。
まとめ
`EventHandler(Of T)` を制する者は、VB.NETのイベント駆動を制す。
「動けばいいや」のスパゲッティコードから脱却し、型安全で拡張性の高いアーキテクチャを取り入れることで、あなたの作る業務ツールやバッチシステムは、数年後も保守に怯えることのない「強靭な資産」へと生まれ変わる。
明日からのコードには、ぜひこのイミュータブルかつ型安全なアプローチを導入してほしい。
