|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//
// Description:
// Validation-related methods and DependencyProperties
//
// See specs at Validation.mht
//
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Threading;
using MS.Internal.Controls;
using MS.Internal.KnownBoxes;
namespace System.Windows.Controls
{
/// <summary>
/// Validation-related methods and DependencyProperties
/// </summary>
public static class Validation
{
/// <summary>
/// ValidationError event
/// </summary>
public static readonly RoutedEvent ErrorEvent =
EventManager.RegisterRoutedEvent("ValidationError",
RoutingStrategy.Bubble,
typeof(EventHandler<ValidationErrorEventArgs>),
typeof(Validation));
/// <summary>
/// Adds a handler for the ValidationError attached event
/// </summary>
/// <param name="element">UIElement or ContentElement that listens to this event</param>
/// <param name="handler">Event Handler to be added</param>
public static void AddErrorHandler(DependencyObject element, EventHandler<ValidationErrorEventArgs> handler)
{
FrameworkElement.AddHandler(element, ErrorEvent, handler);
}
/// <summary>
/// Removes a handler for the ValidationError attached event
/// </summary>
/// <param name="element">UIElement or ContentElement that listens to this event</param>
/// <param name="handler">Event Handler to be removed</param>
public static void RemoveErrorHandler(DependencyObject element, EventHandler<ValidationErrorEventArgs> handler)
{
FrameworkElement.RemoveHandler(element, ErrorEvent, handler);
}
/// <summary>
/// The key needed to set the publicly read-only ValidationErrors property.
/// </summary>
internal static readonly DependencyPropertyKey ErrorsPropertyKey =
DependencyProperty.RegisterAttachedReadOnly("Errors",
typeof(ReadOnlyObservableCollection<ValidationError>), typeof(Validation),
new FrameworkPropertyMetadata(
ValidationErrorCollection.Empty,
FrameworkPropertyMetadataOptions.NotDataBindable));
/// <summary>
/// ValidationErrors DependencyProperty.
/// holds the list of all active validation errors of any data binding targeting the hosting element.
/// </summary>
/// <remarks>
/// The application cannot modify the content of this collection.
/// </remarks>
public static readonly DependencyProperty ErrorsProperty =
ErrorsPropertyKey.DependencyProperty;
/// <summary> Static accessor for Validation.Errors property </summary>
/// <remarks>
/// The application cannot modify the content of this collection.
/// </remarks>
/// <exception cref="ArgumentNullException"> DependencyObject element cannot be null </exception>
public static ReadOnlyObservableCollection<ValidationError> GetErrors(DependencyObject element)
{
ArgumentNullException.ThrowIfNull(element);
return (ReadOnlyObservableCollection<ValidationError>) element.GetValue(ErrorsProperty);
}
/// <summary>
/// holds the internally modifiable collection of validation errors.
/// </summary>
internal static readonly DependencyProperty ValidationErrorsInternalProperty =
DependencyProperty.RegisterAttached("ErrorsInternal",
typeof(ValidationErrorCollection), typeof(Validation),
new FrameworkPropertyMetadata(
(ValidationErrorCollection)null,
new PropertyChangedCallback(OnErrorsInternalChanged)));
// Update HasErrors and Invalidate the public ValidationErrors property whose GetOverride will return
// the updated value of ValidationErrorsInternal, nicely wrapped into a ReadOnlyCollection<T>
private static void OnErrorsInternalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ValidationErrorCollection newErrors = e.NewValue as ValidationErrorCollection;
if (newErrors != null)
{
d.SetValue(ErrorsPropertyKey, new ReadOnlyObservableCollection<ValidationError>(newErrors));
}
else
{
d.ClearValue(ErrorsPropertyKey);
}
}
internal static ValidationErrorCollection GetErrorsInternal(DependencyObject target)
{
return (ValidationErrorCollection) target.GetValue(Validation.ValidationErrorsInternalProperty);
}
/// <summary>
/// The key needed set a read-only property.
/// </summary>
internal static readonly DependencyPropertyKey HasErrorPropertyKey =
DependencyProperty.RegisterAttachedReadOnly("HasError",
typeof(bool), typeof(Validation),
new FrameworkPropertyMetadata(
BooleanBoxes.FalseBox,
FrameworkPropertyMetadataOptions.NotDataBindable,
OnHasErrorChanged));
// This is a workaround to notify the Control because if we try to override
// metadata to have the control hook it's own property change handler
// it introduces a strange ordering of static constructors when not ngened.
private static void OnHasErrorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Control control = d as Control;
if (control != null)
{
Control.OnVisualStatePropertyChanged(control, e);
}
}
/// <summary>
/// HasError DependencyProperty is true if any binding on the target element
/// has a validation error.
/// </summary>
public static readonly DependencyProperty HasErrorProperty=
HasErrorPropertyKey.DependencyProperty;
/// <summary> Static accessor for HasError property </summary>
/// <exception cref="ArgumentNullException"> DependencyObject element cannot be null </exception>
public static bool GetHasError(DependencyObject element)
{
ArgumentNullException.ThrowIfNull(element);
return (bool) element.GetValue(HasErrorProperty);
}
/// <summary>
/// Template used to generate validation error feedback on the AdornerLayer. Default
/// Template is:
/// <code>
/// <Border BorderThickness="1" BorderBrush="Red">
/// <AdornedElementPlaceholder/>
/// </Border>
/// </code>
/// </summary>
public static readonly DependencyProperty ErrorTemplateProperty =
DependencyProperty.RegisterAttached("ErrorTemplate",
typeof(ControlTemplate), typeof(Validation),
new FrameworkPropertyMetadata(
CreateDefaultErrorTemplate(),
FrameworkPropertyMetadataOptions.NotDataBindable,
new PropertyChangedCallback(OnErrorTemplateChanged)));
/// <summary> Static accessor for ErrorTemplate property </summary>
/// <exception cref="ArgumentNullException"> DependencyObject element cannot be null </exception>
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
public static ControlTemplate GetErrorTemplate(DependencyObject element)
{
ArgumentNullException.ThrowIfNull(element);
return element.GetValue(ErrorTemplateProperty) as ControlTemplate;
}
/// <summary> Static modifier for ErrorTemplate property </summary>
/// <exception cref="ArgumentNullException"> DependencyObject element cannot be null </exception>
public static void SetErrorTemplate(DependencyObject element, ControlTemplate value)
{
ArgumentNullException.ThrowIfNull(element);
// (perf) don't set if the existing value is already correct
object oldValue = element.ReadLocalValue(ErrorTemplateProperty);
if (!Object.Equals(oldValue, value))
element.SetValue(ErrorTemplateProperty, value);
}
// when ErrorTemplate changes, redraw the currently visible adorner
private static void OnErrorTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (GetHasError(d))
{
ShowValidationAdorner(d, false);
ShowValidationAdorner(d, true);
}
}
/// <summary>
/// Designates the alternative element to which validation feedback
/// should be directed.
/// </summary>
public static readonly DependencyProperty ValidationAdornerSiteProperty =
DependencyProperty.RegisterAttached("ValidationAdornerSite",
typeof(DependencyObject), typeof(Validation),
new FrameworkPropertyMetadata((DependencyObject)null,
new PropertyChangedCallback(OnValidationAdornerSiteChanged)));
/// <summary> Static accessor for ValidationAdornerSite property </summary>
/// <exception cref="ArgumentNullException"> DependencyObject element cannot be null </exception>
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
public static DependencyObject GetValidationAdornerSite(DependencyObject element)
{
ArgumentNullException.ThrowIfNull(element);
return element.GetValue(ValidationAdornerSiteProperty) as DependencyObject;
}
/// <summary> Static modifier for ValidationAdornerSite property </summary>
/// <exception cref="ArgumentNullException"> DependencyObject element cannot be null </exception>
public static void SetValidationAdornerSite(DependencyObject element, DependencyObject value)
{
ArgumentNullException.ThrowIfNull(element);
element.SetValue(ValidationAdornerSiteProperty, value);
}
// when Site property changes, update the SiteFor property on the other end
private static void OnValidationAdornerSiteChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// ignore SubPropertyChange - we don't care about properties on the site
if (e.IsASubPropertyChange)
return;
DependencyObject oldSite = (DependencyObject)e.OldValue;
DependencyObject newSite = (DependencyObject)e.NewValue;
if (oldSite != null)
{
oldSite.ClearValue(ValidationAdornerSiteForProperty);
}
if (newSite != null)
{
if (d != GetValidationAdornerSiteFor(newSite))
{
SetValidationAdornerSiteFor(newSite, d);
}
}
// if the adorner is currently visible, move it to the new site
if (GetHasError(d))
{
if (oldSite == null)
{
oldSite = d;
}
ShowValidationAdornerHelper(d, oldSite, false);
ShowValidationAdorner(d, true);
}
}
/// <summary>
/// Designates the element for which the current element should serve
/// as the ValidationAdornerSite.
/// </summary>
public static readonly DependencyProperty ValidationAdornerSiteForProperty =
DependencyProperty.RegisterAttached("ValidationAdornerSiteFor",
typeof(DependencyObject), typeof(Validation),
new FrameworkPropertyMetadata((DependencyObject)null,
new PropertyChangedCallback(OnValidationAdornerSiteForChanged)));
/// <summary> Static accessor for ValidationAdornerSiteFor property </summary>
/// <exception cref="ArgumentNullException"> DependencyObject element cannot be null </exception>
[AttachedPropertyBrowsableForType(typeof(DependencyObject))]
public static DependencyObject GetValidationAdornerSiteFor(DependencyObject element)
{
ArgumentNullException.ThrowIfNull(element);
return element.GetValue(ValidationAdornerSiteForProperty) as DependencyObject;
}
/// <summary> Static modifier for ValidationAdornerSiteFor property </summary>
/// <exception cref="ArgumentNullException"> DependencyObject element cannot be null </exception>
public static void SetValidationAdornerSiteFor(DependencyObject element, DependencyObject value)
{
ArgumentNullException.ThrowIfNull(element);
element.SetValue(ValidationAdornerSiteForProperty, value);
}
// when SiteFor property changes, update the Site property on the other end
private static void OnValidationAdornerSiteForChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// ignore SubPropertyChange - we don't care about properties on the siteFor
if (e.IsASubPropertyChange)
return;
DependencyObject oldSiteFor = (DependencyObject)e.OldValue;
DependencyObject newSiteFor = (DependencyObject)e.NewValue;
if (oldSiteFor != null)
{
oldSiteFor.ClearValue(ValidationAdornerSiteProperty);
}
if (newSiteFor != null)
{
if (d != GetValidationAdornerSite(newSiteFor))
{
SetValidationAdornerSite(newSiteFor, d);
}
}
}
internal static void ShowValidationAdorner(DependencyObject targetElement, bool show)
{
// If the element has a VisualStateGroup for validation, then dont show the Adorner
// because the control will handle visualizing the error via VSM states.
if (!HasValidationGroup(targetElement as FrameworkElement))
{
// redirect the adorner to the designated site, if any
DependencyObject adornerSite = GetValidationAdornerSite(targetElement);
if (adornerSite == null)
{
adornerSite = targetElement;
}
ShowValidationAdornerHelper(targetElement, adornerSite, show);
}
}
private static bool HasValidationGroup(FrameworkElement fe)
{
if (fe != null)
{
IList<VisualStateGroup> groups = VisualStateManager.GetVisualStateGroupsInternal(fe);
// the Validation group could be on either the FE or it's StateGroupRoot
if (HasValidationGroup(groups))
{
return true;
}
if (fe.StateGroupsRoot != null)
{
groups = VisualStateManager.GetVisualStateGroupsInternal(fe.StateGroupsRoot);
return HasValidationGroup(groups);
}
}
return false;
}
private static bool HasValidationGroup(IList<VisualStateGroup> groups)
{
if (groups != null)
{
for (int groupIndex = 0; groupIndex < groups.Count; ++groupIndex)
{
VisualStateGroup g = groups[groupIndex];
if (g.Name == VisualStates.GroupValidation)
{
return true;
}
}
}
return false;
}
private static void ShowValidationAdornerHelper(DependencyObject targetElement, DependencyObject adornerSite, bool show)
{
ShowValidationAdornerHelper(targetElement, adornerSite, show, true);
}
private static object ShowValidationAdornerOperation(object arg)
{
object[] args = (object[])arg;
DependencyObject targetElement = (DependencyObject)args[0];
DependencyObject adornerSite = (DependencyObject)args[1];
bool show = (bool)args[2];
// Check if the element is visible, if not try to show the adorner again once it gets visible.
// This is needed because controls hosted in Expander or TabControl don't have a parent/AdornerLayer till the Expander is expanded or the TabItem is selected.
if (adornerSite is UIElement { IsVisible: false } siteUIElement)
{
siteUIElement.IsVisibleChanged += ShowValidationAdornerWhenAdornerSiteGetsVisible;
}
else
{
ShowValidationAdornerHelper(targetElement, adornerSite, show, false);
}
return null;
}
private static void ShowValidationAdornerWhenAdornerSiteGetsVisible(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is not UIElement adornerSite)
{
return;
}
adornerSite.IsVisibleChanged -= ShowValidationAdornerWhenAdornerSiteGetsVisible;
DependencyObject targetElement = GetValidationAdornerSiteFor(adornerSite);
if (targetElement == null)
{
targetElement = adornerSite;
}
ShowValidationAdornerHelper(targetElement, adornerSite, (bool)e.NewValue && GetHasError(targetElement), false);
}
private static void ShowValidationAdornerHelper(DependencyObject targetElement, DependencyObject adornerSite, bool show, bool tryAgain)
{
if (adornerSite is not UIElement siteUIElement)
{
return;
}
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(siteUIElement);
if (adornerLayer is null)
{
if (tryAgain)
{
// try again later, perhaps giving layout a chance to create the adorner layer
adornerSite.Dispatcher.BeginInvoke(DispatcherPriority.Loaded,
new DispatcherOperationCallback(ShowValidationAdornerOperation),
new object[] { targetElement, adornerSite, BooleanBoxes.Box(show) });
}
return;
}
TemplatedAdorner validationAdorner = siteUIElement.ReadLocalValue(ValidationAdornerProperty) as TemplatedAdorner;
if (show && validationAdorner is null)
{
// get the template from the site, or from the target element
ControlTemplate validationTemplate = GetErrorTemplate(siteUIElement);
if (validationTemplate is null)
{
validationTemplate = GetErrorTemplate(targetElement);
}
if (validationTemplate is not null)
{
validationAdorner = new TemplatedAdorner(siteUIElement, validationTemplate);
adornerLayer.Add(validationAdorner);
siteUIElement.SetValue(ValidationAdornerProperty, validationAdorner);
}
}
else if (!show && validationAdorner is not null)
{
validationAdorner.ClearChild();
adornerLayer.Remove(validationAdorner);
siteUIElement.ClearValue(ValidationAdornerProperty);
}
}
/// <summary>
/// Mark this BindingExpression as invalid. If the BindingExpression has been
/// explicitly marked invalid in this way, then it will remain
/// invalid until ClearInvalid is called or another transfer to the source validates successfully.
/// </summary>
public static void MarkInvalid(BindingExpressionBase bindingExpression, ValidationError validationError)
{
ArgumentNullException.ThrowIfNull(bindingExpression);
ArgumentNullException.ThrowIfNull(validationError);
bindingExpression.UpdateValidationError(validationError);
}
/// <summary>
/// Clears the ValidationError that was set through a call
/// to MarkInvalid or a previously failed validation of that BindingExpression.
/// </summary>
public static void ClearInvalid(BindingExpressionBase bindingExpression)
{
ArgumentNullException.ThrowIfNull(bindingExpression);
bindingExpression.UpdateValidationError(null);
}
// add a validation error to the given element
internal static void AddValidationError(ValidationError validationError, DependencyObject targetElement, bool shouldRaiseEvent)
{
if (targetElement == null)
return;
bool wasValid;
ValidationErrorCollection validationErrors = GetErrorsInternal(targetElement);
if (validationErrors == null)
{
wasValid = true;
validationErrors = new ValidationErrorCollection();
validationErrors.Add(validationError);
targetElement.SetValue(Validation.ValidationErrorsInternalProperty, validationErrors);
}
else
{
wasValid = (validationErrors.Count == 0);
validationErrors.Add(validationError);
}
if (wasValid)
{
targetElement.SetValue(HasErrorPropertyKey, BooleanBoxes.TrueBox);
}
if (shouldRaiseEvent)
{
OnValidationError(targetElement, validationError, ValidationErrorEventAction.Added);
}
if (wasValid)
{
ShowValidationAdorner(targetElement, true);
}
}
// remove a validation error from the given element
internal static void RemoveValidationError(ValidationError validationError, DependencyObject targetElement, bool shouldRaiseEvent)
{
if (targetElement == null)
return;
ValidationErrorCollection validationErrors = GetErrorsInternal(targetElement);
if (validationErrors == null || validationErrors.Count == 0 || !validationErrors.Contains(validationError))
return;
bool isValid = (validationErrors.Count == 1); // about to remove the last error
if (isValid)
{
// instead of removing the last error, just discard the error collection.
// This sends out only one property-change event, instead of two.
// Any bindings to Errors[x] will appreciate the economy.
targetElement.ClearValue(HasErrorPropertyKey);
targetElement.ClearValue(ValidationErrorsInternalProperty);
if (shouldRaiseEvent)
{
OnValidationError(targetElement, validationError, ValidationErrorEventAction.Removed);
}
ShowValidationAdorner(targetElement, false);
}
else
{
// if it's not the last error, just remove it.
validationErrors.Remove(validationError);
if (shouldRaiseEvent)
{
OnValidationError(targetElement, validationError, ValidationErrorEventAction.Removed);
}
}
}
static void OnValidationError(DependencyObject source, ValidationError validationError, ValidationErrorEventAction action)
{
ValidationErrorEventArgs args = new ValidationErrorEventArgs(validationError, action);
if (source is ContentElement)
((ContentElement)source).RaiseEvent(args);
else if (source is UIElement)
((UIElement)source).RaiseEvent(args);
else if (source is UIElement3D)
((UIElement3D)source).RaiseEvent(args);
}
private static ControlTemplate CreateDefaultErrorTemplate()
{
ControlTemplate defaultTemplate = new ControlTemplate(typeof(Control));
//<Border BorderThickness="1" BorderBrush="Red">
// <AdornedElementPlaceholder/>
//</Border>
FrameworkElementFactory border = new FrameworkElementFactory(typeof(Border), "Border");
border.SetValue(Border.BorderBrushProperty, Brushes.Red);
border.SetValue(Border.BorderThicknessProperty, new Thickness(1));
FrameworkElementFactory adornedElementPlaceHolder = new FrameworkElementFactory(typeof(AdornedElementPlaceholder), "Placeholder");
border.AppendChild(adornedElementPlaceHolder);
defaultTemplate.VisualTree = border;
defaultTemplate.Seal();
return defaultTemplate;
}
/// <summary>
/// Reference to the ValidationAdorner
/// </summary>
private static readonly DependencyProperty ValidationAdornerProperty =
DependencyProperty.RegisterAttached("ValidationAdorner",
typeof(TemplatedAdorner), typeof(Validation),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.NotDataBindable));
}
}
|