入力データ検証 その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>
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
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にはいくつかのメソッドが用意されています。このうち、BeginEdit、CommitEdit、CancelEditの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
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の入力データ検証についてご紹介したいと思います。