数万件のマスターを瞬時に捌く:ComboBoxインクリメンタルサーチの「極限」最適化
業務アプリにおいて、ComboBoxに数万件の得意先マスターをバインドするのは「悪手」の代名詞だ。`DataSource`に数万件の`DataTable`をそのまま放り込み、`AutoCompleteMode`を有効にするなど、現代のメモリ管理においては自殺行為に等しい。文字を打つたびにUIスレッドがロックされ、ユーザーのストレスはピークに達する。
今日は、イベント駆動UIの限界を超え、数万件のマスターを「爆速」で絞り込むための、アーキテクトとしての回答を提示する。
—
1. なぜ標準のComboBoxではいけないのか?
標準の`ComboBox`は、アイテム数が数千を超えたあたりから、レンダリングと検索アルゴリズムが線形探索の罠に陥る。特に`.NET`の`AutoComplete`は、内部的に文字列比較を繰り返すため、キー入力のたびにガベージコレクション(GC)が頻発する。
我々が目指すべきは、「UIスレッドの解放」と「メモリの定数化」だ。
2. アーキテクチャの核心:Virtual Modeの擬似実装
ComboBoxには公式の`Virtual Mode`が存在しない。そのため、以下の手法を組み合わせる。
1. データソースの分離: `DataTable`をUIに持たせず、メモリ上のインデックスリスト(`List
2. 遅延評価(Lazy Evaluation): キー入力後、数百ミリ秒のデバウンス(Debounce)を挟む。
3. 表示用キャッシュの限定: UIに表示するのは、絞り込まれた結果の「先頭100件」のみとする。
実装コード:爆速絞り込みエンジンの骨子
Imports System.Threading
Public Class FastComboBoxManager
‘ 数万件のマスターデータ(実体)
Private ReadOnly _masterData As List(Of Customer)
‘ 絞り込み後の表示用リスト
Private _filteredList As New List(Of Customer)
‘ デバウンス用のタイマー
Private _debounceTimer As System.Windows.Forms.Timer
Public Sub New(masterData As List(Of Customer))
_masterData = masterData
InitializeTimer()
End Sub
Private Sub InitializeTimer()
_debounceTimer = New System.Windows.Forms.Timer()
_debounceTimer.Interval = 200 ‘ 200msの遅延で入力を確定させる
AddHandler _debounceTimer.Tick, AddressOf OnDebounceTick
End Sub
‘ テキスト変更時のイベントハンドラ
Public Sub HandleTextChanged(targetComboBox As ComboBox)
_debounceTimer.Stop()
_debounceTimer.Start()
End Sub
Private Sub OnDebounceTick(sender As Object, e As EventArgs)
_debounceTimer.Stop()
‘ ここでLINQを使って絞り込み。数万件でも数ミリ秒で完了する
Dim query = targetComboBox.Text
_filteredList = _masterData.Where(Function(c) c.Name.Contains(query)) _
.Take(100) _
.ToList()
‘ UI更新を最小限に抑える
UpdateComboBox(targetComboBox)
End Sub
End Class
—
3. Windows APIで「カクつき」を物理的に消す
ComboBoxの`BeginUpdate`と`EndUpdate`を呼び出すだけでは不十分な場合がある。OSレベルで再描画を完全に停止させ、リストの更新を強制的に同期させるために、`SendMessage`によるメッセージの封印が有効だ。
Private Const WM_SETREDRAW As Integer = &HB
Private Shared Function SendMessage(hWnd As IntPtr, msg As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr
End Function
‘ 更新前に呼び出す
SendMessage(comboBox.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero)
‘ … データの詰め替え処理 …
‘ 更新後に呼び出す
SendMessage(comboBox.Handle, WM_SETREDRAW, New IntPtr(1), IntPtr.Zero)
comboBox.Refresh()
この手法は、Windows Formsの描画サイクルに割り込みをかけるため、大量のアイテムを挿入する際でも画面のチラつきを皆無にできる。
—
4. メモリ管理とレガシーの呪縛
VB.NETのアプリケーションで数万件のオブジェクトを扱う際、最も恐ろしいのは「メモリリーク」ではなく「メモリの断片化」だ。
- 明示的な解放: `DataTable`や`List`を再生成する際は、必ず`Clear()`を呼び出し、`GC.Collect()`を安易に呼ぶのではなく、`IDisposable`を実装したラッパーを介してリソースを管理せよ。
- データ構造の選択: `string`の比較はコストが高い。もし可能であれば、IDやコードをバイナリ検索可能な配列(`Int32`のソート済みリスト)として保持し、名前の検索はサブセットで行うのが、伝説的なアーキテクトが辿り着く「正解」だ。
結論
ComboBoxのインクリメンタルサーチは、単なるUIパーツのプロパティ設定ではない。「データ検索エンジン」と「描画パイプライン」の最適化である。
フレームワークが提供する便利な機能に依存してはいけない。数万件のマスターを扱いたければ、泥臭いメモリ管理と、Windows APIの深淵を覗く覚悟が必要だ。この実装を導入すれば、どんなに巨大なマスターデータを持つシステムであっても、ユーザーは「爆速」という言葉以外の感想を抱くことはないだろう。
エンジニアよ、フレームワークの檻から抜け出し、CPUとメモリを支配せよ。
