【テクニカル・上級編】【中級者・実務】VB.NETとWPFにおける入力値検証(Validation)とエラー情報の視覚的フィードバック実装 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

整合性の防壁:WPFにおけるデータ検証(Validation)のアーキテクチャ設計

業務アプリケーションにおける「入力」とは、単なるデータの受け渡しではない。それは、ユーザーという「不確実な外部因子」が、我々が構築した「厳格なドメインモデル」へと干渉してくる、一種の侵入行為である。

レガシーなVBAシステムやWinFormsの時代、我々は「入力チェック」という名のアドホックなif文の羅列によって、辛うじてデータの整合性を保ってきた。しかし、WPF(Windows Presentation Foundation)というモダンなフレームワークに足を踏み入れたのであれば、その思考を根底から覆さねばならない。

本稿では、単なる「赤枠の出し方」を解説するのではない。オブジェクトのライフサイクル、UIスレッドの負荷、そしてViewModelとViewの疎結合を維持したまま、いかにして堅牢なバリデーション・メカニズムを構築するか、その真髄を記述する。

1. バリデーションの戦略的選択:なぜ `IDataErrorInfo` なのか

WPFにおける入力検証には、主に `ValidationRule` と `IDataErrorInfo`(および非同期対応の `INotifyDataErrorInfo`)の2つのアプローチが存在する。

  • ValidationRule: XAMLレベルでの検証。主に「型」や「書式」といった、ドメイン知識を必要としない低レイヤーの検証に向いている。
  • IDataErrorInfo: ViewModel(ビジネスロジック層)での検証。プロパティの相関関係や、業務ルールに基づく高度な検証に不可欠である。

シニアエンジニアが設計すべきは、後者である。なぜなら、業務ルールはUI(View)ではなく、常にビジネスロジック(ViewModel/Model)に帰属すべきだからだ。Viewが「何が正しいか」を知っている状態は、設計上の敗北を意味する。

2. 実装:堅牢な ViewModel の構築

ここでは、`IDataErrorInfo` を実装した実戦的な ViewModel のコードを示す。VB.NET の強力な型システムを活用し、プロパティの変更通知(`INotifyPropertyChanged`)とバリデーションを統合する。

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

''' <summary>
''' 業務データの整合性を担保するViewModel
''' </summary>
Public Class UserRegistrationViewModel
    Implements INotifyPropertyChanged, IDataErrorInfo

    ' --- Private Fields ---
    Private _userName As String = String.Empty
    Private _age As Integer = 0

    ' --- Public Properties ---

    ''' <summary>
    ''' ユーザー名:必須入力、かつ5文字以上
    ''' </summary>
    Public Property UserName As String
        Get
            Return _userName
        End Get
        Set(value As String) Implements INotifyPropertyChanged.SetProperty(Of String)(value)
            _userName = value
            OnPropertyChanged()
        End Set
    End Property

    ''' <summary>
    ''' 年齢:18歳以上、120歳以下
    ''' </summary>
    Public Property Age As Integer
        Get
            Return _age
        End Get
        Set(value As Integer) Implements INotifyPropertyChanged.SetProperty(Of Integer)(value)
            _age = value
            OnPropertyChanged()
        End Set
    End Property

    ' --- IDataErrorInfo Implementation ---

    ''' <summary>
    ''' オブジェクト全体の検証結果を返す(必要に応じて実装)
    ''' </summary>
    Public ReadOnly Property Error As String Implements IDataErrorInfo.Error
        Get
            Return String.Empty ' オブジェクト全体のエラーが必要な場合はここにロジックを記述
        End Get
    End Property

    ''' <summary>
    ''' プロパティ名に基づき、個別のエラーメッセージを返す
    ''' </summary>
    Public Function Item(columnName As String) As String Implements IDataErrorInfo.this
        Select Case columnName
            Case nameof(UserName)
                If String.IsNullOrWhiteSpace(_userName) Then
                    Return "ユーザー名は必須入力です。"
                ElseIf _userName.Length < 5 Then
                    Return "ユーザー名は5文字以上で入力してください。"
                End If

            Case nameof(Age)
                If _age < 18 OrElse _age > 120 Then
                    Return "年齢は18歳から120歳の間で指定してください。"
                End If
        End Select

        Return String.Empty
    End Function

    ' --- INotifyPropertyChanged Implementation ---

    Public Event Property PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub
End Class

''' <summary>
''' 拡張メソッド:コードの冗長性を排除し、プロパティ変更と通知をひとまとめにする
''' </summary>
<Runtime.CompilerServices.Extension()>
Public Module PropertyExtensions
    <CallerMemberName>
    Public Sub SetProperty(Of T)(ByRef field As T, value As T, ByVal notify As Boolean, ByVal onPropertyChanged As Action(Of String))
        If EqualityComparer(Of T).Default.Equals(field, value) Then Return
        field = value
        If notify Then onPropertyChanged(Nothing)
    End Sub
