Archives

Anticipation

  • No dates present

A Tableau of Conversion

Some people might be wondering why I said that the last two Programming posts were part of a series — after all, the first one was a utility class to help out with type conversion, while the second one focused on using enumerations in the UI. How is that a series? Well, it’s this post which should help to tie everything together.

I mentioned in the last post that a significant drawback of the technique presented there was that it presents the internal names from the code directly to the user. Sometimes that’s ok, if you’re writing something for internal use, or if the names are short enough (a single word) and intended for an audience that speaks a single language. But that still eliminates a lot of potential usefulness.

As a reminder, the (somewhat contrived) example from the previous post went something like this:

<Window.Resources>
    <ObjectDataProvider x:Key="ReportTypes" ObjectType="{x:Type System:Enum}" MethodName="GetValues">
        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="app:ReportType" />
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
    <app:ReportTypesConverter x:Key="ReportTypesConverter" />
    <DataTemplate DataType="{x:Type app:ReportTypes}">
        <TextBlock Text="{Binding Converter={StaticResource ReportTypesConverter}}" />
    </DataTemplate>
</Window.Resources>
...
    <ComboBox ItemsSource="{Binding Source={StaticResource ReportTypes}}" SelectedItem="{Binding ReportType, Mode=TwoWay}" />

But the ReportTypesConverter isn’t very satisfactory there — it requires specific code to be written for each enum type for each set of different text you want to provide (which can potentially mean that in some cases you’ll need more than one converter for a single enum type, if it’s used in different contexts where you want to use different text). And something just feels wrong about putting something so fundamentally UI-related as what text to display in the combo box in the code of a converter class rather than in the view itself.

So I went looking for a better way. And just for a change of pace, I’ll present the usage cases for my solution first:

<Window.Resources>
    <lib:LookupConverter x:Key="ReportTypesConverter">
        <lib:LookupConversion From="Basic" To="Basic Report" />
        <lib:LookupConversion From="Extended" To="Enhanced Report (includes section B)" />
        ...
    </lib:LookupConversion>
</Window.Resources>

It’s still a converter, but the values to be returned for each source value are given in table form within the XAML itself, instead of having to be listed in the code. This means that the same converter can be used for lots of different conversions. The real beauty of it though is that thanks to the conversion helper from the first post in the series, the actual values to be converted enjoy the same automatic type conversion benefits as XAML normally provides for fixed-type properties, despite being variable-type properties. This allows you to just list “Basic” instead of “{x:Static app:ReportTypes.Basic}”, and so on.

And this isn’t limited to just producing text values, either. Let’s say you have a control presenting log information; each entry has a type (info, warning, error, etc) and a message, and you want to use different colours to denote the different types of messages. Again, you could write a custom converter for this job, but it’s easier to use a LookupConverter:

<Window.Resources>
    <lib:LookupConverter x:Key="LogColourConverter">
        <lib:LookupConversion From="Warning" To="Orange" />
        <lib:LookupConversion From="Error" To="Red" />
    </lib:LookupConverter>
    <DataTemplate x:Key="LogEntryTemplate">
        <TextBlock Foreground="{Binding MessageType, Converter={StaticResource LogColourConverter}}"
                   Text="{Binding Message}" />
    </DataTemplate
</Window.Resources>

Note that again, MessageType is an enum, but this time we’re not covering all the possible values — anything that’s not listed in the table (ie. anything that’s not a Warning or Error) will make WPF pretend the binding wasn’t present, and thereby use the default Foreground from parent controls, styles, or the user’s theme. And if you did want to specify a default, then it’s as simple as adding a final entry to the table which omits the From clause — the converter will assume that this matches anything:

        <lib:LookupConversion To="Black" />

(Another way to define a default value is to set a FallbackValue on the binding that references the converter.)

There is another use of this converter which is a particular favourite of mine. In an earlier post I talked about the DataTemplateSelector and how much of a pain in the butt it was, and showed a way to use styles to select templates instead. Well, this converter can help with that too. The case shown on that page, selecting between one template and another depending on a single boolean property, is probably simple enough that styles make the most sense, but it can be rewritten using a LookupConverter if you like:

<Window.Resources>
    <DataTemplate x:Key="ReadOnlyTemplate">...</DataTemplate>
    <DataTemplate x:Key="EditableTemplate">...</DataTemplate>
    <lib:LookupConverter x:Key="EditableTemplateSelector">
        <lib:LookupConversion From="False" To="{StaticResource EditableTemplate}" />
        <lib:LookupConversion From="True" To="{StaticResource ReadOnlyTemplate}" />
    </lib:LookupConverter>
    <DataTemplate x:Key="FooTemplate">
        <ContentPresenter Content="{Binding}"
                          ContentTemplate="{Binding IsReadOnly, Converter={StaticResource EditableTemplateSelector}}" />
    </DataTemplate>
</Window.Resources>

