Using MarkupExtensions as ValueConverters (Part 1 - The Basics)

by OSgAgA 4. November 2012 19:53

Using value converters in XAML is a very powerful, but sometimes cumbersome feature. MarkupExtensions can simplify the usage of converters significantly. There are a lot of posts about this topic allready on the net, but none of them (at least none I know of) provides an approach as complete and flexible as the one presented here.

The standard Approach

The following code shows a typical usage of a converter that scales a value using a factor given as a converter parameter:

<Window.Resources>
    <ResourceDictionary>
        <local:ScaleConverter x:Key="scaleConverter"></local:ScaleConverter>
    </ResourceDictionary>
</Window.Resources>

...

<TextBlock Text="{Binding Value, Converter={StaticResource scaleConverter}, ConverterParameter=3}"></TextBlock>

There are some things to notice here:

  1. To access the converter you have to create a static resource.
  2. The syntax is pretty long and not very readable.
  3. The ConverterParameter cannot be given as a binding as a converter is not a part of the logical tree.

Eliminating the need for a static resource.

The first approach to simplify this will eliminate the use of a static resource. This can be done using a MarkupExtension. With that it will be possible to access the converter using the following code:

<TextBlock Text="{Binding Value, Converter={local:ScaleConverterVersion2}, ConverterParameter=2}"></TextBlock>

To do this, the converter must be derived from MarkupExtension and (obviously) implement the IValueConverter interface. Then the ProvideValue method of the MarkupExtension class must be overriden and implemented so that the converter itself will be returned. The code for that is pretty simple (ConvertBack method ommited for readability):

public class ScaleConverterVersion2: MarkupExtension, IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (double)value * double.Parse(parameter as string);
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

Using a constructor for setting the converter Parameter

Although the usage has been improved using the second approach the usage of the converter parameter is still a little bit to verbose. The following code would be a more convenient approach for calling the converter with the converter parameter "2".

<TextBlock Text="{Binding Value, Converter={local:ScaleConverterVersion3 2}}"></TextBlock>

This can be easily realized with the parameter given to the constructor of the MarkupExtension.

public class ScaleConverterVersion3: MarkupExtension, IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (double)value * factor;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }

    private double factor = 2;

    public ScaleConverterVersion3(double factor) {
        this.factor = factor;
    }
}

With this approach it is even possible to give more than one parameter to the converter.

Using Multibindings

The solution so far may be enough for many situations, but it still has the disadvantage that the converter parameter is not bindable. One idea to handle that problem is to use a multibinding, so that the converter can be used like

<TextBlock>
    <TextBlock.Text>
        <MultiBinding Converter="{local:ScaleConverterVersion4}">
            <Binding Path="Value"></Binding>
            <Binding Path="Factor"></Binding>
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

This can be accomplished using pretty much the same code as before but the class must now implement the IMultiValueConverter interface istead of the IValueConverter interface:

public class ScaleConverterVersion4: MarkupExtension, IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var result = (double)values[0] * (double)values[1];

        if (targetType == typeof(string))
        {
            return result.ToString();
        }

        if (targetType == typeof(double))
        {
            return result;
        }

        throw new ArgumentException("Expected return type string or double");
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

Generate MultiBinding in extension

Althoug the approach mentioned above is working in the sense of allowing to use a binding as a converter Parameter, the usage is still pretty cumbersome. The code changed from a one liner to a 8 line solution which is not very readable.

This problem should be solved with the fifth approach. First of all it is not as simple as adding a property to the converter and bind a value to this property as the converter must be a DependencyObject to have bindable properties, but the converter allready derives from MarkupExtension and multiple object inheritance is not supported by C#.

Another problem is that a converter is not a part of the logical tree, so even if it would be possible to bind to a property of the converter it does not have a DataContext (or access to elements inside the logical tree) to bind against.

To get around these obstacles it is necessary to build the needed structure inside the constructor of the MarkupExtension and use it in a slightly different way as can be seen in the following code.

