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

スポンサーリンク

Windows Formsの呪縛を断つ:カスタム属性駆動型バリデーションフレームワークの極意

レガシーシステムの保全、あるいは泥臭い社内業務アプリケーションの迅速な構築において、Windows Forms(WinForms)がいまだに現役である現場は多い。VB.NETによるUI開発の現場では、ボタンのクリックイベントやテキストボックスの`Validating`イベントに、幾重もの`If`条件分岐を泥臭く書き連ねるアンチパターンが今なお蔓延している。

「名前が空ではないか」「半角英数か」「桁数はオーバーしていないか」――。
このような散在するバリデーションロジックは、仕様変更のたびに開発者を苦しめ、コードの腐敗を加速させる。

私は長年、数多のレガシーシステムと向き合ってきた。その中で導き出した結論は一つだ。「UIの検証ロジックは、コードではなくメタデータ(属性)に語らせよ」。

今回は、VB.NETのカスタム属性(Attribute)とリフレクション、そしてWinFormsのコントロール特性を極限まで理解し、アノテーションを付与するだけで自律的に動作する汎用バリデーションフレームワークの設計手法を授ける。

—

1. アーキテクチャの全体像

今回構築するフレームワークの要件は以下の通りだ。

1. 宣言的記述: コントロール、あるいはそれを内包するフォームに対し、属性(Attribute)を付与するだけで検証ルールが定義できること。
2. メモリとパフォーマンスの最適化: リフレクションのコストを最小限に抑え、不要なオブジェクト生成やメモリリークを防ぐこと。
3. 視覚的フィードバック: エラー時にはWinForms標準の`ErrorProvider`を自動制御し、直感的なUIフィードバックを行うこと。

これを実現するために、以下の3つのレイヤーを設計する。

  • 属性層 (`Attribute`): 検証ルールを定義するカスタム属性。
  • エンジン層 (`ValidationEngine`): リフレクションを用いてコントロールと属性を走査し、検証を実行する中枢。
  • UI統合層 (`Form`基底クラス / 拡張メソッド): イベント駆動でエンジンを呼び出すフック。

—

2. 実装:カスタム属性の定義

まずは、検証ルールの基本となる抽象属性と、具体的な検証属性(必須チェック、最大長チェック)を定義する。

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

‘ 必須入力チェック属性

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 As String = TryCast(value, String)
If str IsNot Nothing Then
Return Not String.IsNullOrWhiteSpace(str)
End If
Return True
End Function
End Class

‘ 最大文字数チェック属性

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 As String = TryCast(value, String)
If str IsNot Nothing Then
Return str.Length <= _maxLength End If Return True End Function End Class End Namespace ---

3. 中枢:リフレクションキャッシュを活用したバリデーションエンジン

リフレクション(`GetType().GetProperties()`)は強力だが、毎回のイベント発生時に実行するとパフォーマンスが著しく低下する。シニアエンジニアであれば、メタデータのキャッシュ機構を組み込み、CPUサイクルとメモリ割り当てを最適化すべきだ。

Imports System.Collections.Concurrent
‘ 変更を検知するための名前空間
Imports System.Reflection
Imports System.Windows.Forms

Namespace Framework.Validation

Public Class ValidationResult
Public Property PropertyName As String
Public Property ErrorMessage As String
Public Property TargetControl As Control
End Class

Public NotInheritable Class ValidationEngine
‘ リフレクション結果をキャッシュし、パフォーマンス劣化を防ぐ
Private Shared ReadOnly _propertyCache As New ConcurrentDictionary(Of Type, PropertyInfo())

Private Sub New()
‘ 静的クラスとしてのインスタンス化を禁止
End Sub

‘ フォーム上のコントロール群を一括検証
Public Shared Function ValidateForm(targetForm As Form, errorProvider As ErrorProvider) As Boolean
errorProvider.Clear()
Dim isValid As Boolean = True
Dim formType As Type = targetForm.GetType()

‘ キャッシュからプロパティ情報を取得(存在しない場合はリフレクション実行)
Dim properties = _propertyCache.GetOrAdd(formType, Function(t) t.GetProperties(BindingFlags.Public Or BindingFlags.Instance))

For Each prop As PropertyInfo In properties
‘ 属性が付与されているプロパティのみを対象とする
Dim attributes = prop.GetCustomAttributes(GetType(ValidationAttribute), True)
If attributes.Length = 0 Then Continue For

