Wouldn’t it be great if you could do this in XAML:
1 2 | <TextBox Text="{Binding Path=IngredientName}" Validation.Error="{Binding Path=IngredientNameError}"/> |
Turns out you can:
1 2 | <TextBox Text="{Binding Path=IngredientName}" vh:ValidationHelper.Error="{Binding Path=IngredientNameError}"/> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Data; namespace Pelebyte.ValidationUtils { /// <summary> /// Provides validation helper methods and utilities. /// </summary> public static class ValidationHelper { /// <summary> /// The <see cref="DependencyProperty"/> that identifies the /// <see cref="P:ValidationHelper.Error"/> attached property. /// </summary> public static readonly DependencyProperty ErrorProperty = DependencyProperty.RegisterAttached( "Error", typeof(string), typeof(ValidationHelper), new PropertyMetadata(null, OnErrorChanged)); /// <summary> /// Gets the custom error applied to the control. /// </summary> /// <param name="d"> /// The control to get the current custom error for. /// </param> /// <returns> /// A string that represents the custom error. /// </returns> public static string GetError(DependencyObject d) { return (string)d.GetValue(ErrorProperty); } /// <summary> /// Sets the custom error applied to the control. /// </summary> /// <param name="d"> /// The control to set the current custom error for. /// </param> /// <param name="value"> /// A string that represents the custom error. An empty or /// <c>null</c> string clears the error on the field. /// </param> public static void SetError(DependencyObject d, string value) { d.SetValue(ErrorProperty, value); } /// <summary> /// Called when the <see cref="P:ValidationHelper.Error"/> /// attached property changes value. /// </summary> /// <param name="d"> /// The <see cref="DependencyObject"/> that is having its /// <see cref="P:ValidationHelper.Error"/> property changing /// values. /// </param> /// <param name="e"> /// The <see cref="DependencyPropertyChangedEventArgs"/> /// instance containing the event data. /// </param> private static void OnErrorChanged( DependencyObject d, DependencyPropertyChangedEventArgs e) { BindingExpressionBase expr; expr = BindingOperations.GetBindingExpressionBase( d, BindingErrorTargetProperty); if ((expr == null) && (e.NewValue != null)) { // create a new binding between two properties that // we're only going to use so that we have an avenue // of our own to attach binding errors Binding b = new Binding(); b.Source = d; b.Path = new PropertyPath(BindingErrorSourceProperty); b.Mode = BindingMode.OneWayToSource; b.ValidationRules.Add(new InternalRule(d)); expr = BindingOperations.SetBinding( d, BindingErrorTargetProperty, b); } if (expr != null) { expr.UpdateSource(); } } } /// <summary> /// The internal implementation of <see cref="ValidationRule"/> /// that returns our real "error" whenever we want. /// </summary> private sealed class InternalRule : ValidationRule { private readonly DependencyObject _d; /// <summary> /// Initializes a new instance of the /// <see cref="InternalRule"/> class specific to a /// particular object. The /// <see cref="P:ValidationHelper.Error"/> property of the /// given object will be used to determine the error on the /// object. /// </summary> /// <param name="d"> /// The <see cref="DependencyObject"/> to return errors /// for. /// </param> public InternalRule(DependencyObject d) { _d = d; } public override ValidationResult Validate( object value, CultureInfo cultureInfo) { // completely ignore /value/ and look for the error // on the DependencyObject that was given to us in // our constructor string error = GetError(_d); if (string.IsNullOrEmpty(error)) { // an empty or null string means no error return ValidationResult.ValidResult; } else { // anything else means an error return new ValidationResult(false, error); } } } // two private dependency properties that we use internally to // set up our useless binding private static readonly DependencyProperty BindingErrorSourceProperty = DependencyProperty.RegisterAttached( "BindingErrorSource", typeof(object), typeof(ValidationHelper), new PropertyMetadata(null)); private static readonly DependencyProperty BindingErrorTargetProperty = DependencyProperty.RegisterAttached( "BindingErrorTarget", typeof(object), typeof(ValidationHelper), new PropertyMetadata(null)); } } |
Why it works
The System.Windows.Controls.Validation.Errors property is a collection for a reason—it’s a collection of all of the binding errors on the object.
For most controls, it’s not readily apparent that more than one binding on the same control could actually fail:
1 | <TextBox Text="{Binding Path=IngredientName, ValidatesOnDataErrors=True}"/> |
But if you had a complex control where more than one property was controlled directly by the user, it’s more obvious why you’d possibly need a collection instead of a single object:
1 2 3 4 5 | <!-- this slider has two thumbs; the user drags both of them around to specify a range --> <my:DoubleSlider MinValue="{Binding Path=Minimum, ValidatesOnDataErrors=True}" MaxValue="{Binding Path=Maximum, ValidatesOnDataErrors=True}"/> |
If both of these properties had errors, WPF would collect both of them in Validation.Errors.
So the ValidationHelper code above is essentially emulating the following XAML snippet in C# (note that the code in green isn’t actually possible—that’s why we’re writing this code in C#):
<TextBox x:Name="MyTextBox"
Text="{Binding Path=IngredientName, ValidatesOnDataErrors=True}"
<vh:ValidationHelper.BindingErrorTarget>
<Binding Source="MyTextBox"
Path="(vh:ValidationHelper.BindingErrorSource)">
Mode="OneWayToSource"
<vh:ValidationHelper+InternalRule (MyTextBox)>
</Binding>
</vh:ValidationHelper.BindingErrorTarget>
</TextBox>
(It may help at this point to open another a window with my little sketch of how WPF data binding data flows around.)
Our hidden BindingErrorTarget property participates in validation just like a regular property. So when the value of ValidationHelper.Error is changed:
- Force the binding on ValidationHelper.BindingErrorTarget to be re-evaluated from the target back to the source (call BindingExpression.UpdateSource()).
- Our validation rule InternalRule gets called as part of the normal validation process; our rule will return ValidationHelper.Error instead of validating the incoming value.
It doesn’t actually matter what the values of the BindingErrorTarget and BindingErrorSource properties are; they only exist so that we can key into the binding system.
Why?
IDataErrorInfo and ValidatesOnDataErrors would seemingly make this technique redundant: why go through all this trouble to expose a binding site for errors on the viewmodel when you could just implement IDataErrorInfo?
- IDataErrorInfo is only consulted when the source property changes value—if you have a data source whose errors can dynamically change independently of the source property, there isn’t a clean way from the viewmodel to force the view to pick up your changes in the error. (If your source implements INotifyPropertyChanged, you can raise PropertyChanged for the relevant property, but if you use DependencyObjects, there is no way to force the binding system to re-evaluate the property from the viewmodel—you’d need the BindingExpression, which then requires your viewmodel to have knowledge of the view).
- It may be inconvenient or impossible from a design standpoint to have the object containing your error property to also implement IDataErrorInfo.
If your error comes from a different object than your viewmodel, then your implementation of IDataErrorInfo would need to know where to fetch it. - If you ever had a situation where you had a ValidationRule that you wanted to bind to, you’ve probably discovered that it’s never going to happen—ValidationRule, not being a DependencyObject, doesn’t support binding—not in code, and certainly not in XAML. An attached behavior
or subclass(you never need to subclass in WPF) is really your only recourse for situations like this. - You don’t want to rely on .NET 3.0 SP1 or .NET 3.5 SP1. Thankfully, Windows 7 comes out of the box with it, but Vista, and certainly XP, do not. This technique works with every version of WPF.
Remember that any DependencyProperty that is the target of a binding can hold validation errors; also remember that you can add attached properties to any object. That means you can add arbitrary validation errors to any DependencyObject through this trick. You can also drive this error generation off of whatever you want—I chose the simplest example and created an attached property specifically to hold an error that will be reported, unchanged, right back through the binding system. You could create a new attached behavior, have the TextBox.Text property and TextBox.TextChanged event drive the error; then set up an attached behavior that provides validation on the text of the TextBox without having to provide an instance of ValidationRule. —DKT
In the interests of not cluttering the picture, I left out a little magic trick:
- We could attach a converter to the binding where IValueConverter.ConvertBack() returns Binding.DoNothing; this would stop data from ever flowing to the source.
- Then we could actually drop the BindingErrorSource property and reuse an existing one (like Tag or something), knowing that our binding will never actually change the value or otherwise interfere with it.
And why use two properties when you could get away with just one?
Sep 24: Updated the OnErrorChanged to fix a bug that would cause the error state to never actually clear…whoops! –DKT