WPF: Validation Summary Control


While writing my previous post on WPF Validation, I came to thinking about a Validation Summary, like the one in ASP.NET. Why isn’t there one? However, WPF has a pretty cool feature that lets you bind and display your validation error to another control (e.g. a TextBlock), via the Validation.ValidationAdornerSite Attached Property. This is very convenient, except for the fact that it’s a one to one relationship; I rather be able to bind all my validation errors to one single control, like the ASP Validation Summary. So I have created a custom control to do just that.

ValidationSummary

Firstly I followed the approach of Validation.ValidationAdornerSite, and created a static class with an attached dependency property. By setting that property, you can hook up your controls (those that require validation) to the Validation Summary control and it will listen for validation errors being raised and display them. This is a snippet of the static class.

public static class ValidationSummaryValidator
{
    public static DependencyProperty AdornerSiteProperty =
        DependencyProperty.RegisterAttached("AdornerSite", typeof(DependencyObject), typeof(ValidationSummaryValidator),
        new PropertyMetadata(null, OnAdornerSiteChanged));

    [AttachedPropertyBrowsableForType(typeof(DependencyObject))]
    public static DependencyObject GetAdornerSite(DependencyObject element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }
        return (element.GetValue(AdornerSiteProperty) as DependencyObject);
    }

    public static void SetAdornerSite(DependencyObject element, DependencyObject value)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }
        element.SetValue(AdornerSiteProperty, value);
    }

    private static void OnAdornerSiteChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        IErrorViewer errorViewer = e.NewValue as IErrorViewer;
        if (errorViewer != null)
        {
            errorViewer.SetElement(d);
        }
    }
}

Next I created a custom control that consist of an ItemsControl to display my error messages. Here’s the Xaml for that.

<UserControl.Resources>
 <DataTemplate x:Key="ErrorViewerItemTemplate" DataType="string" >
     <StackPanel Orientation="Horizontal">
         <Ellipse Fill="Red" Width="5" Height="5" VerticalAlignment="Center"
             HorizontalAlignment="Center" Margin="5,0,0,0" />
         <TextBlock Text="{Binding}" FontSize="11" FontStyle="Italic"
            Foreground="red" Padding="2" Margin="5,0,0,0"
             HorizontalAlignment="Left" VerticalAlignment="Center" />
     </StackPanel>
 </DataTemplate>
</UserControl.Resources>

<ItemsControl x:Name="itemsControl" 
    ItemTemplate="{StaticResource ErrorViewerItemTemplate}"  />

Here’s a snippet of the code behind for the control, and the IErrorViewer interface which provides the method for setting an element to listen to.

public interface IErrorViewer
{
    void SetElement(DependencyObject element);
}

public partial class ValidationSummary : UserControl, IErrorViewer
{
    private List<dependencyobject> _elements = new List</dependencyobject><dependencyobject>();
    private ObservableCollection<string> _errorMessages = new ObservableCollection</string><string>();

    public ValidationSummary()
    {
        InitializeComponent();
        this.Loaded += new RoutedEventHandler(ErrorViewer_Loaded);
    }

    void ErrorViewer_Loaded(object sender, RoutedEventArgs e)
    {
        itemsControl.ItemsSource = _errorMessages;
    }

    private void Element_ValidationError(object sender, ValidationErrorEventArgs e)
    {
        if (e.Action == ValidationErrorEventAction.Added && !_errorMessages.Contains(e.Error.ErrorContent.ToString()))
        {
            _errorMessages.Add(e.Error.ErrorContent.ToString());
        }
        else if (e.Action == ValidationErrorEventAction.Removed && _errorMessages.Contains(e.Error.ErrorContent.ToString()))
        {
            _errorMessages.Remove(e.Error.ErrorContent.ToString());
        }
    }

    #region IErrorViewer Members

    public void SetElement(DependencyObject element)
    {
        if (!_elements.Contains(element))
        {
            _elements.Add(element);
            Validation.AddErrorHandler(element, Element_ValidationError);
        }
    }

    #endregion
}

Now that all this is set up, all we need is to bind our input controls to the Validation Summary control on our UI by using the static ValidationSummaryValidator.AdornerSite Attached Property. This is set in our ErrorTemplate, and below is a snippet of the Xaml.

<Style TargetType="{x:Type TextBox}">
  <...>
  <Setter Property="lib:ValidationSummaryValidator.AdornerSite" 
      Value="{Binding ElementName=validationSummary}" />
  <...>
</Style>

I’ve omitted some of the code for readability, so if you’re keen to know more, do download the solution and have a look. With my limited knowledge this is my version of a Validation Summary. I’m not entirely convinced that this is the best approach, so if someone has a better version, do let me know.

Download this sample here.

Share this post:

About these ads
Posted in WPF. 5 Comments »

5 Responses to “WPF: Validation Summary Control”

  1. DL Says:

    Pretty nice solution, but have you tried it under .NET 4? I’m finding that it works fine under 3.5 but (amazingly enough) not under 4. The problem appears to be the order in which ValidationSummary.Element_ValidationError() method is called. Under .NET 3.5 when you change the value inside the textbox such that it is in an invalid state, first Element_ValidationError() gets called with ValidationErrorEventAction.Removed then it gets called with ValidationErrorEventAction.Added. This results in the ItemsControl having an error message in it. However, under .NET 4, first Element_ValidationError gets called with ValidationErrorEventAction.Added then it gets called with ValidationErrorEventAction.Removed, resulting in no error message being displayed. It’s hard to believe that they would have changed this type of thing unless it’s a bug. Can you control the order in which the calls happen?

  2. DL Says:

    After tinkering around with this problem, I can’t find an explanation as to why the change has occurred or if there is a way to control how the events are called. However, one work around would be to simply always clear out the list of errors and walk the element tree looking for elements in an error state every time Element_ValidationError() is called. Something like this appears to work (this is inside ValidationSummary.xaml.cs):

    private void Element_ValidationError(object sender, ValidationErrorEventArgs e)
    {
    _errorMessages.Clear();
    GetErrorsRecursive(Window.GetWindow(this), _errorMessages);
    }

    private void GetErrorsRecursive(FrameworkElement elem, ObservableCollection errors)
    {
    if (Validation.GetHasError(elem))
    foreach (ValidationError error in Validation.GetErrors(elem))
    errors.Add(error.ErrorContent.ToString());

    foreach (object child in LogicalTreeHelper.GetChildren(elem))
    if (child is FrameworkElement)
    GetErrorsRecursive(child as FrameworkElement, errors);
    }

    There are still other differences I’m noticing between 3.5 and 4.0 (like initially all the fields are marked as invalid even though no data entry has occurred), but hopefully those won’t too hard to overcome.

    • Ed Foh Says:

      Hi DL

      sorry I haven’t had time to look at this. Seems you have found a workaround, which is great. Agree that it would be strange that 3.5 and 4.0 would have caused such a change. Will investigate :)

  3. Tom Pester Says:

    Thanks for sharing your code. It works very nice here :) (using 3.5SP1)


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 97 other followers

%d bloggers like this: