ComboBoxとAutoCompletionの極限最適化:数千件のマスターデータをインクリメンタルサーチで爆速制御するUI設計
業務システムのWindows Forms(WinForms)開発において、避けて通れないのが「マスタデータの選択UI」だ。
社員マスタ、顧客マスタ、商品マスタ——これらが数千件、あるいは数万件規模に膨れ上がったとき、素朴な `ComboBox` の実装は完全に破綻する。
デフォルトの `AutoCompleteMode.SuggestAppend` をそのまま数万件のデータに適用した開発者は、こうして絶望する。
- 「文字を入力するたびにUIが固まる(フリーズする)」
- 「候補のポップアップが出るまでに致命的なタイムラグがある」
- 「あいまい検索ができず、先頭一致しかヒットしないため現場からクレームが来る」
今回は、VB.NETのWindows Forms環境において、大量のマスターデータを背負いながらも、ミリ秒単位のレスポンスでインクリメンタルサーチを実現する極限のUI設計を伝授する。
—
なぜ標準のComboBox機能では不十分なのか?
WinFormsの標準機能である `AutoCompleteSource.ListItems` は、内部で全アイテムを線形探索している。データ件数が数千件程度であればごまかせても、数万件を超えた瞬間、O(N)の計算量がUIスレッドを直撃する。
さらに致命的なのは、「前方一致」しか検索できない点だ。
例えば「株式会社東京システム」という顧客を探すとき、現場のユーザーは「東京」や「システム」といった中間ワードで検索したい。しかし標準機能では「株」から打ち始めなければヒットしない。これでは業務効率化ツールどころか、ストレス製造機だ。
我々が目指すべきアーキテクチャは以下の通りだ:
1. 非同期によるマスターデータのロードとキャッシュ(UIスレッドをブロックしない)
2. LINQと正規表現(または高速フィルタリング)による柔軟なあいまい検索
3. 描画遅延(Virtualization的アプローチ)とカスタムドロップダウン制御
—
設計方針:カプセル化された高速検索コンボボックス
今回は、汎用的に使い回せるように `UserControl` または `ComboBox` を継承したカスタムコントロールとして実装する。
責務を明確に分離し、呼び出し側は「データソースを渡すだけ」で、内部で爆速のインクリメンタルサーチが完結する設計にする。
データ構造の定義
まず、IDと表示名を保持する軽量なエンティティクラスを用意する。
.net
‘ マスターデータの基本単位
Public Class MasterItem
Public Property Id As Integer
Public Property DisplayName As String
‘ ComboBoxにバインドした際に表示される文字列
Public Overrides Function ToString() As String
Return DisplayName
End Function
End Class
—
プロダクションコード:爆速インクリメンタルサーチ・コンボボックス
以下のコードは、数万件のデータであっても軽快に動作し、部分一致(あいまい検索)に対応したカスタムComboBoxの実装だ。
UIスレッドの過負荷を防ぐため、テキスト変更イベントのタイマー制御(デバウンス処理)も組み込んでいる。
.net
Imports System
Imports System.Collections.Generic
ComponentModel
Imports System.Linq
Imports System.Windows.Forms
Public Class HighPerformanceComboBox
Inherits ComboBox
‘ マスターデータの全件保持
private _masterData As List(Of MasterItem) = New List(Of MasterItem)()
‘ 検索処理のデバウンス用タイマー(連続入力を間引く)
private _searchTimer As Timer
‘ 内部フィルタリング中フラグ(無限ループ防止)
private _isFiltering As Boolean = False
Public Sub New()
MyBase.New()
‘ 標準のオートコンプリートは無効化し、自前で完全制御する
Me.AutoCompleteMode = AutoCompleteMode.None
Me.DropDownStyle = ComboBoxStyle.DropDown
‘ デバウンスタイマーの設定(例:入力停止後 150ms後に検索実行)
_searchTimer = New Timer()
_searchTimer.Interval = 150
AddHandler _searchTimer.Tick, AddressOf OnSearchTimerTick
End Sub
”’
”’
Public Sub SetMasterData(data As IEnumerable(Of MasterItem))
_masterData = data.ToList()
‘ 初期状態では全件を表示用ソースへ
Me.DataSource = _masterData.ToList()
End Sub
”’
”’
Protected Overrides Sub OnTextChanged(e As EventArgs)
MyBase.OnTextChanged(e)
If _isFiltering Then Return
‘ ユーザーが直接入力している場合のみタイマー発動
_searchTimer.Stop()
_searchTimer.Start()
End Sub
”’
”’
private Sub OnSearchTimerTick(sender As Object, e As EventArgs)
_searchTimer.Stop()
Dim keyword As String = Me.Text
‘ 入力が空の場合は全件に戻す
If String.IsNullOrWhiteSpace(keyword) Then
Dim currentPos = Me.SelectionStart
_isFiltering = True
Me.DataSource = _masterData.ToList()
Me.IntegralHeight = True
_isFiltering = False
Me.SelectionStart = currentPos
Return
End If
‘ ★極限最適化:LINQを用いた高速な部分一致検索(OrdinalIgnoreCaseでロケール依存のコストを削減)
Dim filtered = _masterData.Where(Function(x) x.DisplayName.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0).ToList()
‘ UIスレッドへの反映とキャレット位置の維持
Dim selectionStart = Me.SelectionStart
Dim selectionLength = Me.SelectionLength
_isFiltering = True
Me.DataSource = filtered
‘ ドロップダウンを自動展開
If filtered.Count > 0 AndAlso Not Me.DroppedDown Then
Me.DroppedDown = True
End If
_isFiltering = False
Me.Text = keyword
Me.SelectionStart = selectionStart
Me.SelectionLength = selectionLength
End Sub
Protected Overrides Sub Dispose(disposing As Boolean)
If disposing Then
If _searchTimer IsNot Nothing Then
_searchTimer.Stop()
_searchTimer.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
End Class
—
アーキテクチャの解説と現場の知見
1. なぜ「デバウンス(Debounce)」が必要なのか?
ユーザーが「T-O-K-Y-O」と素早くキーボードを叩いたとき、文字が1入力されるたびにフィルタリング走査が走ると、UIスレッドはイベントの嵐で窒息する。
タイマーを挟み、「ユーザーがタイピングを止めてから150ミリ秒後に検索を実行する」ことで、無駄なCPUサイクルを劇的に削減し、滑らかな入力感を実現している。
2. `String.IndexOf` による高速検索
正規表現(`Regex`)は柔軟だが、単純な部分一致においてはオーバーヘッドが大きすぎる。
.NETの `String.IndexOf(value, StringComparison.OrdinalIgnoreCase)` は内部最適化されており、数万件のリストであっても数十ミリ秒以下でフィルタリングを完了する。パフォーマンスとメンテナンス性のベストバランスだ。
3. キャレット(カーソル)位置の死守
AutoCompleteを自前実装する際最大の難所が、「文字を入力している途中でフォーカスやテキストが書き換わり、カーソルが勝手に先頭に戻ったり全選択状態になる現象」である。
上記のコードでは `_isFiltering` フラグと `SelectionStart` の退避・復元を徹底することで、ユーザーがストレスなくタイピングを続けられる極上の操作性を担保している。
—
データベースやファイル連携における実務上の注意点
1. マスターデータの取得は必ず非同期(Async/Await)で行うこと
数万件のレコードをSQL ServerやCSVファイルから読み込む際、同期処理(`DbConnection.Open()` など)を書くプログラマーがいるが、これは厳禁だ。アプリ起動時や画面オープン時にUIが固まる原因になる。必ず `Task.Run` や非同期IOを用いてバックグラウンドでロードし、完了後に `SetMasterData` をスレッドセーフに呼び出すこと。
2. メモリフットプリントの意識
数万件程度のオブジェクトであれば .NET のメモリ管理においては微小なものだが、画像パスや不要な巨大XMLなどをマスターデータに含めないこと。純粋な `Id` と `DisplayName` のペアに絞ることで、GC(ガベージコレクション)の負担を最小限に抑えられる。
—
総括
「ComboBoxごとの検索にこれだけのコードが必要か?」と感じただろうか。
しかし、現場のユーザーにとって、日々の業務で何百回、何千回と繰り返す「データ選択」のストレス軽減は、システム全体の評価を決定づける極めて重要な要素だ。
安易な標準機能の貼り付けで妥協せず、ライフサイクルとパフォーマンスのボトルネックをロジカルに潰しこむこと。それこそが、プロフェッショナルな業務アプリケーション開発者のあるべき姿である。
