Sunday, October 27, 2013

Bind WPF controls to attributes using Caliburn Micro

To bind for example an DecimalUpDown control to a RangeAttribute specified in a domain model, or a maxlength to a StringLengthAttribute you need to change the automatic binding of Caliburn Micro.

The example below is for a DecimalUpDown, but you can use it for all kind of fun stuff. In my github working example you can also see a Textbox.

First we need to add an ElementConvention for out DecimalUpDown in the BootStrapper's Configure method. Override this method and add the following convention (edit 2014-03-26 added attribute check to prevent binding errors):
ConventionManager.AddElementConvention<DecimalUpDown>(DecimalUpDown.ValueProperty, "Value", "ValueChanged").ApplyBinding =
(viewModelType, path, property, element, convention) =>
{
    if (!ConventionManager.SetBindingWithoutBindingOrValueOverwrite(viewModelType, path, property, element, convention, DecimalUpDown.ValueProperty))
        return false;

    if (property.GetCustomAttributes(typeof (RangeAttribute), true).Any())
    {
        if (!ConventionManager.HasBinding(element, DecimalUpDown.MaximumProperty))
        {
            var binding = new Binding(path) {Mode = BindingMode.OneTime, Converter = RangeMaximumConverter, ConverterParameter = property};
            BindingOperations.SetBinding(element, DecimalUpDown.MaximumProperty, binding);
        }

        if (!ConventionManager.HasBinding(element, DecimalUpDown.MinimumProperty))
        {
            var binding = new Binding(path) {Mode = BindingMode.OneTime, Converter = RangeMinimumConverter, ConverterParameter = property};
            BindingOperations.SetBinding(element, DecimalUpDown.MinimumProperty, binding);
        }
    }

    return true;
};
As you can see the binding uses a RangeMaximumConverter and a RangeMinimumConverter. These are fairly simple with the AttributeConverter baseclass:
public sealed class RangeMaximumConverter : AttributeConverter<RangeAttribute>
{
    public override object GetValueFromAttribute(RangeAttribute attribute)
    {
        return attribute.Maximum;
    }
}
And the AttributeConverter base class:
public abstract class AttributeConverter<T> : IValueConverter
    where T : Attribute
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var property = parameter as PropertyInfo;

        if (property == null)
            return new ArgumentNullException("parameter").ToString();

        if (!property.IsDefined(typeof(T), true))
            return new ArgumentOutOfRangeException("parameter", parameter,
                "Property \"" + property.Name + "\" has no associated " + typeof(T).Name + " attribute.").ToString();

        return GetValueFromAttribute((T)property.GetCustomAttributes(typeof(T), true)[0]);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException();
    }

    public abstract object GetValueFromAttribute(T attribute);
}
You can find a working example in GitHub.

Happy coding,
Luuk