迷走するイベントハンドラを制御せよ:CheckedListBoxの極致とUIアーキテクチャの最適解
現場で遭遇する「CheckedListBoxの項目が100個を超えた途端、画面が重くなる」「全選択ボタンを押すたびにイベントが暴走する」という現象。これらは単なる実装ミスではなく、Windows Formsのイベント駆動モデルに対する理解不足が招く必然の結果だ。
今日は、レガシーを保守し、かつ次世代の保守性を担保するための「CheckedListBoxの制御術」を、アーキテクトの視点から紐解く。
—
1. イベントの「洪水」を止める:BeginUpdate/EndUpdateの鉄則
CheckedListBoxに限らず、Windows Formsのリスト系コントロールは、項目が変更されるたびに再描画のメッセージを投げる。全選択のためにループで `SetItemChecked` を呼べば、その回数分だけレイアウト計算が走り、UIスレッドは息絶える。
これを防ぐ唯一の解は、再描画の抑制だ。
.net
”’
”’
Public Sub ToggleAllChecks(targetList As CheckedListBox, checkState As Boolean)
‘ 再描画を一時停止し、レイアウト計算のオーバーヘッドを遮断する
targetList.BeginUpdate()
Try
For i As Integer = 0 To targetList.Items.Count – 1
targetList.SetItemChecked(i, checkState)
Next
Finally
‘ 例外発生時でも必ず描画を再開させる。ここを怠るとUIがフリーズする
targetList.EndUpdate()
End Try
End Sub
—
2. イベントループの遮断:フラグ管理による「無限再帰」の回避
「チェック状態が変わったら関連ボタンを制御する」という要件を実装する際、`ItemCheck` イベント内で `SetItemChecked` を呼ぶような設計をしていないか? それは、イベントがイベントを呼ぶ無限ループの引き金だ。
シニアエンジニアは、状態の「更新中フラグ」を導入し、コードによる変更か、ユーザー操作による変更かを厳密に区別する。
.net
Private _isInternalUpdating As Boolean = False
Private Sub CheckedListBox1_ItemCheck(sender As Object, e As ItemCheckEventArgs) Handles CheckedListBox1.ItemCheck
‘ 内部処理による変更なら、イベントハンドラを即時終了させる
If _isInternalUpdating Then Return
‘ ボタンの有効化制御
UpdateDependentControls()
End Sub
Private Sub UpdateDependentControls()
_isInternalUpdating = True
Try
‘ ここでボタンのEnabled制御や、集計処理を行う
btnProcess.Enabled = (CheckedListBox1.CheckedItems.Count > 0)
Finally
_isInternalUpdating = False
End Try
End Sub
—
3. Windows APIによるメモリとパフォーマンスの究極最適化
VB.NETは高レベル言語だが、時として `SendMessage` APIを叩く方が遥かに速い場合がある。特に大量のアイテムを保持する `CheckedListBox` において、`Items.Clear()` や `AddRange()` はメモリのアロケーションを伴う重い操作だ。
もしパフォーマンスがボトルネックとなるなら、Windowsのネイティブメッセージを直接操作する。
.net
‘ P/Invokeの定義
Private Shared Function SendMessage(hWnd As IntPtr, Msg As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr
End Function
Private Const WM_SETREDRAW As Integer = &HB
‘ 描画の完全停止(BeginUpdateよりも強力)
Public Sub SuspendPainting(target As Control)
SendMessage(target.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero)
End Sub
‘ 描画の再開
Public Sub ResumePainting(target As Control)
SendMessage(target.Handle, WM_SETREDRAW, New IntPtr(1), IntPtr.Zero)
target.Refresh()
End Sub
—
4. チーフアーキテクトからの助言:UIとロジックの分離
最後に、技術的な小技以上に重要なことを伝えておく。
CheckedListBoxは、単なる「表示部品」ではない。その背後には必ず「マスタデータ」や「権限定義」というビジネスロジックが存在する。
- ViewModelへの昇華: UIコントロールに直接データを保持させず、`BindingList(Of T)` 等のコレクションをデータソースとしてバインドせよ。
- イベントの集約: フォーム上のいたるところにイベントハンドラを散らばらせるな。コマンドパターンやデリゲートを活用し、状態変更のトリガーを一箇所に集約する。
結論:保守性は「抑制」から生まれる
多くのエンジニアが「いかに動かすか」に腐心する中、熟練者は「いかに無駄を抑制するか」を考える。`BeginUpdate` は再描画の抑制であり、`_isInternalUpdating` はイベントの抑制だ。
UIの動作が軽快であること。それは、システムが設計者の意図通りに制御されている証である。このコードをベースに、貴方のプロジェクトに「静寂と高速」をもたらしてほしい。
健闘を祈る。
