VB のたまご

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



データ検証2

  •  データ検証その2です。

  •  前回、プロパティ内に値の検証処理を追加しましたが、できればシンプル構成のまま、検証処理は書きたくありません。 検証処理をしたくないわけではなく、したいんだけど書くと見通しが悪くなる感じがなんとなくあれです。

  • スポンサーリンク


  •  そういう時は、「System.ComponentModel.DataAnnotations」名前空間の検証属性を書くと良いです。 それでは、よく分からなくていいので、これを使ってどう動くのか見てみましょう。以下は標準の WPF アプリケーションで作成しています。 ※参照設定の追加で「System.ComponentModel.DataAnnotations.dll」を追加しています。

  •  Model 層

  • Imports System.ComponentModel
    Imports System.Runtime.CompilerServices
    Imports System.ComponentModel.DataAnnotations
    
    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
    
    
    
        ' 自前で検証エラー情報を管理するのを止めて、検証クラスにお任せする
        ' 検証内容は、(このクラスを継承したデータクラスに記載した)プロパティ属性に書いた判定処理
        ' 検証 NG の場合は、同じくプロパティ属性に書いたエラーメッセージを返却
    
        ' エラー情報があるかどうかを View に返答
        ' TryValidateObject メソッドによる検証をすることで、プロパティ属性に書いた Required, StringLength 属性のチェックが走る
        ' 第四引数を False、または省略すると、Required 属性のみを対象に検証する。全部してほしいので True をセット
        Public ReadOnly Property HasErrors As Boolean Implements INotifyDataErrorInfo.HasErrors
            Get
    
                Dim context = New ValidationContext(Me, Nothing, Nothing)
                Dim items = New List(Of ValidationResult)
    
                Return Not Validator.TryValidateObject(Me, context, items, True)
    
            End Get
        End Property
    
        ' エラー情報があった場合、指定プロパティに関するエラーを View に返却
        ' ここ(継承元)から継承先クラスのプロパティ値を取得するため、リフレクション経由で取得
        ' 検証は属性数分行うが、NG があった時点で検証終了する。よって後続検証が走らないので、NG で返却がある場合は常に1つ分だけ・・・
        Public Function GetErrors(propertyName As String) As IEnumerable Implements INotifyDataErrorInfo.GetErrors
    
            Dim value = Me.GetType().GetProperty(propertyName).GetValue(Me, Nothing)
            Dim context = New ValidationContext(Me, Nothing, Nothing) With {.MemberName = propertyName}
            Dim items = New List(Of ValidationResult)
    
            If Validator.TryValidateProperty(value, context, items) Then
                Return Nothing
            End If
    
            Return items.Select(Function(x) x.ErrorMessage)
    
        End Function
    
    #End Region
    
    End Class
    

    Imports System.ComponentModel.DataAnnotations
    
    Public Class Class1
        Inherits NotificationObject
    
        Private _MyName As String = "taro"
    
        <Required(ErrorMessage:="入力必須です")>
        <StringLength(10, ErrorMessage:="10文字以下で入力してください")>
        Public Property MyName As String
            Get
                Return _MyName
            End Get
            Set(value As String)
    
                ' 変更されたかどうかを判定
                If _MyName = value Then
                    Return
                End If
    
                _MyName = value
    
                ' イベント発生
                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:WpfApplication4f"
            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
    

  •  実行結果については前回とほとんど同じ動きのため割愛します。 前回のプログラムとどこが変わったか分かりますでしょうか。View 層は前回と同じで何も変わっていないですね。 ということで、変わったのは Model 層です。

  •  Class1.vb を見ると検証処理が書いておらず、代わりにプロパティの頭に属性が書かれています。 「Required(必須)」、「入力必須です」と書かれていますが、シンプルにそのままですね。 「未入力の場合は、「入力必須です」というエラーメッセージを出す」ということです。

  •  「StringLength(文字列の長さ)」、10文字まで、「10文字以下で入力してください」と書かれていますが、 これもそのままですね。「10文字以上入力したら、「10文字以下で入力してください」というエラーメッセージを出す」ということです。

  •  検証処理を処理内から外して、属性として処理を追加する形式の検証処理です。 見通し良いし分かりやすいですね。

  • スポンサーリンク


  •  続いて、NotificationObject.vb を見てみます。 イベント通知は変更無しですが、前回の構成と違って、エラー管理を止めて判定処理も変わりました。

  •  まず「HasErrors」プロパティです。ここでは、Validator.TryValidateObject メソッドに検証処理を委託するように変えました。 TryValidateObject メソッドでは、Class1.vb 全体として検証した結果、NG があるか無いかを検証します。 True だとエラーなし、False だとエラーありです。メソッド名的に「エラーがあるかどうか」なので、戻り値に False を掛けています。 結果が「はい(True)」なら「エラーあり」、「いいえ(False)」なら「エラーなし」になるので考え方が逆になります。

  •  次に「GetErrors」メソッドです。こちらも同じような判定になります。 TryValidateObject メソッドがクラス自体を見る版で、TryValidateProperty メソッドがクラスの内指定プロパティを見る版になります。 これらのメソッドの中で、Required 属性やStringLength 属性による検証が行われています。

  •  第三引数に ValidationResult なリストのインスタンスをセットしています。 検証の結果 NG があった場合、このリストにエラーメッセージを含む検証結果データが入ってきます。メソッドの中でリストにデータ登録されるんですね。 TryValidateProperty メソッドの戻り値は、エラーなしなら「True」、エラーありなら「False」です。 エラーが無い場合は、Nothing を返却して、エラーがある場合は、メッセージのリストに変換して返却します。 ラムダ式の Select メソッドです。

  •  属性による検証は他にもありますので、詳しく調べてみるのもいいかもしれませんね。

  •  最後に、登録画面のサンプルを見て終わりたいと思います。 Class1.vb や NotificationObject.vb は上記と同じものを使っています。

  • <Window x:Class="ValidateWindow3"
            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:w07_DataBinding6"
            mc:Ignorable="d"
            Title="ValidateWindow3" Height="150" Width="500">
    
        <Window.DataContext>
            <local:Class1 />
        </Window.DataContext>
    
        <StackPanel Margin="10">
    
            <StackPanel Orientation="Horizontal" Margin="10">
    
                <TextBlock Text="名前:" Width="100" />
    
                <TextBox 
                    Name="textbox1"
                    Width="150"
                    Text="{Binding MyName, UpdateSourceTrigger=PropertyChanged}" />
    
                <TextBlock
                    Width="200"
                    Text="{Binding ElementName=textbox1, Path=(Validation.Errors).CurrentItem.ErrorContent}" />
    
            </StackPanel>
    
            <StackPanel Orientation="Horizontal" Margin="10" HorizontalAlignment="Right">
    
                <Button Content="OK" Width="80" Margin="10">
                    <Button.Style>
                        <Style TargetType="Button">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding ElementName=textbox1, Path=(Validation.HasError)}" Value="True">
                                    <Setter Property="IsEnabled" Value="False" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Button.Style>
                </Button>
                
                <Button 
                    Content="キャンセル" Width="80" Margin="10" />
    
            </StackPanel>
            
    
        </StackPanel>
    
    </Window>
    

  •  これを実行して、入力欄の文字列を全消ししてみましょう。すると OK ボタンがグレーアウトします。 続いて、今度は10文字以上何か入力してみてください。同じように OK ボタンがグレーアウトします。 1文字以上10文字以下の場合は OK ボタンが活性化して押すことができます。

  •  実際のアプリでは、複数の入力欄が用意すると思いますので、MultiDataTrigger を使うか、 ボタンの IsEnabled をバインドデータでバインドしておいて、ViewModel で複数チェックと活性/グレーアウト切り替えをおこなうか、 まあそれは置いておいて、こういうやり方もありますよということでした。