ユーザーに「間違い」を悟らせるな: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スタイルを当てることで、あなたの作るアプリケーションの品質は、一気にプロフェッショナルの領域へと引き上げられるだろう。
設計に妥協するな。ユーザーに不備を許すな。 それがエンジニアとしての誇りである。
