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

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

ユーザーに「間違い」を悟らせるな:WPFにおける堅牢な入力値検証(Validation)の極意

業務アプリケーションの開発において、最もコストがかかり、かつユーザーのストレスを増大させる要因は何か? 私は断言する。それは「不完全なデータが、保存ボタンを押した後に初めてエラーとして発覚すること」だ。

データベースに投げた後に「型が違います」「桁数が足りません」といったエラーが返ってくる設計は、プロフェッショナルとは言えない。フロントエンド(UI)の段階で、ユーザーがミスを犯した瞬間に、あるいは入力の最中に、エレガントかつ直感的に「何が間違っているのか」を提示すること。これが、真に使いやすい業務ツールを作るための最低条件である。

本稿では、VB.NETとWPFを用いたモダンなUI構築において、`IDataErrorInfo` を核とした、保守性と堅牢性を両立させる検証ロジックの実装手法を伝授する。

1. なぜ `ValidationRule` だけでは不十分なのか

WPFには、XAML側で定義できる `ValidationRule` という仕組みがある。しかし、実務レベルの複雑な業務ロジックを扱う場合、これだけに頼るのは危険だ。

  • ValidationRuleの限界:
  • ロジックの分散: 入力形式(正規表現など)のチェックには向いているが、「このIDがデータベースに既に存在するか?」といった、ビジネスロジックやデータ連携を伴う検証が書けない。
  • MVVMパターンの破壊: 検証ロジックがView(XAML)に依存してしまい、ユニットテストが困難になる。

我々が目指すべきは、「データそのものが自身の正当性を知っている」状態だ。そのためには、ViewModel側で実装可能な `IDataErrorInfo`(あるいはより高度な `INotifyDataErrorInfo`)を活用するのが定石である。

2. アーキテクチャ設計:責務の分離

堅牢な実装には、以下の3層の責務を明確にする必要がある。

1. View (XAML): エラーの「表示」に徹する。赤枠やエラーメッセージを出す仕組みを持つ。
2. ViewModel: 入力値の保持と、ビジネスルールに基づく「検証」を行う。
3. Model: 純粋なデータ構造。

この設計を守ることで、検証ロジックをUIから切り離し、単体テストが可能になる。

3. 実装ガイド:プロダクションレベルのコード例

それでは、具体的な実装を見ていこう。ここでは「ユーザー登録画面」を想定し、「ユーザー名(必須かつ5文字以上)」と「年齢(数値かつ18歳以上)」を検証する例を示す。

Step 1: 基底クラス(ViewModelBase)の構築

まず、すべてのViewModelの土台となる `INotifyPropertyChanged` を実装した基底クラスを用意する。これは、プロパティの値が変わったことをUIに通知するための必須要件だ。

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

''' <summary>
''' ViewModelの基底クラス。プロパティ変更通知を管理する。
''' </summary>
Public MustInherit Class ViewModelBase
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    ''' <summary>
    ''' プロパティの値を変更し、UIに通知する。
    ''' </summary>
    Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub
End Class

Step 2: 実践的な ViewModel の実装

ここで `IDataErrorInfo` を実装する。`this[string columnName]` インデクサが、WPFのバインディングエンジンから「このプロパティのエラーは何?」と問われた際の回答窓口となる。

''' <summary>
''' ユーザー登録用のViewModel。
''' IDataErrorInfoを実装することで、ビジネスロジックに基づく検証を行う。
''' </summary>
Public Class UserRegistrationViewModel
    Inherits ViewModelBase

    Implements IDataErrorInfo

    Private _userName As String = ""
    Private _age As String = "" ' 入力は文字列として受け取り、検証後に数値化するのが定石

    Public Property UserName As String
        Get
            Return _userName
        End Get
        Set(value As String) Implements INotifyPropertyChanged.PropertyChanged
            If _userName <> value Then
                _userName = value
                OnPropertyChanged()
                ' 値が変わったので、エラー状態を再評価させるために通知
                OnPropertyChanged(NameOf(Error))
                OnPropertyChanged(NameOf(this))
            End If
        End Set
    End Property

    Public Property Age As String
        Get
            Return _age
        End Get
        Set(value As String) Implements INotifyPropertyChanged.PropertyChanged
            If _age <> value Then
                _age = value
                OnPropertyChanged()
                OnPropertyChanged(NameOf(Error))
                OnPropertyChanged(NameOf(this))
            End If
        End Set
    End Property

    ' --- IDataErrorInfo の実装 ---

    ''' <summary>
    ''' エラーがある場合に、最初のメッセージを返す。
    ''' </summary>
    Public ReadOnly Property Error As String Implements IDataErrorInfo.Error
        Get
            Return String.Empty ' 全体的なエラーが必要なければEmptyを返す
        End Get
    End Property

    ''' <summary>
    ''' 各プロパティごとのエラーメッセージを返す。
    ''' </summary>
    Public ReadOnly Property this(columnName As String) As String Implements IDataErrorInfo.this
        Get
            Select Case columnName
                Case NameOf(UserName)
                    If String.IsNullOrWhiteSpace(UserName) Then
                        Return "ユーザー名は必須入力です。"
                    ElseIf UserName.Length < 5 Then
                        Return "ユーザー名は5文字以上で入力してください。"
                    End If

                Case NameOf(Age)
                    Dim ageValue As Integer
                    If Not Integer.TryParse(Age, ageValue) Then
                        Return "年齢には数値を入力してください。"
                    ElseIf ageValue < 18 Then
                        Return "18歳以上である必要があります。"
                    End If
            End Select

            Return String.Empty ' エラーなし
        End Get
    End Property
