Friday 1 June 2012

TabControl's TabItem validation Part1

Recently I needed to add some additional functionality to WPF validation. The basic idea was for tab item's headers to inform user about validation errors inside these tabs. To put it plainly and give you an example, when the user clicks "Create new user", several TabItem's headers highlight with a red outline marking them as containing TextBoxes that need filling. Every validation error inside of the TabItem should propagate and mark that TabItem as erroneous.
This is how it should look:


First thing I did was to check out the standard Validation attached behaviour for anything that might help. The property Validation.ValidationAdornerSite brought my attention. This attached property can be used to set  another control as the one that receives the validation error of the source control. Unfortunately it turned out, that ValidationAdornerSite only redirects the instance of error appearance. Although the TabItem header gets the outline, the control which got validated originally looses it, due to these errors being redirected.

After much brainstorming I got to a conclusion that I need to code something more complex, general-purpose and also comfortable. I though of attached properties, because after proper implementation all I needed was add one property in xaml.

Code Snippet
  1. public static class TabItemValidation
  2. {
  3.     // Dictionary of validated controls along with their parent TabItems
  4.     private static List<KeyValuePair<DependencyObject, TabItem>>
  5.       elements = new List<KeyValuePair<DependencyObject, TabItem>>();
  6.  
  7.     // Activate validation property
  8.     public static readonly DependencyProperty ActivateValidationProperty =
  9.     DependencyProperty.RegisterAttached("ActivateValidation", typeof(bool),
  10.             typeof(TabItemValidation), new                                        UIPropertyMetadata(ValueChanged));

  11.     // Current state of the validation property
  12.     public static readonly DependencyProperty IsTabValidProperty =
  13.             DependencyProperty.RegisterAttached("IsTabValid", typeof(bool),
  14.             typeof(TabItemValidation), new UIPropertyMetadata(true));

Firstly we add a collection that will hold all validated controls and their parents and 2 attached dependency properties, first ActivateValidation allows us to hook up validation to our tab item. Second IsTabValid lets us set proper style for our TabItem or bind to it.  (I'm omitting all the standard code that adding an attached property requires)

Code Snippet
  1. private static void ValueChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
  2. {
  3.     TabItem tabItem = dp as TabItem;
  4.     if (tabItem != null)
  5.     {
  6.         Validation.AddErrorHandler(tabItem, ValidationOccurred);
  7.     }
  8. }

The above is the standard property value changed callback method. In this case, on "ActivateValidation" property changed we add error handler to our tab item. This will cause any error that happens inside the TabItem to call our ValidationOccurred method.
Next method is the one that we registered above and the one that gets called every time there's an error added or removed inside of the TabItem's content.
Code Snippet
  1. public static void ValidationOccurred(object sender,
  2.     ValidationErrorEventArgs args)
  3. {
  4.     TabItem item = sender as TabItem;
  5.     // ValidationSource is the control that caused the validation to occur
  6.     var validationSource = args.OriginalSource as DependencyObject;
  7.     for (int i = elements.Count - 1; i >= 0; i--)
  8.     {
  9.         // Iterate through elements and remove the ones without errors
  10.         if (!Validation.GetHasError(elements[i].Key))
  11.         {
  12.             ((FrameworkElement)elements[i].Key).Unloaded -= UnloadedEvent;
  13.             elements.RemoveAt(i);
  14.         }
  15.         else if (!((FrameworkElement)elements[i].Key).IsEnabled)
  16.         {
  17.             ((FrameworkElement)elements[i].Key).Unloaded -= UnloadedEvent;
  18.             elements.RemoveAt(i);
  19.         }
  20.     }
  21.  
  22.     // If the control is not in the list and has got validation error we
  23.     // add it to the list
  24.     if (!elements.Any(p => p.Key == validationSource) &&
  25.         Validation.GetHasError(validationSource))
  26.     {
  27.         // We add Unloaded item to get the notification when the control
  28.         // leaves the tree
  29.         ((FrameworkElement)validationSource).Unloaded += UnloadedEvent;
  30.         elements.Add(new KeyValuePair<DependencyObject, TabItem>
  31.             (validationSource, (TabItem)sender));
  32.     }
  33.  
  34.     bool anyInvalid = false;
  35.     // We check if any control in the list causes errors and if it is
  36.     // inside our TabItem's content
  37.     for (int i = 0; i < elements.Count; i++)
  38.     {
  39.         if (IsVisualChild(item.Content as DependencyObject,
  40.  
  41.         elements[i].Key) && Validation.GetHasError(elements[i].Key))
  42.         {
  43.             anyInvalid = true;
  44.         }
  45.     }
  46.     // We properly set the IsTabValid property on our TabItem
  47.     if (anyInvalid)
  48.         item.SetValue(IsTabValidProperty, false);
  49.     else
  50.         item.SetValue(IsTabValidProperty, true);
  51. }

What this code does basically is, it reacts to every validation error in the TabItem's content and adds the control that casuses the validation to our list along with its parent TabItem (or removes it, if the event marks the end of the validation). Every single such situation in every TabItem populates our elements list. Afterwards we check if any controls with errors are still left in the elements list and we set the corresponding TabItem's state to IsTabValid = false. The unloaded event will be used in the 2nd part of this article, so you may comment it out for now.

In the next part I'll delve deeper into the meaty details, there are additional precautions to be taken into consideration - especially in scenerios that involve reverting changes.

No comments:

Post a Comment