WPF BindingGroup and Validation Feedback
Recap
In a previous set of posts I covered the introduction to the BindingGroup feature and I expanded in a following post with an example of using BindingGroup and IEditableCollectionView together. Last thing on the BindingGroup list to discuss is the additional validation feedback options with item-level validation.
Motivation
For item-level validation, the target element is often a container of some kind, such as a ListViewItem, Panel, or form. Some common scenarios for showing item-level validation feedback are possibly a ListViewItem wanting to show an exclamation mark in a particular column or a panel wanting to display error information in a dedicated child element.
Validation Feedback
Two new APIs have been added to the Validation class to enable feedback for BindingGroups. The APIs include Validation.ValidationAdornerSite and Validation.ValidationAdornerSiteFor. These attached properties work together to point to the element that will show the validation error. The remarks in MSDN describe both attached properties quite well actually so I’m not going to describe them again here. What I will do is go through some different usage scenarios.
In the BindingGroups and IEditableCollectionView post, I used standard validation that existed before SP1. I placed it on the entire row item. That is one option that you have to display validation feedback. I list it here for reference.
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Foreground="Red"
FontSize="14" FontWeight="Bold">*</TextBlock>
<Border BorderBrush="Red" BorderThickness="2">
<AdornedElementPlaceholder Name="adornerPlaceholder" />
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent,
Converter={StaticResource ItemErrorConverter}}"/>
</Trigger>
</Style.Triggers>
</Style>
Another way that you may want to display validation errors is through a dedicated area in your UI. Let’s say that you have a TextBlock or ListBox specifically for validation errors. You can design it in a couple ways using the new Validation APIs. One way, you can set Validation.ValidationAdornerSiteFor on an error ListBox to point to the element that is being validated (which in this case is the ListViewItem). Here is an example:
<!--Validation Feedback-->
<StackPanel Grid.Row="3">
<!--set the DataContext to the itemsList ListViewItem-->
<StackPanel.DataContext>
<MultiBinding Converter="{StaticResource ValidationAdornerConverter}">
<Binding ElementName="itemsList" Path="."/>
<Binding ElementName="itemsList" Path="ItemsSource.CurrentItem" />
</MultiBinding>
</StackPanel.DataContext>
<TextBlock Text="Errors: " />
<ListBox MinHeight="30"
Validation.ValidationAdornerSiteFor="{Binding}"
ItemsSource="{Binding Path=(Validation.Errors)}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=ErrorContent}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
I had to use a MultiBinding to get the current ListViewItem visual to set as the DataContext for the error ListBox. As you can see, there is a bit of work involved taking this route. The next approach I am going to discuss uses less code and may be a better approach for lists. But there are other scenarios where this particular approach is more convenient. The reason it is a bit involved in the first place is because you are dealing with a list of items and you need to find the correct item. If you were dealing with a form instead of a ListView all you have to do is set the ValidationAdornerSiteFor on the form element instead of having to find it in a converter. That is just something to keep in mind.
The second approach that you can do is set the Validation.ValidationAdornerSite on the ListViewItem to point to the error ListBox. Here is an initial implementation:
<!—WARNING: Setting ValidationAdornerSite this way may have an undesired side effect-->
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="Validation.ValidationAdornerSite"
Value="{Binding ElementName=lb_ErrorList}" />
</Style>
<!--Validation Feedback-->
<StackPanel Grid.Row="3">
<TextBlock Text="Errors: " />
<ListBox Name="lb_ErrorList" MinHeight="30"
ItemsSource="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.ValidationAdornerSiteFor).(Validation.Errors)}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=ErrorContent}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
By setting Validation.ValidationAdornerSite on the ListViewItem, lb_ErrorList now has access to the ListViewItem through Validation.ValidationAdornerSiteFor on itself. There is one problem with the above code however. Validation.ValidationAdornerSite is a static property and therefore will only exist as a single entity. When each ListViewItem attaches to Validation.ValidationAdornerSite, the value of Validation.ValidationAdornerSite will end up being the last ListViewItem that attached to it. What this means is that anytime there is a validation error, lb_ErrorList will check the last ListViewItem for Validation.Errors (instead of the current ListViewItem that is being edited). To correct this, you can update Validation.ValidationAdornerSite dynamically. A possible implementation for this is to remove the Style set on the ListViewItem and attach to the Validation.Error event. In this event, you can set Validation.ValidationAdornerSite like so:
<ListView Name="itemsList" Validation.Error="itemsList_Error" … />
private void itemsList_Error(object sender, ValidationErrorEventArgs e)
{
Validation.SetValidationAdornerSite((DependencyObject)e.OriginalSource, this.lb_ErrorList);
}
Now when a validation error occurs, lb_ErrorList will check the correct ListViewItem for validation errors. I have the full code for this solution attached.
One last scenario that I want to cover is showing feedback on a particular property within an item. Let’s say as the item-level validation occurs a failure occurs between two properties and you want to show an error on the specific property. The approach for this one is very similar to the second approach I discussed. In my example I use the column header to show any validation error feedback and it is in the Validation.Error event where I set the ValidationAdornerSite to the correct column header. Here is a snippet of the template for the GridViewColumnHeader and the full code you can see from the solution attached.
<Style TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GridViewColumnHeader}" >
<StackPanel Orientation="Horizontal">
<ContentPresenter />
<TextBlock Text="*" Foreground="Red">
<TextBlock.Visibility>
<MultiBinding Converter="{StaticResource ErrorVisibilityConverter}">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type GridViewColumnHeader}}" Path="(Validation.ValidationAdornerSiteFor).(Validation.HasError)"/>
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type GridViewColumnHeader}}" Path="(Validation.HasError)" />
</MultiBinding>
</TextBlock.Visibility>
</TextBlock>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.ValidationAdornerSiteFor).(Validation.Errors)[0].ErrorContent}" />
</Style>
Download the complete solution here.
BindingGroupSample_IECV_Validation.zip
Comments
Anonymous
September 08, 2008
PingBack from http://blogs.msdn.com/vinsibal/archive/2008/08/22/bindinggroups-and-ieditablecollectionview.aspxAnonymous
September 09, 2008
The comment has been removedAnonymous
September 09, 2008
wonderliza, Could you give me the exact repro steps that is giving you this error. Since it is with BoatRentalCustomers it shouldn't be a db problem.Anonymous
September 09, 2008
The comment has been removedAnonymous
September 11, 2008
Stéphane Goudeau m'a remonté un lien ce matin : Un nouveau billet vient de sortir, qui présente une nouvelleAnonymous
September 14, 2008
hi, so, am I the only one that can't run this sample ?Anonymous
September 15, 2008
I've tried on both Vista and XP and didn't run into your issue. Do you have SP1 installed?Anonymous
September 16, 2008
yes, i do have SP1 installed in fact. Weird...Anonymous
September 23, 2008
The comment has been removedAnonymous
September 24, 2008
Sergei Gaidukov, Thanks a lot for your help. I've updated the solution accordingly.Anonymous
September 15, 2009
Hi there, i wonder if you could help me out with a question of mine. I have a usercontrol that has a border and a Image as its child. If the image source hasnt been set by the app user i set an error via the IDataErrorInfo interface in my viewmodel. My problem is now how i can update the sourrounding borders style when this happens. I have tried to play around with the Validation.ValidationAdornerSiteFor property but with no luck. Below you can see my "attempt". Could you point me in direction how to accomplish this, to update another control that didnt fire the errors style? <UserControl x:Class="Admin.Thumbnail" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <UserControl.Resources> <!-- Style setting the border as validation adorner site. --> <Style x:Key="ThumbnailStyle" TargetType="{x:Type Image}"> <Setter Property="Validation.ValidationAdornerSite" Value="{Binding ElementName=theBorder}" /> </Style> <!-- This style checks if any errors have been set and should change the border color. --> <Style x:Key="ErrorAd" TargetType="{x:Type Border}"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="false"> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" /> </Trigger> </Style.Triggers> </Style> </UserControl.Resources> <DockPanel> <Border Name="theBorder" BorderThickness="1" Background="Red" BorderBrush="Black" Validation.ValidationAdornerSiteFor="{Binding ElementName=imageMain}" Style="{DynamicResource ErrorAd}"> <Image x:Name="imageMain" HorizontalAlignment="Center" VerticalAlignment="Center" Source="{Binding Path=Source, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True,NotifyOnValidationError=True}" /> </Border> </DockPanel> </UserControl>