In the previous article we took a step further on input validation over WPF controls by means of an attached property. The attached property’s owner would then have the required programming to add validation logic to the control’s target dependency property binding.
Keeping optimization in mind we can go, once again, a step further on this and reduce the code required by implementing our own Binding object, with it’s own validation mechanism, that could be configured by the developer in design time. An example of this methodology would be going from this:
<TextBox x:Name="ValidatedTextBox" Width="200"
Rules:MandatoryRule.TargetProperty="{x:Static TextBox.TextProperty}"
Style="{DynamicResource EditableTextBox}" Text="{Binding MyProperty}" />
To this:
<TextBox x:Name="ValidatedTextBox" Width="200"
Style="{DynamicResource EditableTextBox}"
Text="{validation:ValidationBinding MandatoryInputRule, MyProperty}" />
So what’s the trick when we wish to replace the good old Binding class by a custom one? The Binding class, like many others also familiar to you like StaticResource, DynamicResource, RelativeSource or Templatebinding, is a MarkupExtension. You can detect MarkupExtensions in XAML by the curly braces. They are the actual indicator of a markup to the XAML parser. So the idea here is to create a new binding-like MarkupExtension and extend it to accommodate binding functionality with built in validation.
Before we dig into the code, I believe a brief explanation on MarkupExtensions is needed.
MarkupExtensions are a XAML concept and can be described as a behavior that the XAML parser acquires when going through the XAML to discard the general treatment of attribute values as either a literal string or a directly string-convertible value and, instead, “pass the ball” to a handling class perform its own logic in the visual tree buildup. In attribute syntax, curly braces ({ and }) indicate a markup extension usage. Its a good alternative when the requirement is more ambitious a type converters implementation on types or properties.
So why a MarkupExtension and not just use nested property element syntax? – you ask. Well, for two reasons. It reduces the amount of code required to perform the task, and also because when using property element syntax, a new instance is created, while a markup extension usage can potentially return an existing instance, and thus can be more versatile or might incur less object overhead. But there’s a disadvantage though, and that’s collections. You can’t define collection elements in attribute syntax, unlike property element syntax. Take the x:Array MarkupExtension for instance:
<x:Array Type="sys:String">
<sys:String>dot</sys:String>
<sys:String>Net</sys:String>
<sys:String>Brainwork</sys:String>
<sys:String>Rocks!</sys:String>
</x:Array>
Each markup extension identifies itself to a XAML parser by means of a class that derives from MarkupExtension, and provides an implementation of the ProvideValue method. This method defines what object is returned once the markup extension is evaluated. The returned object is typically instantiated or set by using the various string tokens passed to the markup extension.
Finally, with this in mind, lets dig into the code. The following ValidationBinding class is a MarkupExtension that aggregates a validation mechanism and allows its definition in the attribute syntax binding declaration.
[MarkupExtensionReturnType(typeof (object))]
[Localizability(LocalizationCategory.None, Modifiability = Modifiability.Unmodifiable,
Readability = Readability.Unreadable)]
public class ValidationBinding : MarkupExtension
{
#region Private Fields
// Rule collection
private readonly List rules = new List();
// Our binding object, pre-initialized with PropertyChanged trigger.
private Binding binding = new Binding {UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged};
private ValidationRule rule;
// Convert string to rule instance.
private readonly StringToValidationRuleConverter ruleConverter = new StringToValidationRuleConverter();
#endregion
#region Public properties
[DefaultValue(null)]
public object AsyncState
{
get { return binding.AsyncState; }
set { binding.AsyncState = value; }
}
[Browsable(false)]
public Binding Binding
{
get { return binding; }
set { binding = value; }
}
// ...
[DefaultValue(null)]
public string XPath
{
get { return binding.XPath; }
set { binding.XPath = value; }
}
[DefaultValue(null)]
public Collection ValidationRules
{
get { return binding.ValidationRules; }
}
[DefaultValue(null)]
[EditorBrowsable(EditorBrowsableState.Always)]
[TypeConverter(typeof (StringToValidationRuleConverter))]
[ConstructorArgument("ruleName")]
public ValidationRule Rule
{
get { return rule; }
set
{
rule = value;
rules.Add(rule);
}
}
#endregion
#region Constructor
public ValidationBinding()
{
}
public ValidationBinding(string ruleName)
{
AddRule(ruleName);
}
public ValidationBinding(string ruleName, string path)
: this(ruleName)
{
Path = new PropertyPath(path);
}
public ValidationBinding(string ruleName1, string ruleName2, string path)
: this(ruleName1, path)
{
AddRule(ruleName2);
}
public ValidationBinding(string ruleName1, string ruleName2, string ruleName3, string path)
: this(ruleName1, ruleName2, path)
{
AddRule(ruleName3);
}
#endregion
#region Implementation
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
return this;
var provideValueTarget = serviceProvider.GetService(typeof (IProvideValueTarget)) as IProvideValueTarget;
DependencyObject targetDependencyObject;
DependencyProperty targetDependencyProperty;
CheckCanReceiveMarkupExtension(this, provideValueTarget, out targetDependencyObject,
out targetDependencyProperty);
if (targetDependencyObject == null || targetDependencyProperty == null)
{
return this;
}
// Fetch metadata - unused, just for your reference
var metadata =
targetDependencyProperty.GetMetadata(targetDependencyObject.DependencyObjectType) as
FrameworkPropertyMetadata;
if ((metadata != null && !metadata.IsDataBindingAllowed) || targetDependencyProperty.ReadOnly)
throw new ArgumentException("");
// Two-way binding requires a path
if (
(binding.Mode == BindingMode.TwoWay || binding.Mode == BindingMode.Default) &&
binding.XPath == null &&
(binding.Path == null || String.IsNullOrEmpty(binding.Path.Path))
)
throw new InvalidOperationException("Two way binding has no Path.");
if (rules.Count > 0)
foreach (var r in rules)
binding.ValidationRules.Add(r);
return binding.ProvideValue(serviceProvider);
}
///
/// This static method validates the holder of the current extension, where it is being defined. It detects the various scenarios
/// where a MarkupExtension is allowed to be set and tests their candidacy for the assignment.
///
public static void CheckCanReceiveMarkupExtension(MarkupExtension markupExtension,
IProvideValueTarget provideValueTarget,
out DependencyObject targetDependencyObject,
out DependencyProperty targetDependencyProperty)
{
targetDependencyObject = null;
targetDependencyProperty = null;
if (provideValueTarget == null)
return;
var targetObject = provideValueTarget.TargetObject;
if (targetObject == null)
return;
var targetType = targetObject.GetType();
var targetProperty = provideValueTarget.TargetProperty;
if (targetProperty != null)
{
targetDependencyProperty = targetProperty as DependencyProperty;
if (targetDependencyProperty != null)
{
targetDependencyObject = targetObject as DependencyObject;
Debug.Assert(targetDependencyObject != null,
"DependencyProperties can only be set on DependencyObjects");
}
else
{
// For the implementation at hand we should do this, because nothing else matters.
// throw new XamlParseException("Type not assignable.");
// Moving on with all possible cases...
var targetMember = targetProperty as MemberInfo;
if (targetMember != null)
{
// If targetMember is a MemberInfo, then its the CLR Property case.
// Setters, Triggers, DataTriggers & Conditions are the special cases of
// CLR properties in which DynamicResource and Bindings are allowed.
// Since StyleHelper.ProcessSharedPropertyValue prevents calls to ProvideValue
// in such cases, there is no need for special code to handle them here.
Debug.Assert(targetMember is PropertyInfo || targetMember is MethodInfo,
"TargetMember is either a CLR property or an attached static setter method");
Type memberType;
var propertyInfo = targetMember as PropertyInfo;
if (propertyInfo != null)
memberType = propertyInfo.PropertyType;
else
{
var methodInfo = (MethodInfo) targetMember;
var parameterInfos = methodInfo.GetParameters();
Debug.Assert(parameterInfos.Length == 2,
"The signature of a static settor must contain two parameters");
memberType = parameterInfos[1].ParameterType;
}
// Check if the MarkupExtensionType is assignable to the given MemberType
// This checking allows properties like DataTrigger.Binding, Condition.Binding
// HierarchicalDataTemplate.ItemsSource, GridViewColumn.DisplayMemberBinding
if (!typeof (MarkupExtension).IsAssignableFrom(memberType) ||
!memberType.IsAssignableFrom(markupExtension.GetType()))
{
throw new XamlParseException("Type not assignable.");
}
}
else
{
throw new XamlParseException("Type not assignable.");
}
}
}
else
{
throw new XamlParseException("Type not assignable.");
}
}
private void AddRule(string ruleName)
{
var validationRule =
new StringToValidationRuleConverter().ConvertFrom(null, null, ruleName) as ValidationRule;
if (validationRule != null)
rules.Add(validationRule);
}
#endregion
}
The above code is purposely expanded to better understanding. You can refactor on your implementation afterwards.
Lets start by explaining the two attributes defined for the class. MarkupExtensionReturnType specifies type information for the type returned in the ProvideValue method. We’ve specified object for this example. Localizability specifies the localization attributes for the class. This has to to with the Localization features in WPF. Checkout the LocBaml tool for more info on this. For now the class is being defined as not modifiable by localizers and also not readable.
Next come some private field declarations. The rules list will hold every ValidationRule objects defined in the class declaration in XAML. There’s also a Binding object, which is the foundation of the ValidationBinding class. Everything the extension does is meant with the purpose of configuring and defining this binding instance. That’s why the extension exposes a set of properties that are typical of the binding class, to allow the extension to expose binding behavior to the outside. You are already familiar with all of those, except for the last one, which is:
[DefaultValue(null)]
[EditorBrowsable(EditorBrowsableState.Always)]
[TypeConverter(typeof (StringToValidationRuleConverter))]
[ConstructorArgument("ruleName")]
public ValidationRule Rule
{
get { return rule; }
set
{
rule = value;
rules.Add(rule);
}
}
This property serves the purpose of direct rule definition in the attribute. Like so:
Text="{val:ValidationBinding Rule=MyCustomRule, Path=MyProperty}"
There are a couple of attributes defined on this class member:
- DefaultValueAtribute: The default attribute the property will have once the class is initialized. This is typically used by designers to reset the member’s value, or even code generators, to determine whether code should be generated for the member or other reflection tasks.
- EditorBrowsableAttribute tells the designer to display this property. This type can be used in a visual designer or text editor to determine what properties or methods are visible to the user when in design mode. The IntelliSense engine in Visual Studio is an example of such usage, because it uses this attribute to determine whether or not to show a property or method.
- TypeConverterAttribute specified the value converter to use when the user specifies the property in attribute syntax. This is typically a string converter.
- ConstructorArgumentAttribute specifies that this property is set by a constructor parameter with the given name. In the ValidationBinding class example, the first parameter in two overloads is “ruleName” and it sets the Rule property afterwards.
That said, It’s time to explain the purpose of the several constructor overloads. In case you don’t know, when dealing with attribute syntax attributes a lot is considered and assumed by the XAML processor. Consider the following example:
Since we are assigning values to properties (hence the “equals” = sign), the XAML parser will assume commas as delimiters for each property set operation. On the other hand, if no assignments are made the processor assumes commas are constructor parameter delimiters:
The text property of this TextBox control is being set to a Binding MarkupExtension object, with the target property as its parameter. Underneath, a one parameter constructor is being called to process the given property name.
So in the ValidationBinding class we define several constructor overloads in order to be able to define more than one ValidationBinding at a time. This is only required for the sake of attribute syntax comfort. It wouldn’t be necessary if you consider property element syntax in your implementation, but then that would be more troublesome wouldn’t it?
Then there’s the ProvideValue method implementation that is responsible for making all the pieces come together. It starts by getting a reference to a IProvideValueTarget object through the IServiceProvider argument provided. It then calls the CheckCanReceiveMarkupExtension method to analyze this extension’s owner characteristics and see if it meets the requirements of our MarkupExtension to be able to execute its purpose, which is data binding validation. Several scenarios are considered and evaluated for this, but the present case targets only DependencyProperties. You should take into account all possible MarkupExtension usages in XAML when validating your own logic. That way you can filter all the scenarios where your code might be used and react accordingly.
To wrap things up, validations are added to the binding object, and the object that is to be set on the property the extension is applied on, is returned. The AddRule method actually adds validation rules to the binding instance. It does so by converting the rule name to the actual object, through reflection.
private void AddRule(string ruleName)
{
var validationRule = ruleConverter.ConvertFrom(null, null, ruleName) as ValidationRule;
if (validationRule != null)
rules.Add(validationRule);
}
When initialized, the converter checks for all ValidationRules in the executing assembly and collects them in order to hold all possible types and to be able to initialize them on demand, or shall I say, when a compatible rule name is passed onto the ConvertFrom method:
#region #using Directives
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Windows.Controls;
#endregion
namespace ValidationSample4
{
public class StringToValidationRuleConverter : TypeConverter
{
private static readonly Dictionary Rules;
static StringToValidationRuleConverter()
{
Rules = new Dictionary();
var assembly = Assembly.GetExecutingAssembly();
foreach (var type in assembly.GetTypes().Where(type => type.IsSubclassOf(typeof (ValidationRule))))
Rules.Add(type.Name, type);
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof (string) || base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof (string))
return true;
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string text = value as string;
if (text != null)
{
try
{
if (Rules.Any(type => type.Key == text))
{
var instance = Activator.CreateInstance(Rules);
return instance;
}
}
catch (Exception e)
{
throw new Exception(String.Format("Cannot convert '{0}' ({1}) because {2}", value, value.GetType(), e.Message), e);
}
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == null)
throw new ArgumentNullException("destinationType");
var rule = value as ValidationRule;
if (rule != null && CanConvertTo(context, destinationType))
return rule.GetType().Name;
return base.ConvertTo(context, culture, value, destinationType);
}
}
}
I guess that wraps it up. But there’s nothing better than give it a go yourself. Take a good look at the example, mess around with it, and feel free to comment this implementation. I’ll continue to evolve this mechanism in following articles.
Here’s the sample with all the code:
Wpf Input Validation (WpfInputValidation.rar)
Includes:
- ValidationSample1 (Base implementation)
- ValidationSample2 (Attached property)
- ValidationSample3 (Attached property – no need for UpdatePropertyTrigger)
- ValidationSample4 (Markup Extension)
In case you missed them, here are the previous articles:
1) Wpf Input Validation Part I – Validation Rules
2) Wpf Input Validation Part II – Attached Properties