Windows FormsのUI検証を劇的に洗練させる:カスタム属性駆動型バリデーションフレームワークの構築
こんにちは。開発プロジェクトのアーキテクトとして現場を見渡していると、いまだにWindows Formsの入力値チェックにおいて、このような「負の遺産」を目にすることがあります。
.net
‘ 【アンチパターン】各コントロールのValidatingイベントにベタ書きされた地獄の条件分岐
Private Sub txtUserCode_Validating(sender As Object, e As CancelEventArgs) Handles txtUserCode.Validating
If String.IsNullOrWhiteSpace(txtUserCode.Text) Then
ErrorProvider1.SetError(txtUserCode, “コードを入力してください。”)
e.Cancel = True
ElseIf txtUserCode.Text.Length > 10 Then
ErrorProvider1.SetError(txtUserCode, “10文字以内で入力してください。”)
e.Cancel = True
Else
ErrorProvider1.SetError(txtUserCode, “”)
End If
End Sub
画面数が数十、数百とある業務システムにおいて、この手続き的なバリデーションをコントロールごとに書くのは、保守性・拡張性の観点から完全な悪手です。仕様変更のたびにテスト工数が膨れ上がり、ヒューマンエラーによるバグの温床となります。
今回は、VB.NETのカスタム属性(Attribute)とリフレクション(Reflection)の機微を極め、エンティティクラスにアノテーションを付与するだけで、Windows FormsのUIと自動連動する堅牢かつ優雅なフレームワークの設計手法を授けましょう。
—
1. なぜカスタム属性駆動なのか?
業務アプリケーションの本質は「データの永続化と正確な状態管理」です。UI(Windows Forms)の都合と、ドメインモデル(データ構造)の検証ルールを密結合させてはなりません。
私たちが目指すべきアーキテクチャは以下の通りです。
1. 検証ルールはデータモデル(クラス)に定義する(UI非依存)
2. コントロールへのバインドとエラー表示は共通エンジンに委譲する(DRY原則の徹底)
3. 新しい検証ルールは属性の追加だけで拡張できる(OCP: 開放閉鎖原則)
これをVB.NETの強力な型システムを使って実装します。
—
2. 実装:プロダクションコードの全体像
プロジェクトに以下の3つのレイヤー(属性、検証エンジン、フォーム統合)を用意します。コピペでそのまま実務に投入可能です。
ステップ1:ベースとなるカスタム属性の定義
まずはすべての検証属性の根幹となる抽象クラスと、代表的な「必須チェック」「最大長チェック」を実装します。
.net
Imports System
Namespace Framework.Validation
‘ すべてのカスタム検証属性の基底クラス
Public MustInherit Class ValidationAttribute
Inherits Attribute
‘ エラーメッセージ
Public Property ErrorMessage As String
Protected Sub New(errorMessage As String)
Me.ErrorMessage = errorMessage
End Sub
‘ 検証ロジック(派生クラスで実装)
Public MustOverride Function IsValid(value As Object) As Boolean
End Class
‘ 1. 必須入力検証属性
Public Class RequiredAttribute
Inherits ValidationAttribute
Public Sub New(Optional errorMessage As String := “この項目は必須です。”)
MyBase.New(errorMessage)
End Sub
Public Overrides Function IsValid(value As Object) As Boolean
If value Is Nothing Then Return False
Dim str = TryCast(value, String)
If str IsNot Nothing Then
Return Not String.IsNullOrWhiteSpace(str)
End If
Return True
End Function
End Class
‘ 2. 文字列長(最大値)検証属性
Public Class MaxLengthAttribute
Inherits ValidationAttribute
Private ReadOnly _maxLength As Integer
Public Sub New(maxLength As Integer, Optional errorMessage As String := “”)
MyBase.New(If(String.IsNullOrEmpty(errorMessage), $”{maxLength}文字以内で入力してください。”, errorMessage))
_maxLength = maxLength
End Sub
Public Overrides Function IsValid(value As Object) As Boolean
If value Is Nothing Then Return True ‘ 必須チェックは別属性に任せる
Dim str = TryCast(value, String)
If str IsNot Nothing Then
Return str.Length <= _maxLength
End If
Return True
End Function
End Class
End Namespace
ステップ2:リフレクションを用いた汎用バリデーションエンジン
次に、定義された属性を読み取り、オブジェクト全体を検証するエンジンを作成します。
.net
Imports System.Collections.Generic
‘ 修正: System.Reflectionを明示しつつ、VBのAmbiguous(曖昧)な参照を回避
Imports System.Reflection
Namespace Framework.Validation
Public Class ModelValidator
‘ オブジェクトの属性を解析し、エラーメッセージのリストを返す
Public Shared Function Validate(model As Object) As Dictionary(Of String, List(Of String))
Dim errorResults As New Dictionary(Of String, List(Of String))
Dim modelType As Type = model.GetType()
‘ プロパティを走査
For Each prop As PropertyInfo In modelType.GetProperties()
‘ 適用されている ValidationAttribute を取得
Dim attributes = prop.GetCustomAttributes(GetType(ValidationAttribute), True)
If attributes.Length > 0 Then
Dim propValue As Object = prop.GetValue(model)
Dim propertyErrors As New List(Of String)()
For Each attr As ValidationAttribute In attributes
If Not attr.IsValid(propValue) Then
propertyErrors.Add(attr.ErrorMessage)
End If
Next
If propertyErrors.Count > 0 Then
errorResults.Add(prop.Name, propertyErrors)
End If
End If
Next
Return errorResults
End Function
End Class
End Namespace
ステップ3:Windows Formsとの統合(UIバインディング)
ここが肝心要です。ビュー(Windows Forms)側でエンティティをバインドし、`ErrorProvider` を使って一括、あるいはコントロール単位でスマートにエラーを可視化します。
.net
Imports System.Windows.Forms
Imports Framework.Validation
‘ — 使用例としてのエンティティクラス —
Public Class UserEditModel
Public Property UserCode As String
Public Property UserName As String
End Class
‘ — フォーム側の実装 —
Public Class FormUserEdit
Inherits Form
Private txtUserCode As TextBox
Private txtUserName As TextBox
Private btnSave As Button
Private ErrorProvider1 As ErrorProvider
‘ コンポーネントの初期化(簡略化のためコード生成風に記述)
Private Sub InitializeComponentCustom()
Me.txtUserCode = New TextBox() With {.Top = 20, .Left = 120, .Width = 200}
Me.txtUserName = New TextBox() With {.Top = 60, .Left = 120, .Width = 200}
Me.btnSave = New Button() With {.Top = 100, .Left = 120, .Text = “保存”}
Me.ErrorProvider1 = New ErrorProvider()
Me.Controls.AddRange(New Control() {txtUserCode, txtUserName, btnSave})
AddHandler btnSave.Click, AddressOf BtnSave_Click
End Sub
Private Sub BtnSave_Click(sender As Object, e As EventArgs)
‘ 1. 画面の入力値をドメインモデルに逆バインド(DTOへのマッピング)
Dim model As New UserEditModelWithBinding() With {
.UserCode = txtUserCode.Text,
.UserName = txtUserName.Text
}
‘ 2. エンジンによる一括検証
Dim errors = ModelValidator.Validate(model)
‘ 3. エラープロバイダのクリア
ErrorProvider1.Clear()
If errors.Count > 0 Then
‘ 4. エラーのハンドリング(簡易的に先頭のエラーを表示、マッピングも可能)
If errors.ContainsKey(NameOf(UserEditModel.UserCode)) Then
ErrorProvider1.SetError(txtUserCode, errors(NameOf(UserEditModel.UserCode))(0))
End If
If errors.ContainsKey(NameOf(UserEditModel.UserName)) Then
ErrorProvider1.SetError(txtUserName, errors(NameOf(UserEditModel.UserName))(0))
End If
MessageBox.Show(“入力内容に誤りがあります。確認してください。”, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Warning)
Return
End If
‘ 5. 検証クリア時の処理(DB登録など)
MessageBox.Show(“保存処理を実行しました。”, “成功”, MessageBoxButtons.OK, MessageBoxInfo.Information)
End Sub
End Class
—
3. 実務(ファイル・データベース連携)における注意点
このフレームワークを実際の業務システムに組み込む際、チーフアーキテクトとして以下の2点を強く進言します。
1. DBのスキーマ定義(制約)と属性の同期
データベースの `VARCHAR(10)` フィールドに対し、VB.NET側で `
2. リフレクションのパフォーマンスへの配慮
「毎回のリフレクション呼び出しで動作が重くなるのでは?」という懸念を持つエンジニアは優秀です。通常、フォームのバリデーション頻度であれば体感できる遅延はありませんが、極限のパフォーマンスを求める場合は、`Type.GetProperties()` の結果を `Dictionary(Of Type, PropertyInfo())` などにキャッシュ(メモ化)する機構を ModelValidator に組み込むことで、オーバーヘッドを完全に排除できます。
—
4. 結び:保守性の高いコードは「退屈」であるべきだ
優れたアーキテクチャによって構築されたアプリケーションは、ビジネスロジックの変更に対して「退屈なほど容易に」追従できます。新しい入力項目にバリデーションを追加したくなった時、プログラマはイベントハンドラーの迷宮に迷い込む必要はありません。ただ、プロパティに新しい属性(例: `
VB.NETという枯れつつも極めて堅牢な言語の特性を理解し、手作業によるスパゲッティコードから脱却しましょう。あなたのプロジェクトのコードベースが、洗練されたモジュール群に生まれ変わることを期待しています。