‘ プロパティにバインドされたコントロールを名前解決または直接取得する想定
‘ ここでは簡略化のため、コントロール名がプロパティ名と一致すると仮定
Dim control As Control = FindControlByName(targetForm, prop.Name)
If control Is Nothing Then Continue For

Dim propValue As Object = prop.GetValue(targetForm)

For Each attr As ValidationAttribute In attributes
If Not attr.IsValid(propValue) Then
errorProvider.SetError(control, attr.ErrorMessage)
isValid = False
Exit For ‘ 最初のエラーで次のプロパティへ
End If
Next
Next

Return isValid
End Function

‘ 効率的なコントロール検索ヘルパー
Private Shared Function FindControlByName(parent As Control, name As String) As Control
Dim matches = parent.Controls.Find(name, True)
If matches.Length > 0 Then
Return matches(0)
End If
Return Nothing
End Function
End Class

End Namespace

—

4. UI統合とメモリ最適化の極意

WinFormsにおいて、イベントハンドラの多重登録はメモリリーク(GC Rootの残留)の主原因となる。フォームのライフサイクル、特に破棄時(`Disposed`イベント)におけるリソースの明示的解放は、プロのエンジニアにとって譲れない条件だ。

以下は、このバリデーションエンジンを実際のフォームに組み込む実装例である。

Imports Framework.Validation

Public Class UserEntryForm
Inherits Form

‘ デザイナで配置されていると仮定するコントロール
‘ Friend WithEvents txtUserName As TextBox
‘ Friend WithEvents txtEmail As TextBox
‘ Friend WithEvents btnSubmit As Button
‘ Friend WithEvents errProvider As ErrorProvider

‘ — 属性をバインドするためのプロパティ定義 —


Public Property UserName As String
Get
Return txtUserName.Text
End Get
Set(value As String)
txtUserName.Text = value
End Set
End Property


Public Property Email As String
Get
Return txtEmail.Text
End Get
Set(value As String)
txtEmail.Text = value
End Set
End Property

Public Sub New()
‘ この呼び出しはデザイナーで必要です。
InitializeComponent()

‘ イベントハンドラの安全なアタッチ
AddHandler Me.btnSubmit.Click, AddressOf BtnSubmit_Click
AddHandler Me.Disposed, AddressOf UserEntryForm_Disposed
End Sub

Private Sub BtnSubmit_Click(sender As Object, e As EventArgs)
‘ エンジンによる一括検証の実行
If ValidationEngine.ValidateForm(Me, errProvider) Then
‘ 検証成功時の処理
MessageBox.Show(“検証成功!データを送信します。”, “通知”, MessageBoxButtons.OK, MessageBoxIcon.Information)
‘ TODO: ビジネスロジック層への引き渡し
Else
MessageBox.Show(“入力内容にエラーがあります。確認してください。”, “警告”, MessageBoxButtons.OK, MessageBoxIcon.Warning)
End If
End Sub

‘ 【極意】イベントのアンレジストとメモリの確実な解放
Private Sub UserEntryForm_Disposed(sender As Object, e As EventArgs)
‘ 明示的なイベントハンドラのデタッチ(メモリリーク防止)
RemoveHandler Me.btnSubmit.Click, AddressOf BtnSubmit_Click
RemoveHandler Me.Disposed, AddressOf UserEntryForm_Disposed

‘ ErrorProviderのリソース解放
If errProvider IsNot Nothing Then
errProvider.Dispose()
End If
End Sub

End Class

—

5. チーフアーキテクトからの提言

この設計を導入することで、以下の圧倒的なメリットがもたらされる。

1. ビジネスロジックとUIの完全な分離: 検証ルールがプロパティの属性として宣言されるため、フォームのコードビハインドが肥大化しない。
2. 保守性の劇的な向上: 新たな検証ルール(例:正規表現によるパターンマッチングなど)を追加したい場合、`ValidationAttribute`を継承したクラスを1つ作成し、プロパティに付与するだけで完了する。既存の検証ロジックに手を加える必要(オープン・クローズドの原則)がない。
3. レガシー環境への適合: .NET Framework 4.x世代の古い環境であっても、`ConcurrentDictionary`やリフレクションキャッシュを活用すれば、モダンなアーキテクチャの恩恵を十分に受けることができる。

コードの美しさは、システムの寿命を延ばす。
泥臭いWinForms開発であっても、妥協なき設計思想を貫くことこそが、真のプロフェッショナルエンジニアの仕事である。

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