End Class

Step 3: XAMLによる視覚的フィードバックの設定

ここが最も重要だ。単に赤枠が出るだけでは、ユーザーは何が悪いのか分からない。`ControlTemplate` をカスタマイズして、エラーメッセージを吹き出しやテキストとして表示させる。

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="User Registration" Height="300" Width="400">
    
    <Window.Resources>
        <!-- エラー表示用のスタイル定義 -->
        <Style x:Key="ErrorValidationStyle" TargetType="TextBox">
            <Setter Property="Margin" Value="0,5,0,15"/>
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <StackPanel>
                            <!-- 入力コントロール本体 -->
                            <AdornedElementPlaceholder x:Name="errorAdorner"/>
                            <!-- エラーメッセージの表示 -->
                            <TextBlock Text="{Binding [0].ErrorContent}" 
                                       Foreground="Red" 
                                       FontSize="11" 
                                       Margin="0,2,0,0"/>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <!-- エラー時の枠線の色を変更 -->
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="BorderBrush" Value="Red"/>
                    <Setter Property="BorderThickness" Value="2"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <StackPanel Margin="20">
        <TextBlock Text="ユーザー名:"/>
        <!-- ValidatesOnDataErrors=True が必須 -->
        <TextBox Style="{StaticResource ErrorValidationStyle}"
                 Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"/>

        <TextBlock Text="年齢:"/>
        <TextBox Style="{StaticResource ErrorValidationStyle}"
                 Text="{Binding Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"/>
        
        <Button Content="登録" Width="100" HorizontalAlignment="Right"/>
    </StackPanel>
</Window>

4. 伝説のアーキテクトからの忠告:実務で陥る罠

コードを書いただけで満足してはいけない。現場で「動かない」「使いにくい」と言われないために、以下の3点を肝に銘じておくことだ。

① `UpdateSourceTrigger=PropertyChanged` を忘れるな

デフォルトでは、TextBoxの `Text` プロパティの更新は「フォーカスが外れた時」に行われる。これでは、ユーザーが入力している最中のエラー検知が遅れ、UXを損なう。リアルタイムなフィードバックを求めるなら、必ず `PropertyChanged` を指定せよ。

② データベース連携時の「非同期検証」

もし「入力されたIDがDBに存在するか」をチェックする場合、`IDataErrorInfo` 内で直接DBにクエリを投げてはいけない。UIスレッドをブロックし、アプリケーションがフリーズする原因になる。
その場合は、`INotifyDataErrorInfo` を使い、`Task.Run` 等を用いてバックグラウンドで検証を行い、結果が出たタイミングで `ErrorsChanged` イベントを発行する設計に昇華させる必要がある。

③ 「保存ボタン」の制御(Commandの活用)

エラーが表示されているのに「保存」ボタンが押せてしまう設計は、不完全だ。
ViewModel側で `ICommand`(RelayCommand等)を実装し、`CanExecute` メソッドの中で「すべての検証がパスしているか」を判定せよ。「間違いがある状態では、そもそも実行させない」。これが、バグの起きない堅牢なシステムの鉄則である。

結びに

検証(Validation)は、単なるエラーチェックではない。ユーザーに対する「ガイド」であり、データの整合性を守る「防壁」である。
`IDataErrorInfo` を使いこなし、適切なXAMLスタイルを当てることで、あなたの作るアプリケーションの品質は、一気にプロフェッショナルの領域へと引き上げられるだろう。

設計に妥協するな。ユーザーに不備を許すな。 それがエンジニアとしての誇りである。

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