.NETで作る!

.NETに関するあれこれ(C#、VB.NET)

WPF で属性を使って値検証を実装する

WPFでは値検証方法が複数用意されていますので、標準機能を理解しつつより良い実装を考えてみます。

標準機能

  1. XAMLにValidationRuleを書く
    入力データ検証 その6 カスタムValidationRule - Yuya Yamaki’s blog
  2. EntityクラスのプロパティSetterで例外を起こす
    入力データ検証 その8 Silverlight 2 - Yuya Yamaki’s blog
  3. 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 」にてプロパティの名前が渡されるので、

  1. 同プロパティに紐づく検証属性をスキャンし、
  2. 検証属性があったら順次実行(戻り値はエラーメッセージ)し、
  3. エラーメッセージを返す。

という具合の実装になります。上記は特別な処理は何もないので難しくもなんともないですね。

実装

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

で完成。ほぼ理想どおりに実装できました。

. .