Where this converter excels is if you have a larger group of templates to select between — for example if you wanted to use a different template for all 5 possible ReportTypes.

As with any other property assignment in XAML, you’re not limited to simple values — you can use XAML’s extended assignment syntax to assign entire trees of objects to either of From or To if you so require; for example you can inline the templates above rather than specifying them as separate resources if you want:

<Window.Resources>
    <lib:LookupConverter x:Key="EditableTemplateSelector">
        <lib:LookupConversion From="False">
            <lib:LookupConversion.To>
                <DataTemplate>... editable template ...</DataTemplate>
            </lib:LookupConversion.To>
        </lib:LookupConversion>
        <lib:LookupConversion From="True">
            <lib:LookupConversion.To>
                <DataTemplate>... read-only template ...</DataTemplate>
            </lib:LookupConversion.To>
        </lib:LookupConversion>
    </lib:LookupConverter>
</Window.Resources>

Of course, this converter still has some limitations. Because of the way the conversion helper works, it needs to know the types of both the From and To values, and it’s limited to one auto-conversion step. It handles the former with the help of the binding itself — it assumes that the From value is convertible to the binding’s source and that the To value is convertible to the binding’s target. The second is a safety measure, so that you don’t get hit with surprise conversions to completely the wrong values — usually it won’t be an issue especially with the standard WPF types, since they’re usually smart enough to handle the edge cases themselves; for example, you’ll notice above that you can use “Red” (a string describing a Color) to bind to Background (which takes a Brush), because WPF itself defines a conversion between string and Brush (by creating a SolidColorBrush with that colour). But occasionally these limits may bite you (eg. when binding to a property of type object, thereby providing no type hints for the conversion); that doesn’t mean that you can’t use the converter, but you may need to supply an object of the exact type you want (via {x:Static} or extended property assignment) rather than relying on type inference.

And in its current form you can’t use bindings, although you can use resources (as shown above). This is because the conversions aren’t defined as DependencyObjects, so can’t support a DependencyProperty; while it could be possible to change that, unless you start getting really tricky with the WPF code you can’t use bindings anyway, since a converter is usually in the resources and items in the resources typically don’t have access to the binding context of their containers. And usually when you start to want to get things that complicated you ought to be using view-model properties instead of complex bindings anyway.

Finally, there are some performance consequences. Whenever the source property changes, the converter is consulted and it has to attempt conversion and comparison on each entry in turn. These are usually too fast to notice, and property changes are usually infrequent enough for this to not matter, but it is something to bear in mind. (Also note that this is potentially not much different from the rest of the property binding and styling system in WPF, although there are some potential optimisation opportunities available there which this converter can’t take advantage of.)

But it’s still quite a handy converter to have in the toolbelt. :) I’m interested to hear about any other interesting uses people can come up with.

And so at last, here’s the code. Remember, you’ll also need the ConversionUtilities class:

/// <summary>Conversion specifier class for the LookupConverter.</summary>
public class LookupConversion
{
    public LookupConversion()
    {
        From = To = DependencyProperty.UnsetValue;
    }
 
    /// <summary>If the incoming object compares equal to this, then the conversion applies.</summary>
    public object From { get; set; }
 
    /// <summary>The object to use if this conversion applies.</summary>
    public object To { get; set; }
 
    public bool Matches(object value)
    {
        if (From == DependencyProperty.UnsetValue) { return true; }
        return ConversionUtilities.EqualsWithConversion(From, value);
    }
 
    public bool MatchesBack(object value)
    {
        if (To == DependencyProperty.UnsetValue) { return false; }
        return ConversionUtilities.EqualsWithConversion(To, value);
    }
}
 
/// <summary>
/// Looks up a value against a list of conversions (<see cref="LookupConversion" /> objects) in order.
/// The first conversion that is satisfied will determine the resulting output value.
/// </summary>
[ContentProperty("Conversions")]
public class LookupConverter : IValueConverter
{
    private readonly List<LookupConversion> _Conversions = new List<LookupConversion>();
    public List<LookupConversion> Conversions
    {
        get { return _Conversions; }
    }
 
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        foreach (var conversion in Conversions)
        {
            if (conversion.Matches(value))
            {
                return ConversionUtilities.Convert(conversion.To, targetType);
            }
        }
        return DependencyProperty.UnsetValue;
    }
 
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        foreach (var conversion in Conversions)
        {
            if (conversion.MatchesBack(value))
            {
                return ConversionUtilities.Convert(conversion.From, targetType);
            }
        }
        return DependencyProperty.UnsetValue;
    }
}

(Yep, that’s right, this is a bidirectional converter, so it can be used on TwoWay properties — but having said that, I’ve yet to find a case when that’s useful.)

3 comments to A Tableau of Conversion


  • Catchable fatal error: Object of class WP_Comment could not be converted to string in /home/uecasm/public_html/lambert/wpdata/wp-content/themes/atahualpa/functions/bfa_custom_comments.php on line 16