【中級〜上級】ActiveControlを掌握せよ:フォーム入力バリデーションの極限一元管理
レガシーなAccessシステムを保守・拡張する現場において、最も不毛なコードの重複は何か。それは、フォーム上の数十個に及ぶコントロールの「1つひとつに個別記述された入力バリデーション」である。
`Txt_Name_AfterUpdate`、`Txt_Age_AfterUpdate`、`Cbo_Category_BeforeUpdate`……。
コントロールが増えるたびにイベントプロシージャが乱立し、仕様変更のたびに同じようなNullチェックや桁数制限のコードを散らかす。このアンチパターンは、保守性をドブに捨てるようなものだ。
真にスケーラブルなAccessアーキテクチャを構築するためには、`Screen.ActiveControl`(あるいはフォーム自身の`ActiveControl`プロパティ)の本質を理解し、イベントを「ルーティング」する発想が必要となる。
今回は、Accessオブジェクトモデルの深層に踏み込み、あらゆるコントロールの入力検証をたった1つの汎用関数に集約する極限の設計手法を解説する。
—
1. Accessオブジェクトモデルの急所:ActiveControlの正体
多くの開発者が誤解しているが、`Screen.ActiveControl`は魔法の杖ではない。これは「現在フォーカスを持っているオブジェクトの参照」を返すvolatile(揮発性)なプロパティであり、イベントの発生タイミングによっては意図しないオブジェクトを指していることがある。
特に、`BeforeUpdate`イベント(コントロールの更新前処理)において、このプロパティと親フォームの密な連携を理解していないと、バリデーションのすり抜けや、最悪の場合はランタイムエラーを引き起こす。
ライフサイクルとイベントの罠
コントロールのデータ検証は、必ずデータベースへ書き込まれる手前(BeforeUpdate)で行わなければならない。この瞬間、VBAの実行コンテキストにおいて「どのコントロールが値の検証を求めているか」を特定する最も確実な手段が、イベントを発生させたフォームの`ActiveControl`、あるいは`Screen.ActiveControl`の捕捉である。
しかし、これを愚直に各コントロールのイベントから呼び出すのではなく、「タグ(Tag)プロパティによるメタデータ駆動型バリデーション」と組み合わせることで、真の汎用化が達成される。
—
2. 実装:タグ駆動型・汎用バリデーションエンジンの構築
ここに示すのは、標準モジュールに配置し、あらゆるフォームから呼び出し可能な汎用バリデーション関数である。
コントロールの`Tag`プロパティに検証ルール(例: `REQ;LEN=10;TYPE=NUM` など)を記述させ、それを`ActiveControl`経由で動的に解析・検証する。
標準モジュール(`modValidator`)
Option Explicit
Option Compare Database
‘ ==============================================================================
‘ 汎用入力バリデーションエンジン
‘ 概要: フォーム上の ActiveControl の Tag プロパティを解析し、動的に検証を行う
‘ ==============================================================================
Public Function ValidateActiveControl(ByRef frm As Form) As Boolean
Dim ctl As Control
Dim tagRules As String
Dim ruleItem As Variant
Dim varValue As Variant
Dim isValid As Boolean
On Error GoTo ErrorHandler
‘ 1. 現在アクティブなコントロールの安全な取得
Set ctl = frm.ActiveControl
‘ コントロールが値を持つ型かチェック(ラベルやコマンドボタンは除外)
If Not HasValueProperty(ctl) Then
ValidateActiveControl = True
Exit Function
End If
tagRules = Nz(ctl.Tag, “”)
‘ タグが未設定の場合は検証スキップ
If Len(tagRules) = 0 Then
ValidateActiveControl = True
Exit Function
End If
varValue = ctl.Value
isValid = True
‘ 2. タグに記述されたルールをセミコロン(;)で分解して評価
Dim rules As Variant
rules = Split(tagRules, “;”)
For Each ruleItem In rules
Dim rulePair As Variant
rulePair = Split(ruleItem, “=”)
Select Case UCase(Trim(rulePair(0)))
‘ 【必須チェック】 (REQ)
Case “REQ”
If IsNull(varValue) Or Trim(CStr(varValue)) = “” Then
MsgBox “【入力エラー】” & GetControlLabel(frm, ctl) & ” は必須入力です。”, vbCritical, “検証エラー”
isValid = False
Exit For
End If
‘ 【最大文字数チェック】 (LEN=10)
Case “LEN”
If Not IsNull(varValue) Then
If Len(CStr(varValue)) > Val(rulePair(1)) Then
MsgBox “【入力エラー】” & GetControlLabel(frm, ctl) & ” は ” & rulePair(1) & ” 文字以内で入力してください。”, vbCritical, “検証エラー”
isValid = False
Exit For
End If
End If
‘ 【数値型チェック】 (TYPE=NUM)
Case “TYPE”
If UCase(Trim(rulePair(1))) = “NUM” Then
If Not IsNull(varValue) And Not IsNumeric(varValue) Then
MsgBox “【入力エラー】” & GetControlLabel(frm, ctl) & ” には数値を入力してください。”, vbCritical, “検証エラー”
isValid = False
Exit For
End If
End If
End Select
Next ruleItem
ValidateActiveControl = isValid
Exit Function
ErrorHandler:
‘ 予期せぬエラーの捕捉とログ出力(実運用ではErr.Descriptionをログへ)
MsgBox “バリデーション実行中に予期せぬエラーが発生しました: ” & Err.Description, vbCritical
ValidateActiveControl = False
End Function
‘ — ヘルパー関数: コントロールが値を持つか判定 —
Private Function HasValueProperty(ctl As Control) As Boolean
Select Case ctl.ControlType
Case acTextBox, acComboBox, acListBox, acCheckBox, acOptionButton, acToggleButton
HasValueProperty = True
Case Else
HasValueProperty = False
End Select
End Function
‘ — ヘルパー関数: 関連付けられたラベルのテキストを取得 —
Private Function GetControlLabel(frm As Form, ctl As Control) As String
Dim lbl As Label
On Error Resume Next
Set lbl = ctl.Controls(0)
If Err.Number = 0 And Not lbl Is Nothing Then
GetControlLabel = Replace(lbl.Caption, “(&)”, “”)
GetControlLabel = Replace(GetControlLabel, “&”, “”)
Else
GetControlLabel = ctl.Name
End If
On Error GoTo 0
End Function
—
3. フォーム側での実装:驚異的なコードの軽量化
このアーキテクチャを導入した場合、各フォームのコード量は劇的に削減される。個別のコントロールごとにイベントを書く必要はなく、フォーム全体の`BeforeUpdate`、あるいは共通のマクロ/イベントルーティングを利用する。
しかし、より確実な制御を行うためには、各コントロールの`BeforeUpdate`イベントからこの関数を呼び出す。ここでのポイントは、「コードのコピー&ペーストを排除する」ことである。
‘ ==============================================================================
‘ フォームモジュール(例: 顧客管理フォーム)
‘ ==============================================================================
Option Explicit
Private Sub Form_Error(DataErr As Integer, Response As Integer)
‘ Access標準のエラーメッセージを制御する場合のフック
‘ 必要に応じてカスタムエラーハンドリングを記述
End Sub
‘ ——————————————————————————
‘ 各コントロールの BeforeUpdate から集約して呼び出す例
‘ ※もしすべてのコントロールで共通処理したい場合は、クラスモジュールによる
‘ イベントトラッピング(WithEvents)を適用するが、中級レベルとしては
‘ タグと共通関数呼び出しの組み合わせが最もメンテナブルである。
‘ ——————————————————————————
Private Sub Txt_CustomerName_BeforeUpdate(Cancel As Integer)
If Not ValidateActiveControl(Me) Then Cancel = True
End Sub
Private Sub Txt_Age_BeforeUpdate(Cancel As Integer)
If Not ValidateActiveControl(Me) Then Cancel = True
End Sub
Private Sub Cbo_Category_BeforeUpdate(Cancel As Integer)
If Not ValidateActiveControl(Me) Then Cancel = True
End Sub
設計の極意:なぜクラスモジュールではなく関数呼び出しなのか?
「すべてのコントロールのイベントを`WithEvents`で動的フックすべきだ」という過剰に難解なアーキテクチャを好むエンジニアもいる。しかし、Accessの限られたリソースと、現場の保守担当者のスキルセットを考慮した場合、「コントロールのTagにルールを書き、各イベントから1行の関数を叩く」という構成が、パフォーマンス(メモリ消費・インスタンス生成のオーバーヘッド)と可読性のバランスにおいて最も優れている。
—
4. シニアエンジニアが押さえるべきパフォーマンスとメモリ最適化
Access VBA環境、特にJet/ACEデータベースエンジンとCOMコンポーネントが絡む環境では、メモリ管理の不手際がパフォーマンス低下や「メモリ不足」エラーの温床となる。
1. オブジェクト変数の明示的な解放
上記のコード内でも、`Set ctl = Nothing` や `Set lbl = Nothing` といったオブジェクト参照の解放を徹底すべきである(VBAのスコープ抜けても即座にCOM参照が解放されないケースがあるため、特にループ内やエラー処理では重要)。
2. Variant型の乱用抑制
`Split` 関数やプロパティの評価において`Variant`型が発生することは避けられないが、不要な型変換を最小限に抑え、Jetエンジンへのクエリ負荷を軽減する。
3. レガシー環境(32bit/64bit Officeの混在)への配慮
ActiveXコントロールを使用せず、ネイティブのAccessコントロールとフォームのプロパティ(`ActiveControl`)のみで完結させることで、API依存を排除し、環境差異によるクラッシュリスクをゼロに抑え込んでいる。
—
総括
プログラミングにおける「美しさ」とは、コードの短さではない。「変更に対する強靭さと、構造の透明性」のことだ。
今回紹介した `ActiveControl` を活用したバリデーションの一元管理は、フォーム設計のモジュール化を推し進め、数千行に及ぶスパゲッティコードと化したレガシーAccessシステムを救うための強力な武器となる。
個別のイベントに縛られる古い開発手法から脱却し、メタデータ駆動型のスマートなアーキテクチャをあなたの現場へ導入してほしい。