End Module

アーキテクトの視点:メモリとパフォーマンス

上記の `SetProperty` 拡張メソッドは、単なる記述の簡略化ではない。`EqualityComparer` を用いて値の変化を検知することで、不必要な `PropertyChanged` イベントの発行を抑制している。これは、大規模なデータグリッドなどで数百のプロパティが頻繁に更新される際、UIスレッドのメッセージループへの負荷を最小限に抑えるための、極めて重要な最適化である。

3. XAML:視覚的フィードバックの設計

ViewModel で定義したエラー情報は、View が「どう表示するか」に委ねられる。WPF の `ControlTemplate` を活用し、エラー発生時にのみ表示される「アドーナー(Adorner)」をカスタマイズすることで、レイアウトを破壊することなく、洗練されたフィードバックを実現する。

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Validation Masterclass" Height="300" Width="400">
    
    <Window.Resources>
        <!-- エラー表示用のコントロールテンプレート定義 -->
        <ControlTemplate x:Key="ErrorTemplate">
            <StackPanel>
                <!-- 入力コントロールの周囲を赤枠で囲む -->
                <AdornedElementPlaceholder x:Name="placeholder"/>
                <!-- エラーメッセージを赤文字で表示 -->
                <TextBlock Text="{Binding [ErrorContent]}" 
                           Foreground="Red" 
                           FontSize="10" 
                           Margin="0,2,0,0"/>
            </StackPanel>
        </ControlTemplate>
    </Window.Resources>

    <StackPanel Margin="20">
        <Label Content="ユーザー名:"/>
        <!-- 
            ValidatesOnDataErrors=True: IDataErrorInfoを有効化
            UpdateSourceTrigger=PropertyChanged: 入力ごとに即時検証
            Validation.ErrorTemplate: カスタムテンプレートの適用
        -->
        <TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"
                 Margin="0,0,0,15"/>

        <Label Content="年齢:"/>
        <TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"
                 Margin="0,0,0,15"/>

        <Button Content="登録" Width="100" HorizontalAlignment="Left"/>
    </StackPanel>
</Window>

重要な技術的詳細

`UpdateSourceTrigger=PropertyChanged` を指定することで、ユーザーがフォーカスを外すのを待たずに、一文字入力するごとにバリデーションが走る。これはユーザー体験(UX)を向上させるが、一方で計算コストの高いバリデーション(例:DBへの重複チェック)をここに記述してはならない。そのような場合は、`INotifyDataErrorInfo` を用いて非同期処理を行うのが、プロフェッショナルの流儀である。

4. レガシー環境との境界線:システム間連携の極限知見

実務において、我々は常に「純粋なWPF」だけで戦っているわけではない。既存のCOMコンポーネントや、VBAで構築された巨大なExcelマクロ、あるいは古いWin32 APIを呼び出すDLLとの連携を迫られる。

ここで注意すべきは、「状態の同期」である。

1. オブジェクトのライフサイクル管理:
WPFのBindingは、ViewModelがGC(ガベージコレクション)によって回収されると、当然ながら動作を停止する。レガシーなDLLからコールバックを受け取る際、そのコールバックのコンテキストがUIスレッドであることを保証しなければ、`InvalidOperationException`(スレッド間操作の不正)を招く。

2. Windows APIとの境界:
もし、入力値に基づいて特定のWindows API(例:`SendMessage` による他プロセスへの通知)を呼び出す必要がある場合、その呼び出しは必ず `Dispatcher.Invoke` を介してUIスレッドのコンテキストで行うか、あるいはバックグラウンドスレッドで安全に処理し、結果だけを `PropertyChanged` でViewModelに戻すという、厳格な分離が必要である。

結論

WPFにおけるバリデーションとは、単なるエラー表示の技術ではない。それは、「データの整合性を守るための防壁」を、いかにしてUIスレッドのパフォーマンスを損なわずに、かつ保守性の高い形で構築するかという、アーキテクチャ設計そのものである。

`IDataErrorInfo` を使いこなし、ViewModelに責務を集約させ、XAMLで洗練されたフィードバックを与える。この一連の流れをマスターすることこそが、レガシーな「手続き型」から、モダンな「宣言型」へと脱皮するための、唯一の道である。

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