Monday 11 June 2012

TabControl's TabItem validation Part2

In the previous part we've added some basic functionality to our TabItem validation. There are 2 main scenerios I've found, that add certain difficulty.

First one occurs when we are using DataGrid or its derivative. In DataGrid, when we remove an invalid row or row that has got children in invalid state (for example a validated textbox) despite the "invalid" row deletion, invalid state is retained and will stay, unless we clear all elements in the datagrid and repopulate it.

Second one is related to "revert to previous state" mechanisms often found in solutions that involve accepting and cancelling changes. When we have several TabItems in invalid state and we want to press the "Cancel" button to revert changes we've made, we want all TabItem's to receive information about that change.

To start with, we'll add a helper method, which we've already used in the part1.
IsVisualChild checks, if the control we pass as the second parameter in is places inside of the parent control passed as the first parameter.

Code Snippet
  1. public static bool IsVisualChild(DependencyObject parent, DependencyObject child)
  2. {
  3.     if (parent == null || child == null)
  4.         return false;
  5.  
  6.     bool result = false;
  7.     for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
  8.     {
  9.         var visualChild = VisualTreeHelper.GetChild(parent, i);
  10.         if (child != visualChild)
  11.         {
  12.             result = IsVisualChild(visualChild, child);
  13.             if (result)
  14.                 break;
  15.         }
  16.         else
  17.         {
  18.             result = true;
  19.             break;
  20.         }
  21.     }
  22.     return result;
  23. }

Now we can add the Unloaded event handling method, you have probably already noticed, that in the ValidationOccured method in the Part1 we've added Unloaded method handler to the control that caused validation errors. Unloaded event is fired when the control is being disconnected from the visual object tree.

We need to know of two particular cases - when the validation row is being deleted from a datagrid and when the tab is being switched to another one in the tabcontrol (the tabitem with our control is no longer visible - the control's getting unloaded). When the former happens, we have to remove the control from our elements collection, contrary we should omit removing when the control is not visible due to the parent tab being deselected - that's why in code below we need to check if the control is still a visual child of the tab, before we can sefaly remove it from the erroneous elements list.

Code Snippet
  1. public static void UnloadedEvent(object sender, RoutedEventArgs args)
  2. {
  3.     if (elements.Count > 0)
  4.     {
  5.         var dpSender = elements.Find(p => p.Key == sender);
  6.         if (dpSender.Value != null)
  7.         {
  8.             var parent = dpSender.Value.Content as DependencyObject;
  9.             var child = sender as DependencyObject;
  10.             if (!IsVisualChild(parent, child))
  11.             {
  12.                 if (elements.Count == 1)
  13.                 {
  14.                     elements[0].Value.SetValue(IsTabValidProperty, true);
  15.                 }
  16.                 ((FrameworkElement)sender).Unloaded -= UnloadedEvent;
  17.                 elements.RemoveAll(p => p.Key == sender);
  18.             }
  19.         }
  20.     }
  21. }

Last part is adding changes cancellation support. Different programmers do it differently, in my case my ViewModel has got a Mode property which changes accordingly to the state of properties. When I start changing values in textboxes my ViewModel goes into Edit state. When I cancel changes it goes back to Normal state - and properties go back to their former values (using entity framework in my case). So depending on your implementation you might want to do things differently.
Property, which matches my ViewModel's property Mode:

Code Snippet
  1. public static readonly DependencyProperty DisplayModeProperty =
  2.     DependencyProperty.RegisterAttached("DisplayMode", typeof(DisplayMode), 
  3.     typeof(TabItemValidation),
  4.     new UIPropertyMetadata(DisplayMode.ReadOnly, DisplayModeChanged));

And the DisplayModeChanged callback method. Depending on the mode to which my ViewModel changes to, we can clear elements in our list and switch TabItem to valid state.

Code Snippet
  1. private static void DisplayModeChanged(DependencyObject dobj, DependencyPropertyChangedEventArgs args)
  2. {
  3.     if((DisplayMode)args.NewValue == DisplayMode.ReadOnly &&
  4.         ((DisplayMode)args.OldValue != DisplayMode.ReadOnly))
  5.     {
  6.         foreach (var tabItem in elements.Select(p => p.Value))
  7.         {
  8.             tabItem.SetValue(IsTabValidProperty, true);
  9.         }
  10.         elements.Clear();
  11.     }
  12. }

It's the first this elaborate tutorial I've posted, so I beg your understanding, and I'd be happy to answer to any questions.






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.