<TextBlock Text="{local:ScaleConverterVersion5 {Binding Value}, {Binding Factor}}"></TextBlock>

The thing to notice here is that no standard binding is used but the extension is called directly. This can be done with two simple steps. First a constructor is needed accepting two parameters of type Binding. But as the extension does not know the framework element it is applied to in its constructor it is not possible to apply the bindings yet. Therefor they are stored in some fields of the class.

private Binding valueBinding;

private Binding factorBinding;

public ScaleConverterVersion5(Binding valueBinding, Binding factorBinding)
{
    this.valueBinding = valueBinding;
    this.factorBinding = factorBinding;
}

The binding to the property of the framework element can be done in the ProvideValue method of the extension. Therefor the method must provide the binding as the object returned from the MultiBinding.ProvideValue method.

public override object ProvideValue(IServiceProvider serviceProvider)
{
    MultiBinding multibinding = new MultiBinding();
    multibinding.Bindings.Add(this.valueBinding);
    multibinding.Bindings.Add(this.factorBinding);
    multibinding.Converter = (IMultiValueConverter)this;

    return multibinding.ProvideValue(serviceProvider);
}

Thats all! With this solution it is possible to use binding for an arbitrary number of arguments inside a converter.

Supporting constant and data bound values

Nearly there. There is only one problem left: It is not possible to choose between using a binding and a constant value! Fortunately this can be solved easily. First the constuctor must accept the parameters as objects instead of Bindings and can then decide whether to store a binding or a constant value.

To keep it simple, this is shown here for the factor parameter only.

private Binding valueBinding;

private Binding factorBinding;

private double? factor = null;

public ScaleConverterVersion6(Binding valueBinding, object factor)
{
    this.valueBinding = valueBinding;

    if (factor is Binding)
    {
        this.factorBinding = factor as Binding;
    }
    else if (factor is string)
    {
        double value = 2;
        double.TryParse(factor as string, out value);
        this.factor = value;
    }
    else if (factor is double)
    {
        this.factor = (double)factor;
    }
}

The ProvideValue method will then only have to add the factor binding if the factor is not a constant value.

public override object ProvideValue(IServiceProvider serviceProvider)
{
    MultiBinding multibinding = new MultiBinding();
    multibinding.Bindings.Add(this.valueBinding);
    if (!this.factor.HasValue)
    {
        multibinding.Bindings.Add(this.factorBinding);
    }

    multibinding.Converter = (IMultiValueConverter)this;
    return multibinding.ProvideValue(serviceProvider);
}

The Convert method obviously has to check for this situation and react accordingly.

public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
    double result;

    if (factor.HasValue)
    {
        result = (double)values[0] * this.factor.Value;
    }
    else
    {
        result = (double)values[0] * (double)values[1];
    }

    if (targetType == typeof(string))
    {
        return result.ToString();
    }

    if (targetType == typeof(double))
    {
        return result;
    }

    throw new ArgumentException("Expected return type string or double");
}

Finally! Now its possible to call the extension methods either with a constant value or with a binding.

<TextBlock Text="{local:ScaleConverterVersion6 {Binding Value}, 10}"></TextBlock>
<TextBlock Text="{local:ScaleConverterVersion6 {Binding Value}, {Binding Factor}}"></TextBlock>

No StaticResource is needed any more, bindings are possible and the code will be much more readable than with the standard approach.

The complete code can be downloaded here:

MarkupExtensions Part 1.zip (86.01 kb) [Downloads: 308]

Follow ups

This is the first part of a series planned containing of three posts:

  1. Using MarkupExtensions as ValueConverters (Part 1 - The Basics)
  2. Using MarkupExtensions as ValueConverters (Part 2 - Setting default values for an application)
  3. Using MarkupExtensions as ValueConverters (Part 3 - An example: TranslationConverter)

Part 2 and 3 will be published soon. Stay tuned.

Tags: , , , ,

Add comment




biuquote
  • Comment
  • Preview
Loading


Month List