Applying MetaData to WPF Bindings

This post describes how I’m applying MetaData defined in my model to WPF controls at runtime. The goal is to keep the XAML concise without cluttering it with property settings that obscure the form layout intent and ensuring it stays in sync with the model across all forms in the application. Refer to Karl Schifflet’s recent passionate post about why MetaData is important.

In WinForms I’ve traditionally used the data Binding to identify how MetaData should map to controls. So the following describes my attempt at the same thing in WPF.

I’m using attributes to define MetaData directly against my model classes. I’d like for it to be more open than that – for instance extracting the MetaData from a database, config file etc. – but attributes is a good place to start. Here’s an example property from my Holiday model class.

    [Mandatory]
[Annotation("Name", "Name of this holiday")]
[StringData(MaximumLength=50, Case=CharacterCasing.Upper)]
public string Name
{
get { return _name; }
set
{
if (!Object.Equals(_name, value))
{
_name = value;
OnPropertyChanged("Name");
}
}
}

So in this example I’m using three custom attributes. [Mandatory] is pretty self explanatory. Whilst its not used by the MetaData Applicator it is used by the ValidationEngine to create a MandatoryRule.

The [Annotation] attribute derives from System.ComponentModel.DisplayName and extends it by including extended descriptions for tooltips and possible help keys etc.

The third [StringData] attribute is the one that defines the MetaData specific to string data types. The attributes themselves are defined in a neutral namespace and assembly (Spencen.MetaData) with no references to any presentation assemblies.

Taking a look at the StringDataAttribute class its just a bunch of properties with no inherent behaviour (like most attributes).

    [MetaData]
public class StringDataAttribute : StringLengthAttribute 
    {
public StringDataAttribute() : base()
{
}
        #region Public Properties
public string Format { get; set; }
public Type DataType { get; set; }
public CharacterCasing Case { get; set; }
public bool MultiLine { get; set; }
public bool AllowTab { get; set; }
public bool AllowNewline { get; set; }
        #endregion
    }

The StringDataAttribute class is itself decorated with a [MetaData] attribute. This tells the MetaData Applicator that it needs to be considered when applying MetaData properties to the UI. [The StringLengthAttribute that this class derives from is actually another ValidationRule attribute that results in the creation of the StringLengthValidationRule.]

Now the tricky part in all of this was trying to “hook” into the WPF Binding pipeline. I tried all the likely approaches – inheriting from Binding, looking at BindingExpression and eventually creating a custom MarkupExtension that offloads most of the work to the standard Binding MarkupExtension.

Once I got that far I looked around and found that the MarkupExtension appears to be the current approach. Mine is currently a very simplified implementation – as I later found out there are much better examples around including this one or the one used by the Enterprise Library Contrib’s Standalone Validation Block.

Having a customised Binding gives several advantages not the least of which is applying default values for the Binding itself. Ever got tired of adding ValidatesOnDataErrors=true, ValidatesOnExceptions=true, NotifyOnValidationError=true, UpdateSourceTrigger=UpdateSourceTrigger.PropertyChanged to every Binding!?

Anyhow – more on that later – for now I use the following code within the ProvideValue() method of my custom MarkupExtension.

    IProvideValueTarget valueTarget = (IProvideValueTarget)serviceProvider.GetService(
typeof(IProvideValueTarget)); DependencyObject dependencyObject = valueTarget.TargetObject as DependencyObject; DependencyProperty dependencyProperty = valueTarget.TargetProperty as DependencyProperty; if (dependencyObject != null && dependencyProperty != null) { if (dependencyObject is FrameworkElement) { object dataContext = ((FrameworkElement) dependencyObject).GetValue(
FrameworkElement.DataContextProperty); if (Applicator != null) { PropertyDescriptor property = TypeDescriptor.GetProperties(dataContext).Find(_path, false); if (property != null) { foreach (Attribute propertyAttribute in property.Attributes) { // Check if the attribute class is itself decorated with a MetaData attribute. if (TypeDescriptor.GetAttributes(propertyAttribute).Contains(new MetaDataAttribute())) { Applicator.ApplyTo(propertyAttribute, dependencyObject, dependencyProperty); } } } } } }

Essentially its just checking each Binding and then looking for [MetaData] decorated attributes on the data source. [Note that this code overly simplifies resolving the DataContext and Path. Remember the path is just that not necessarily just a property name, it can even have things like indexers etc. in it.]. Once its found an attribute it calls out to the statically assigned Applicator.

The Applicator simply consists of a registered list of types and IMetaDataApplicators. Prior to any custom data Binding the application registered those [MetaData] attributes that it wishes to apply to the UI with the Applicator on the markup extension. Like so…

    MetaDataExtension.Applicator = new MetaDataApplicator();
MetaDataExtension.Applicator.RegisteredApplicators.Add(
typeof(StringDataAttribute), new StringDataApplicator());

The StringDataApplicator class then does whatever it desires with the StringDataAttribute, target object and property that its been given. Here’s a simple partially complete example:

    public class StringDataApplicator : IMetaDataApplicator
    {
public void ApplyTo(Attribute attribute, object targetObject, object targetProperty)
{
StringDataAttribute stringData = attribute as StringDataAttribute;
if (stringData == null) 
throw new InvalidOperationException("StringDataApplication only supports StringDataAttribute."); TextBoxBase textBoxBase = targetObject as TextBoxBase; if (textBoxBase != null) { textBoxBase.AcceptsTab = stringData.AllowTab; textBoxBase.AcceptsReturn = stringData.AllowNewline;
} TextBox textBox = targetObject as TextBox; if (textBox != null) { switch(stringData.Case) { case Spencen.MetaData.CharacterCasing.Lower: textBox.CharacterCasing = System.Windows.Controls.CharacterCasing.Lower; break; case Spencen.MetaData.CharacterCasing.Upper: textBox.CharacterCasing = System.Windows.Controls.CharacterCasing.Upper; break; case Spencen.MetaData.CharacterCasing.Camel: // TODO: Put hooks in to do formatting. break; case Spencen.MetaData.CharacterCasing.Title: // TODO: Put hooks in to do formatting. break; } textBox.MaxLength = stringData.MaximumLength; } } }

The XAML to bind the Holiday classes Name property using the MetaData MarkupExtension would be:

    <TextBox Grid.Column="1" Grid.Row="0" Width="150" Text="{meta:MetaData Name}"/>

The rendered TextBox would use the Binding properties defined as the default on the MetaData MarkupExtension (as opposed to the standard Binding class defaults). It will have its MaxLength set to 10 and entry forced to uppercase characters. Of course there are plenty of other properties on the StringDataAttribute that could have been used. For example:

  • Defining a DataType that specifies a type that is used as an IValueConverter and/or formatter. This can be used to setup all the necessary plumbing for allowing data entry of specific string types – such as phone numbers, e-mail addresses etc.
  • Using MaximumLength to determine the optimum length for a TextBox. Again this helps with consistency – all 4 character code fields are automatically set to the standard width for a four character field.
  • Using the AnnotationAttribute to set the Tooltip.

MetaData

2 thoughts on “Applying MetaData to WPF Bindings”

  1. Thanks Denis. Well – the source if very rough – but it gets the idea across. You can grab a ZIP of a sample that I put together using the code from the article here. It contains one project for the MetaData attributes and another for the MetaDataBinding extension (e.g. UI related stuff).

Comments are closed.