入力データ検証 その7 BindingGroup

個々のデータではなく入力されたデータをまとめて検証したい場合(たとえば値段が1000円以下の場合には個数は5以下でなければならないといったように複合的な検証が行われる場合)、DataErrorValidationRuleのようにバインドしているソースオブジェクト側に検証ロジックがある場合であれば実現可能ですが、カスタムのValidationRuleを作成する方法では対応できません。そのような場合に使用するのがBindingGroupです。なお、BindingGroupクラス、およびそれに関係する機能は.NET Framework 3.5 SP1と3.0 SP1から追加されています。


ある要素のBindingGroupプロパティBindingGroupオブジェクトを設定すると、以下の2つの条件のどちらかを満たしている場合にBindingがグループ化されます。

  • BindingのソースがBindingGroupを設定した要素のDataContextと同じ場合
  • BindingのBindingGroupNameプロパティがBindingGroupのNameと同じ場合



Window1.xaml

<Window x:Class="Window1"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:local="clr-namespace:ValidationSample"
   Title="Window1" Height="300" Width="350" FontSize="32">
    <Window.Resources>
        <Style TargetType="Grid">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip"
                          Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
 
    <Grid x:Name="LayoutRoot" Margin="10" Loaded="LayoutRoot_Loaded">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
 
        <Grid.BindingGroup>
            <BindingGroup>
                <BindingGroup.ValidationRules>
                    <local:ObjectValidation/>
                </BindingGroup.ValidationRules>
            </BindingGroup>
        </Grid.BindingGroup>
 
        <TextBlock VerticalAlignment="Center" Margin="10" Text="値段:"/>
        <TextBox Grid.Column="1" Margin="10" VerticalAlignment="Center" Text="{Binding Price}"/>
        <TextBlock VerticalAlignment="Center" Grid.Row="1" Margin="10" Text="個数:"/>
        <TextBox Grid.Column="1" Grid.Row="1" Margin="10" VerticalAlignment="Center" Text="{Binding Unit}"/>
 
        <Button Grid.Row="2" Margin="10" VerticalAlignment="Center" Content="OK" Click="SubmitButton_Click"/>
        <Button Grid.Column="1" Grid.Row="2" Margin="10" VerticalAlignment="Center" Content="キャンセル" Click="CancelButton_Click"/>
 
    </Grid>
</Window>


Window1.xaml.vb
Class Window1
 
    Private Sub LayoutRoot_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
        LayoutRoot.DataContext = New DataObject()
        LayoutRoot.BindingGroup.BeginEdit()
    End Sub
 
    Private Sub SubmitButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
        If LayoutRoot.BindingGroup.CommitEdit() Then
            MessageBox.Show("登録されました。")
            LayoutRoot.BindingGroup.BeginEdit()
        End If
    End Sub
 
    Private Sub CancelButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
        LayoutRoot.BindingGroup.CancelEdit()
        LayoutRoot.BindingGroup.BeginEdit()
    End Sub
 
End Class


上記のサンプルコードの場合、GridのBindingGroupプロパティにBindingGroupオブジェクトを設定しています。GridのDataContextプロパティにデータオブジェクトを設定した場合その値は下位の要素に継承されるため、GridのDataContextとTextBoxにおけるBindingのソースは同一となります。


BindingGroupにはいくつかのメソッドが用意されています。このうち、BeginEditCommitEditCancelEditの3つは対で使用します。BeginEditメソッドを実行した段階で、BindingGroupで管理されるBindingの編集トランザクションが開始されます。CancelEditを実行すると、保留中の変更を破棄して編集トランザクションが終了し、BeginEdit実行時の値に戻ります。CommitEditを実行すると、BindingGroupのValidationRuleが実行され、検証が合格した場合にはバインディングソースが更新されます。


ObjectValidation.vb
Public Class ObjectValidation
    Inherits ValidationRule
 
    Public Overrides Function Validate(ByVal value As Object, ByVal cultureInfo As System.Globalization.CultureInfo) As System.Windows.Controls.ValidationResult
        Dim bg As BindingGroup = value
        Dim dao As DataObject = bg.Items(0)
 
        Dim unitProp As String = Nothing
        Dim priceProp As String = Nothing
 
        Dim unitResult As Boolean = bg.TryGetValue(dao, "Unit", unitProp)
        Dim priceResult As Boolean = bg.TryGetValue(dao, "Price", priceProp)
 
        If (Not unitResult) Or (Not priceResult) Then
            Return New ValidationResult(False, "指定されたプロパティが見つかりません。")
        End If
 
        Dim unit, Price As Integer
 
        If (Not Integer.TryParse(unitProp, unit)) Or (Not Integer.TryParse(priceProp, Price)) Then
            Return New ValidationResult(False, "値段と個数には整数値を入力してください。")
        ElseIf Price <= 1000 And unit > 5 Then
            Return New ValidationResult(False, "1000円以下の商品はお一人様5個までとなっております。")
        End If
 
        Return ValidationResult.ValidResult
 
    End Function
End Class


BindingGroupのValidationRulesプロパティに設定したValidationRuleでは、ValidateメソッドのパラメータvalueにはBindingGroupオブジェクトが渡ってきます。BindingGroup.ItemsプロパティからBindingで使用しているソース(データオブジェクト)を取得し、そこからTryGetValueメソッドを使用して個々の(特定のプロパティの)値が取得できます。この例では、Priceプロパティが1000以下の場合でUnitプロパティが5より大きかった場合に検証エラーとしています。





BindingGroupを使うと、各Bindingを個別に更新するのではなくBindingのソースを一度に更新することが可能になります。この性質はBindingGroupのValidationRuleを使用せず個々のBindingでのみValidationRuleを使用する場合でも有効です。個々のBindingに設定されたValidationRuleは、ValidationStepがRawProposedValueであるもののみがBindingGroupのValidationRuleよりも前に実行されます。つまり、通常のカスタムValidationRuleであれば個々のBindingでも編集トランザクションを利用することができます。DataErrorValidationRuleのようなValidationStepをRawProposedValueにできないものについては、編集トランザクションを利用することはできないもののCommitEditメソッドやUpdateSourcesメソッドを使って、値の検証、更新を一気に行うことが可能になります。


コードが多くなってしまいましたので、今回のプロジェクトは下記の場所に置いておきます。
ValidationSample_090205.zip


WPFの入力データ検証については今回でおしまいです。次回はSilverlight 2の入力データ検証についてご紹介したいと思います。