WPF で属性を使って値検証を実装する
WPFでは値検証方法が複数用意されていますので、標準機能を理解しつつより良い実装を考えてみます。
標準機能
- XAMLにValidationRuleを書く
入力データ検証 その6 カスタムValidationRule - Yuya Yamaki’s blog - EntityクラスのプロパティSetterで例外を起こす
入力データ検証 その8 Silverlight 2 - Yuya Yamaki’s blog - EntityクラスでIDataErrorInfoインターフェイスを実装する
入力データ検証 その5 DataErrorValidationRule - Yuya Yamaki’s blog
それぞれ一長一短がありますので、まとめます。
XAMLにValidationRuleを書く
メリット
- 画面依存の検証に向く。(当然、画面が違えば再利用できない。)
デメリット
- だいたいのケースはEntity依存なので使用頻度はあまりなさそう。
EntityクラスのプロパティSetterで例外を起こす
メリット
- Entity依存のため、どの画面であっても検証される。
デメリット
- Setterに挟むということで省略記述ができなくなり面倒。といっても一般的にEntityクラスはINotifyPropertyChangedインターフェイスを実装している関係上、Setterを省略せずに記述することが大半なのでそれほどでもないか。
- Setterなので画面から代入しようが、ロジックで代入しようが確実にこける。(分離できないという点では自由度低い)
- 複数プロパティ間での整合性判定、クラス全体の整合性判定は、Setterがないので実装しづらい。
EntityクラスでIDataErrorInfoインターフェイスを実装する
メリット
- Entity依存のため、どの画面であっても検証される。
- 値検証はIDataErrorInfoを経由してコールされるので、ブレークポイントを置きやすい。
- クラス全体の整合性判定も可能。
デメリット
- 特になし。
標準機能の結論
ひとまず、「EntityクラスでIDataErrorInfoインターフェイスを実装する」が自由度もあってよさそうです。こちらをベースで考えます。
IDataErrorInfoインターフェイスの問題点
問題点というと語弊がありますが、値検証というのは「必須」、「X文字以下」とかある程度パターン化されているものが多いので、再利用性が高いとうれしいです。
そこでWPFとかその辺の無視して、「理想の値検証イメージコード」を書き、それをIDataErrorInfoインターフェイスでうまく表現できるかというアプローチを試みます。
理想のEntityクラス(値検証)
私はこんな感じで検証を属性指定できたら最高です。
Public Class City <Require, LengthRange(Max:=8)> Public Property CityId() As Integer <Require, ByteRange(Max:=200)> Public Property CityName() As String End Class
説明不要かと思いますが、「CityIdは必須、数字8ケタ以下」、「CityNameは必須、200バイト以下」という検証が必要だとしています。データベースの列の型定義みたいなもんですね。
理想に近づく
IDataErrorInfoインターフェイスの実装は前提条件なので、実装しましょう。するとこんな感じになります。
Public Class City Implements IDataErrorInfo '略 Public ReadOnly Property [Error] As String Implements IDataErrorInfo.Error Get 'TODO: ここに全プロパティとクラス全体の検証処理と検証結果(エラー時はメッセージを返す)を実装する End Get End Property Public ReadOnly Property PropertyDataErrorInfo(propName As String) As String Implements IDataErrorInfo.Item Get 'TODO: ここにプロパティの検証処理と検証結果(エラー時はメッセージを返す)を実装する End Get End Property End Class
IDataErrorInfo.Error は IDataErrorInfo.Item の全体版なので話をスムーズに進めるため省略。IDataErrorInfo.Item だけにフォーカスを当てます。
引数「propName 」にてプロパティの名前が渡されるので、
- 同プロパティに紐づく検証属性をスキャンし、
- 検証属性があったら順次実行(戻り値はエラーメッセージ)し、
- エラーメッセージを返す。
という具合の実装になります。上記は特別な処理は何もないので難しくもなんともないですね。
実装
Public Interface IValidator Function Validate(value As Object, cultureInfo As Globalization.CultureInfo) As ValidationResult End Interface ''' <summary> ''' 検証属性 ''' </summary> ''' <remarks></remarks> <AttributeUsage(AttributeTargets.Property, Allowmultiple:=True)> Public MustInherit Class ValidationAttribute Inherits Attribute Implements IValidator MustOverride Function Validate(value As Object, cultureInfo As Globalization.CultureInfo) As ValidationResult Implements IValidator.Validate End Class ''' <summary> ''' 必須検証属性 ''' </summary> ''' <remarks></remarks> <AttributeUsage(AttributeTargets.Property, Allowmultiple:=False)> Public NotInheritable Class RequireAttribute Inherits ValidationAttribute Public Overrides Function Validate(ByVal value As Object, ByVal cultureInfo As System.Globalization.CultureInfo) As System.Windows.Controls.ValidationResult Dim length As Integer = If(value Is Nothing, 0, value.ToString.Trim.Length) If length = 0 Then Return New ValidationResult(False, "必須です。") End If Return New ValidationResult(True, Nothing) End Function End Class
今回はValidationRuleクラスを意識してこんな感じに実装してみましたが、ダイレクトにString型(エラーメッセージ)を返しても問題ないと思います。
あとは、IDataErrorInfoインターフェイス実装部分を基底クラス(ex.EntityBase)に押し込んでおけば
Public Class City Inherits EntityBase <Require, LengthRange(Max:=8)> Public Property CityId() As Integer <Require, ByteRange(Max:=200)> Public Property CityName() As String End Class
で完成。ほぼ理想どおりに実装できました。