VB のたまご

作成日: 2017/03/20, 更新日: 2017/03/20



データ検証1

  •  ここでは、入力した値の検証機能について説明します。 どこの部分かと言うと ViewModel → View へプロパティ値変更の連絡をする部分です。 全体図の画像イメージには記載していませんが、INotifyPropertyChanged と同じ位置、同じ方向です。

  • スポンサーリンク


  •  基本的に画面を持つアプリケーションの場合、ユーザーから何らかの情報を入力してもらって処理を行うことが多いです。 顧客管理とか在庫管理とか、要するに TextBox 等の入力コントロールを使ったアプリケーションです。

  •  この時、画面から入力した値が正しければいいのですが、もし間違った値を入力された場合、または入力必須なのに未入力のままの場合、 後続処理で困ってしまうので、次の処理に遷移する前に、入力された値が妥当かどうかをチェックしないといけません。

  •  例えば「名前」の未入力、「年齢」に500歳などされたら嘘情報なんじゃないかと困ってしまいます。 検証処理は、後続の仕様に合わせるために必要になる必須処理なんですね。

  •  検証処理と言うと、チェック用メソッドを用意して、コントロールから値を取得して妥当性チェックを思い浮かべますが(古いかな?)、 ここでは、「INotifyDataErrorInfo」インターフェースを継承した検証処理を学習します。

  • Imports System.Collections
    
    Namespace System.ComponentModel
        '
        ' 概要:
        '     カスタムの同期および非同期の検証をサポートするデータ エンティティ クラスの実装ができるメンバーを定義します。
        Public Interface INotifyDataErrorInfo
            '
            ' 概要:
            '     エンティティに検証エラーがあるかどうかを示す値を取得します。
            '
            ' 戻り値:
            '     エンティティになって検証エラーがある場合 true ; それ以外 false。
            ReadOnly Property HasErrors As Boolean
    
            '
            ' 概要:
            '     プロパティまたはエンティティ全体で検証エラーが変更されたときに発生します。
            Event ErrorsChanged As EventHandler(Of DataErrorsChangedEventArgs)
    
            '
            ' 概要:
            '     指定したプロパティまたはエンティティ全体の検証エラーを取得します。
            '
            ' パラメーター:
            '   propertyName:
            '     検証エラーを取得するプロパティ名 ; または null または System.String.Empty、エンティティ レベルのエラーを取得します。
            '
            ' 戻り値:
            '     プロパティまたはエンティティの検証エラー。
            Function GetErrors(propertyName As String) As IEnumerable
        End Interface
    End Namespace
    

  •  「INotifyDataErrorInfo」インターフェースのメンバーは「ErrorsChanged」イベント、「HasErrors」プロパティ、「GetErrors」メソッドです。 それでは、よく分からなくていいので、このメンバーを使ってどう動くのか見てみましょう。以下は標準の WPF アプリケーションで作成しています。

  •  Model 層

  • Imports System.ComponentModel
    Imports System.Runtime.CompilerServices
    
    Public Class NotificationObject
        Implements INotifyPropertyChanged, INotifyDataErrorInfo
    
    #Region "INotifyPropertyChanged"
        ' プロパティ値が変更されたら、イベントを発生させて View に通知
    
        Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    
        ' System.Runtime.CompilerServices 名前空間の CallerMemberName 属性を付けると、
        ' 呼び出し元のプロパティ名が自動的にセットされる(ので、呼び出し元ではいちいち文字列指定しなくてもセットしてくれるので楽)
        Protected Sub RaisePropertyChanged(<CallerMemberName> Optional propertyName As String = "")
    
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    
        End Sub
    
    #End Region
    
    #Region "INotifyDataErrorInfo"
        ' プロパティ値が妥当ではない場合、イベントを発生させて View に通知
    
        Public Event ErrorsChanged As EventHandler(Of DataErrorsChangedEventArgs) Implements INotifyDataErrorInfo.ErrorsChanged
    
        Protected Sub RaiseErrorsChanged(<CallerMemberName> Optional propertyName As String = "")
    
            RaiseEvent ErrorsChanged(Me, New DataErrorsChangedEventArgs(propertyName))
    
        End Sub
    
    
    
        ' 検証エラーを管理する辞書。プロパティ名:エラーリストというペア
        ' プロパティ名が登録されていたとしても、エラーリストが Nothing なら問題無し
        Public ErrorDictionary As New Dictionary(Of String, List(Of String))
    
        ' エラー情報があるかどうかを View に返答
        Public ReadOnly Property HasErrors As Boolean Implements INotifyDataErrorInfo.HasErrors
            Get
                Return Me.ErrorDictionary.Values.Any(Function(x) x IsNot Nothing)
            End Get
        End Property
    
        ' エラー情報があった場合、指定プロパティに関するエラーリストを View に返却
        Public Function GetErrors(propertyName As String) As IEnumerable Implements INotifyDataErrorInfo.GetErrors
    
            If String.IsNullOrWhiteSpace(propertyName) Then
                Return Nothing
            End If
    
            If Not Me.ErrorDictionary.ContainsKey(propertyName) Then
                Return Nothing
            End If
    
            Return Me.ErrorDictionary(propertyName)
    
        End Function
    
    #End Region
    
    End Class
    

    Public Class Class1
        Inherits NotificationObject
    
        Private _MyName As String = "taro"
    
        Public Property MyName As String
            Get
                Return _MyName
            End Get
            Set(value As String)
    
                ' 変更されたかどうかを判定
                If _MyName = value Then
                    Return
                End If
    
                _MyName = value
    
                ' 値の検証
                Dim errorFound = False
                If value.Length = 0 Then
                    errorFound = True
                    MyBase.ErrorDictionary(NameOf(Me.MyName)) = New List(Of String) From {"入力必須です"}
                End If
    
                If 5 < value.Length Then
                    errorFound = True
                    MyBase.ErrorDictionary(NameOf(Me.MyName)) = New List(Of String) From {"5文字以上入力された"}
                End If
    
                If Not errorFound Then
                    MyBase.ErrorDictionary(NameOf(Me.MyName)) = Nothing
                End If
    
                ' イベント発生
                Me.RaiseErrorsChanged()
                Me.RaisePropertyChanged()
    
            End Set
        End Property
    
    End Class
    

  •  View 層

  • <Window x:Class="MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WpfApplication4e"
            mc:Ignorable="d"
            Title="MainWindow" Height="350" Width="525">
    
        <Window.DataContext>
            <local:Class1 />
        </Window.DataContext>
    
        <StackPanel Margin="10">
    
            <TextBox 
                Name="textbox1"
                Text="{Binding MyName, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}">
    
                <!-- 検証エラー時のみツールチップ表示 -->
                <TextBox.Style>
                    <Style TargetType="TextBox">
                        <Style.Triggers>
                            <Trigger Property="Validation.HasError" Value="True">
                                <Setter Property="ToolTip">
                                    <Setter.Value>
                                        <Binding RelativeSource="{RelativeSource Self}" Path="(Validation.Errors).CurrentItem.ErrorContent" />
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </TextBox.Style>
            </TextBox>
            
            <TextBlock 
                Text="{Binding ElementName=textbox1, Path=Text.Length}" />
    
            <TextBlock
                Name="textblock1"
                Text="{Binding ElementName=textbox1, Path=ToolTip}" />
    
        </StackPanel>
    
    </Window>
    

    Class MainWindow
    
    End Class
    

  •  プログラムの仕組みを知る前に、まずはどういう風に動くのか見てみましょう。 実行すると、「taro」と「4」が表示されています。「4」は「taro」という文字列の長さを表しています。 入力文字数が変わると、合わせてカウントも変わります。

  •  ここで入力文字列を全部消してみます。 すると TextBox が赤枠になり、カウント数の下に「入力必須です」という文字列が表示されました。 TextBox にマウスカーソルを移動してくるとツールチップが表示され、やはり同じ文言です。 何か1文字だけでも入力すると、赤枠が消えて注意文も消えます。ツールチップも表示されなくなります。

  •  続いて、今度は5文字以上何か入力してみます。すると6文字目以降から「5文字以上入力された」と注意文が出てきます。 ツールチップも再度表示されるようになり、やはり同じ文言です。 5文字になるまで文字列を消すと、同じように赤枠が消えて注意文とツールチップが表示されなくなります。

  •  検証処理が走っていることと、検証 NG の場合にユーザーへアピールしていることが分かると思います。

  • スポンサーリンク


  •  データ検証の流れは以下の通りです。 継承先となるデータクラスのプロパティ値が変更されたとします。①この時プロパティ内で自前の検証チェックをします。 もし検証 NG があった場合、ベースクラス内で管理している自前のエラー管理辞書に、プロパティ名とエラーメッセージを登録していきます。

  •  検証が終わったら、②検証状態変更通知として「ErrorsChanged」イベントを発生させて View に通知します。

  •  ③ View から、「継承先となるデータクラス」で、何か1つ以上エラーがあるかどうかを確認するため「HasErrors」プロパティが呼び出されます。 ここでは、「任意のプロパティ」に対する「エラー」という粒度ではなく、とりあえず「このクラス全体」の中で何かエラーがあるかどうかを聞いています。

  •  ④「HasErrors」プロパティが True の場合、「今扱っているプロパティ」に対する検証エラーがあるかどうかを知りたいので、「GetErrors」メソッドを呼び出して、 該当データを返却します。このデータにエラーが登録されていれば、コントロール(今回であれば TextBox)に赤枠が付いて、任意に指定した個所に「検証エラーメッセージ」が表示されます。

  • イメージ
  •  どういう動き方か分かったところで、プログラムを見てみましょう。 まずは、「INotifyDataErrorInfo」インターフェースを継承している Model 層の NotificationObject.vb からです。 このクラスは以前学習した変更通知プロパティで出てきたベースクラスで、このクラスに検証機能も持たせています。

  •  まず、「ErrorsChanged」イベントを発生させるための発火メソッドを用意しています。ただイベントを投げるだけです。 続いて、検証エラーの結果を管理する辞書変数を自前で持っています。「ErrorDictionary」フィールドです。 キーが「プロパティ名」、値が「エラーメッセージリスト」です。1つのプロパティに付き複数のエラーメッセージを管理できます。

  •  「HasErrors」プロパティは読み取り専用で何か1つ以上エラーがあるかどうかを返却します。 すなわち、エラー辞書の値のうち、Nothing ではない List(Of String) が1つ以上あるか無いかを返却しています。 ラムダ式の Any メソッドですね。

  •  「GetErrors」メソッドでは、「指定のプロパティに該当する」エラーメッセージリストを返却しています。

  •  次に Class1.vb です。 ここでは、セッター処理時に、値の検証処理を行っています。NG があればベースクラスのエラー管理辞書に登録します。 ここで注意しなければいけないことは、NG から 正常値に戻った時のフォロー処理です。 NG で登録していたエラー情報を削除しておかないと、正常値なのに赤枠と注意文言が表示されたままになってしまいます。 検証チェックが終わったら View にイベント通知します。

  •  最後に「MainWindow.xaml」です。 「TextBox」のバインド設定内で、「ValidatesOnNotifyDataErrors」に「True」をセットしています。 これがオンになっていると、「INotifyDataErrorInfo」とのやり取りをはじめとした検証機能を動かすことができるようになります。

  •  本来は、「ValidatesOnNotifyDataErrors」は規定値で「True」がセットされていますので、記載を省略しても検証機能が動作しますが、 明示的に記載することで検証していることがすぐ把握できますので、ケースバイケースかなと思います。

  •  以下は検証処理というよりは Xaml の書き方の説明です。 ツールチップの定義のところで、スタイルにトリガーを追記して、エラー時のみツールチップを表示するように書いています。 この中でバインド設定をしていますが、RelativeSource に Self (自分自身=textbox1)を指定して、「(Validation.Errors).CurrentItem.ErrorContent」というメンバーを設定するように書いています。 「エラー管理辞書から引っ張ってきたエラーメッセージリストの内、最初のエラーメッセージ文言」という意味のメンバーになります。

  •  後は、入力文字列数をカウントした値を表示している TextBlock と、ツールチップに表示されたエラー文言を表示している TextBlock です。 こちらもバインド設定の書き方についてですが、ElementName に該当コントロール名を指定して、そのコントロールの ToolTip メンバーを指定することでその内容をバインドすることができるようになります。