The default validation does not cater for this scenario, but with the help of an attached property I was able to come up with something that works and is reusable.
First, the new class
public class ValidationEx
{
public static bool GetAttach(DependencyObject obj)
{
return (bool)obj.GetValue(AttachProperty);
}
public static void SetAttach(DependencyObject obj, bool value)
{
obj.SetValue(AttachProperty, value);
}
// Using a DependencyProperty as the backing store for Attach. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AttachProperty =
DependencyProperty.RegisterAttached("Attach", typeof(bool), typeof(ValidationEx), new FrameworkPropertyMetadata(false, AttachChanged));
private static void AttachChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is FrameworkElement)
{
var fe = sender as FrameworkElement;
if (((bool)e.NewValue))
{
fe.SetValue(Validation.ErrorTemplateProperty, Application.Current.Resources["LimitedValidationErrorTemplate"]);
fe.IsKeyboardFocusWithinChanged += KeyboardFocusWithinChanged;
fe.SizeChanged += ElementSizeChanged;
fe.LayoutUpdated += ElementLayoutUpdated;
}
else
{
fe.SetValue(Validation.ErrorTemplateProperty, null);
fe.IsKeyboardFocusWithinChanged -= KeyboardFocusWithinChanged;
fe.SizeChanged -= ElementSizeChanged;
fe.LayoutUpdated -= ElementLayoutUpdated;
}
}
}
static void ElementLayoutUpdated(object sender, EventArgs e)
{
var fe = sender as FrameworkElement;
if (fe != null && (bool)fe.GetValue(ValidationEx.AttachProperty))
{
UpdatePopupPosition(fe);
}
}
public static string GetAttachWithTemplate(DependencyObject obj)
{
return (string)obj.GetValue(AttachWithTemplateProperty);
}
public static void SetAttachWithTemplate(DependencyObject obj, string value)
{
obj.SetValue(AttachWithTemplateProperty, value);
}
// Using a DependencyProperty as the backing store for AttachWithTemplate. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AttachWithTemplateProperty =
DependencyProperty.RegisterAttached("AttachWithTemplate", typeof(string), typeof(ValidationEx), new FrameworkPropertyMetadata("", AttachWithTemplatechanged));
private static void AttachWithTemplatechanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is FrameworkElement)
{
var fe = sender as FrameworkElement;
var template = Application.Current.Resources[e.NewValue.ToString()];
fe.SetValue(Validation.ErrorTemplateProperty, template);
fe.IsKeyboardFocusWithinChanged -= KeyboardFocusWithinChanged;
fe.SizeChanged -= ElementSizeChanged;
fe.LayoutUpdated -= ElementLayoutUpdated;
fe.IsKeyboardFocusWithinChanged += KeyboardFocusWithinChanged;
fe.SizeChanged += ElementSizeChanged;
fe.LayoutUpdated += ElementLayoutUpdated;
}
}
static void ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var fe = Keyboard.FocusedElement as FrameworkElement;
if (fe != null && (bool)fe.GetValue(ValidationEx.AttachProperty))
{
UpdatePopupPosition(fe);
}
}
private static ScrollViewer GetScrollViewer(FrameworkElement fe)
{
var parent = VisualTreeHelper.GetParent(fe);
while (parent != null && !(parent is Window))
{
if (parent is ScrollViewer)
return parent as ScrollViewer;
if (parent is Border)
{
var child = ((Border)parent).Child;
if (child is ScrollViewer)
return child as ScrollViewer;
}
parent = VisualTreeHelper.GetParent(parent);
}
return null;
}
static void ElementSizeChanged(object sender, SizeChangedEventArgs e)
{
FrameworkElement fe = sender as FrameworkElement;
if (fe != null && fe.IsKeyboardFocusWithin)
{
UpdatePopupPosition(fe);
}
}
private static void UpdatePopupPosition(FrameworkElement fe)
{
KeyboardFocusWithinChanged(fe, new DependencyPropertyChangedEventArgs(FrameworkElement.IsKeyboardFocusWithinProperty, true, false));
KeyboardFocusWithinChanged(fe, new DependencyPropertyChangedEventArgs(FrameworkElement.IsKeyboardFocusWithinProperty, false, true));
}
private static void KeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
{
FrameworkElement fe = sender as FrameworkElement;
if (fe != null)
{
var scrollViewer = GetScrollViewer(fe);
if (((bool)e.NewValue))
{
var value = fe.GetValue(Validation.ErrorTemplateProperty);
var resourceKey = fe.GetValue(ValidationEx.AttachWithTemplateProperty).ToString();
if (string.IsNullOrWhiteSpace(resourceKey))
resourceKey = "ValidationErrorTemplate";
var template = Application.Current.Resources[resourceKey];
fe.SetValue(Validation.ErrorTemplateProperty, template);
if (scrollViewer != null)
{
scrollViewer.ScrollChanged -= ScrollChanged;
scrollViewer.ScrollChanged += ScrollChanged;
}
}
else
{
fe.SetValue(Validation.ErrorTemplateProperty, Application.Current.Resources["LimitedValidationErrorTemplate"]);
if (scrollViewer != null)
{
scrollViewer.ScrollChanged -= ScrollChanged;
}
}
}
}
}
This makes available two attached properties.
Attach – which triggers the code to use custom templates
AttachWithTemplate – which allows you to specify a custom validation template key.
So, the above code specifies the validation template to use when the control does not have focus. When it or anyone of its children gets focus, the validation template is changed to reflect the different UI requirement.
The Resource Dictionary
Of course, nothing will look any different if we don’t code up the templates. So, below are some basic templates to use.To use the validation extension I have a different resource dictionary where I've specified various settings. So, for a Textbox I've set a global style to hook up the validation extension.
So to visualize this, here are a few screenshots to help.
Here, the last name field is not focused, so the last name validation shows a bar at the side.
Hovering over bar shows the details of the error
Once focused, the bar is replaced with a custom validation message which will display the current error.

0 comments:
Post a Comment