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