整合性の防壁: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で洗練されたフィードバックを与える。この一連の流れをマスターすることこそが、レガシーな「手続き型」から、モダンな「宣言型」へと脱皮するための、唯一の道である。
