【実務・中級編】VB.NETで実現するカスタム属性(Attribute)を用いた入力値バリデーションフレームワークの自社開発 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

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側で `` などと定義すれば、当然データベース層で例外(SqlException)が発生します。モデルの属性は、DBの物理設計(文字数やNOT NULL制約)と厳密に一致させるか、ORM(Entity FrameworkやDapper等)を使用する場合は、データ注釈(Data Annotations)を流用・統合する設計を検討してください。

2. リフレクションのパフォーマンスへの配慮
「毎回のリフレクション呼び出しで動作が重くなるのでは?」という懸念を持つエンジニアは優秀です。通常、フォームのバリデーション頻度であれば体感できる遅延はありませんが、極限のパフォーマンスを求める場合は、`Type.GetProperties()` の結果を `Dictionary(Of Type, PropertyInfo())` などにキャッシュ(メモ化)する機構を ModelValidator に組み込むことで、オーバーヘッドを完全に排除できます。

—

4. 結び:保守性の高いコードは「退屈」であるべきだ

優れたアーキテクチャによって構築されたアプリケーションは、ビジネスロジックの変更に対して「退屈なほど容易に」追従できます。新しい入力項目にバリデーションを追加したくなった時、プログラマはイベントハンドラーの迷宮に迷い込む必要はありません。ただ、プロパティに新しい属性(例: `` や ``)を一行追加するだけです。

VB.NETという枯れつつも極めて堅牢な言語の特性を理解し、手作業によるスパゲッティコードから脱却しましょう。あなたのプロジェクトのコードベースが、洗練されたモジュール群に生まれ変わることを期待しています。

タイトルとURLをコピーしました