Introduction
This article examines how to combine multiple physical value converters into one logical value converter, in the context of data binding in the Windows Presentation Foundation. It is expected that the reader is already familiar with data binding in WPF and the use of XAML to declare user interface elements. This article does not explain the fundamentals of WPF data binding, but the reader can refer to this article in the Windows SDK for a comprehensive overview of WPF data binding, if necessary.
The code presented in this article was compiled and tested against the June 2006 CTP of the .NET Framework 3.0.
Background
The data binding infrastructure of the WPF is extremely flexible. One of the major contributors to that flexibility is the fact that a custom value converter can be injected between two bound objects (i.e. the data source and target). A value converter can be thought of as a black box into which a value is passed, and another value is emitted.
A value converter is any object which implements the IValueConverter interface. That interface exposes two methods: Convert
and ConvertBack
. Convert
is called when a bound value is being passed from the data source to the target, and ConvertBack
is called for the inverse operation. If a value converter decides that it cannot return a meaningful output value based on the input value, it can return Binding.DoNothing
, which will inform the data binding engine to not push the output value to the binding operation�s respective target.
The Problem
The WPF data binding support allows a Binding
object to have one value converter, which can be assigned to its Converter
property. Having a single converter for a binding operation is limiting because it forces your custom value converter classes to be very specific. For example, if you need to base the color of some text in the user interface on the value of a numeric XML attribute, you might be inclined to make a value converter which converts the XML attribute value to a number, then maps that number to an enum value, then maps that enum value to a Color
, and finally creates a Brush
from that color. This technique would work, but the value converter would not be reusable in many contexts.
It would be better if you could create a library of modular value converters and then somehow pipe them together, like how most command-line environments allow the output of one command to be piped into another command as input.
The Solution
In response to this problem, I created a class called ValueConverterGroup
. The ValueConverterGroup
class is a value converter (it implements IValueConverter
) which allows you to combine multiple value converters into a set. When the ValueConverterGroup
�s Convert
method is invoked, it delegates the call to the Convert
method of each value converter it contains. The first converter added to the group is called first, and the last converter added to the group is called last. The opposite occurs when the ConvertBack
method is called.
The output of one value converter in the group becomes the input of the next value converter, and the output of the last value converter is returned to the WPF data binding engine as the output value of the entire group. For the sake of convenience, I made it possible to declare a ValueConverterGroup
and its child value converters directly in a XAML file.
Using the Code
The following is a short demo which demonstrates how to use the ValueConverterGroup
class. The entire demo is available for download at the top of this article.
Here is the simple XML data used in the demo:
="1.0" ="utf-8"
<Tasks>
<Task Name="Paint the living room" Status="0" />
<Task Name="Wash the floor" Status="-1" />
<Task Name="Study WPF" Status="1" />
</Tasks>
The Status XML attribute will be mapped to values of this enum type:
public enum ProcessingState
{
[Description("The task is being performed.")]
Active,
[Description( "The task is finished." )]
Complete,
[Description( "The task is yet to be performed." )]
Pending,
[Description( "" )]
Unknown
}
The following are some custom value converters that will be piped together to convert the Status value to a SolidColorBrush
:
[ValueConversion( typeof( string ), typeof( ProcessingState ) )]
public class IntegerStringToProcessingStateConverter : IValueConverter
{
object IValueConverter.Convert(
object value, Type targetType, object parameter, CultureInfo culture )
{
int state;
bool numeric = Int32.TryParse( value as string, out state );
Debug.Assert( numeric, "value should be a String which contains a number" );
Debug.Assert( targetType.IsAssignableFrom( typeof( ProcessingState ) ),
"targetType should be ProcessingState" );
switch( state )
{
case -1:
return ProcessingState.Complete;
case 0:
return ProcessingState.Pending;
case +1:
return ProcessingState.Active;
}
return ProcessingState.Unknown;
}
object IValueConverter.ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture )
{
throw new NotSupportedException( "ConvertBack not supported." );
}
}
[ValueConversion( typeof( ProcessingState ), typeof( Color ) )]
public class ProcessingStateToColorConverter : IValueConverter
{
object IValueConverter.Convert(
object value, Type targetType, object parameter, CultureInfo culture )
{
Debug.Assert(value is ProcessingState, "value should be a ProcessingState");
Debug.Assert( targetType == typeof( Color ), "targetType should be Color" );
switch( (ProcessingState)value )
{
case ProcessingState.Pending:
return Colors.Red;
case ProcessingState.Complete:
return Colors.Gold;
case ProcessingState.Active:
return Colors.Green;
}
return Colors.Transparent;
}
object IValueConverter.ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture )
{
throw new NotSupportedException( "ConvertBack not supported." );
}
}
[ValueConversion( typeof( Color ), typeof( SolidColorBrush ) )]
public class ColorToSolidColorBrushConverter : IValueConverter
{
object IValueConverter.Convert(
object value, Type targetType, object parameter, CultureInfo culture )
{
Debug.Assert( value is Color, "value should be a Color" );
Debug.Assert( typeof( Brush ).IsAssignableFrom( targetType ),
"targetType should be Brush or derived from Brush" );
return new SolidColorBrush( (Color)value );
}
object IValueConverter.ConvertBack( object value, Type targetType,
object parameter, CultureInfo culture )
{
Debug.Assert(value is SolidColorBrush, "value should be a SolidColorBrush");
Debug.Assert( targetType == typeof( Color ), "targetType should be Color" );
return (value as SolidColorBrush).Color;
}
}
Lastly we have the XAML for the Window (the relevant portions are in bold):
<Window x:Class="PipedConverters.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PipedConverters"
Title="PipedConverters" Height="300" Width="300"
FontSize="18"
>
<Window.Resources>
<!-- Loads the Tasks XML data. -->
<XmlDataProvider
x:Key="xmlData"
Source="..\..\data.xml"
XPath="Tasks/Task" />
<!-- Converts the Status attribute text to the display name
for that processing state. -->
<local:ValueConverterGroup x:Key="statusDisplayNameGroup">
<local:IntegerStringToProcessingStateConverter />
<local:EnumToDisplayNameConverter />
</local:ValueConverterGroup>
<!---->
<local:ValueConverterGroup x:Key="statusForegroundGroup">
<local:IntegerStringToProcessingStateConverter />
<local:ProcessingStateToColorConverter />
<local:ColorToSolidColorBrushConverter />
</local:ValueConverterGroup>
<!---->
<local:ValueConverterGroup x:Key="statusDescriptionGroup">
<local:XmlAttributeToStringStateConverter />
<local:IntegerStringToProcessingStateConverter />
<local:EnumToDescriptionConverter />
</local:ValueConverterGroup>
<DataTemplate x:Key="taskItemTemplate">
<StackPanel
Margin="2"
Orientation="Horizontal"
ToolTip="{Binding XPath=@Status,
Converter={StaticResource statusDescriptionGroup}}"
>
<TextBlock Text="{Binding XPath=@Name}" />
<TextBlock Text=" (" xml:space="preserve" />
<TextBlock
Text="{Binding XPath=@Status,
Converter={StaticResource statusDisplayNameGroup}}"
Foreground="{Binding XPath=@Status,
Converter={StaticResource statusForegroundGroup}}" />
<TextBlock Text=")" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock
Background="Black"
Foreground="White"
HorizontalAlignment="Stretch"
Text="Tasks"
TextAlignment="Center" />
<ItemsControl
Grid.Row="1"
DataContext="{StaticResource xmlData}"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource taskItemTemplate}" />
</Grid>
</Window>
The window declared above looks like this (notice that the color of the task's status text depends on the status value):
As you can see in the XAML above, the demo project creates several ValueConverterGroup
s. The one which determines the foreground of the status text contains three child value converters. The first converter converts the Status attribute value from a number to a ProcessingState
enum value. The next converter maps the enum value to a Color
which is used to graphically represent that processing state. The last converter in the group creates a SolidColorBrush
from the color emitted by the previous converter.
The aggregated approach to value conversion presented above makes it possible for value converters to remain simple and have an easily defined purpose. This large advantage comes with a very small price. There is one requirement imposed on the value converters used in a ValueConverterGroup
. The value converter class must be decorated with the System.Windows.Data.ValueConversionAttribute
attribute exactly once. That attribute is used to specify the data type the converter expects the input value to be, and the data type of the object it will emit.
To understand why this restriction exists, it is necessary to look under the covers at how the ValueConverterGroup
class works and the requirements that it must satisfy.
How it Works
The remainder of this article discusses how the ValueConverterGroup
class works. You do not need to read this section in order to use the class.
The ValueConverterGroup
class is relatively simple. It merely delegates calls to its Convert
and ConvertBack
methods off to the value converters it contains. There were two aspects of creating this class that required some extra planning to get right. First let�s examine its implementation of IValueConverter.Convert
:
object IValueConverter.Convert(
object value, Type targetType, object parameter, CultureInfo culture )
{
object output = value;
for( int i = 0; i < this.Converters.Count; ++i )
{
IValueConverter converter = this.Converters[i];
Type currentTargetType = this.GetTargetType( i, targetType, true );
output = converter.Convert( output, currentTargetType, parameter, culture );
if( output == Binding.DoNothing )
break;
}
return output;
}
The process of converting the input value to an output value requires us to call into every value converter in the group. That�s simple. The problem is that each value converter has certain expectations regarding the targetType
argument. The overall conversion process might require that the output value is a Brush
, but the intermediate converters in the group might have completely different expectations for the type of object they are supposed to emit.
For example, the demo shown in the previous section of this article converts a string (which contains an integral value) to a SolidColorBrush
. Along the way, it converts the integer to a ProcessingState
enum value, and then that value to a Color
, and finally the Color
gets turned into a SolidColorBrush
. Only the last converter in the group expects to be emitting a brush, so only that converter should receive the original target type value which was passed into the ValueConverterGroup
�s Convert
method.
The solution to this problem is to require that all value converters added to the group are decorated with the ValueConversionAttribute
. Here is the code that enforces this requirement:
private readonly ObservableCollection<IValueConverter> converters =
new ObservableCollection<IValueConverter>();
private readonly Dictionary<IValueConverter,ValueConversionAttribute>
cachedAttributes = new Dictionary<IValueConverter,ValueConversionAttribute>();
public ValueConverterGroup()
{
this.converters.CollectionChanged +=
this.OnConvertersCollectionChanged;
}
void OnConvertersCollectionChanged(
object sender, NotifyCollectionChangedEventArgs e )
{
IList convertersToProcess = null;
if( e.Action == NotifyCollectionChangedAction.Add ||
e.Action == NotifyCollectionChangedAction.Replace )
{
convertersToProcess = e.NewItems;
}
else if( e.Action == NotifyCollectionChangedAction.Remove )
{
foreach( IValueConverter converter in e.OldItems )
this.cachedAttributes.Remove( converter );
}
else if( e.Action == NotifyCollectionChangedAction.Reset )
{
this.cachedAttributes.Clear();
convertersToProcess = this.converters;
}
if( convertersToProcess != null && convertersToProcess.Count > 0 )
{
foreach( IValueConverter converter in convertersToProcess )
{
object[] attributes = converter.GetType().GetCustomAttributes(
typeof( ValueConversionAttribute ), false );
if( attributes.Length != 1 )
throw new InvalidOperationException( "All value converters added to a " +
"ValueConverterGroup must be decorated with the " +
"ValueConversionAttribute attribute exactly once." );
this.cachedAttributes.Add(
converter, attributes[0] as ValueConversionAttribute );
}
}
}
When a value converter is added to the Converters
property (not shown above) the OnConvertersCollectionChanged
method is executed, and it will throw an exception if any of the converters are not decorated with the ValueConversionAttribute
. For performance reasons, the attribute instance tied to the value converter is cached once it has been retrieved.
Since each value converter in the group is guaranteed to explain what types it expects to deal with, the Convert
method can determine the target type for each converter. As seen in the Convert
method above, the following method is called before a value converter is executed:
protected virtual Type GetTargetType(
int converterIndex, Type finalTargetType, bool convert )
{
IValueConverter nextConverter = null;
if( convert )
{
if( converterIndex < this.Converters.Count - 1 )
{
nextConverter = this.Converters[converterIndex + 1];
}
}
else
{
if( converterIndex > 0 )
{
nextConverter = this.Converters[converterIndex - 1];
}
}
if( nextConverter != null )
{
ValueConversionAttribute attr = cachedAttributes[nextConverter];
return convert ? attr.SourceType : attr.TargetType;
}
return finalTargetType;
}
That method simply checks to see if the converter about to executed is the last or first in the group. If the Convert
method is being called and the current converter is not the last one in the group, the target type value is retrieved from SourceType
property on the ValueConversionAttribute
instance associated with the next converter in the list (the order of converter execution reverses when ConvertBack
is called). If ConvertBack
is executing, the TargetType
of the previous converter becomes the target type for the current converter. Note, the source and target semantics are swapped when dealing with ConvertBack
.
The second aspect of creating this class that was not immediately obvious to me was how to enable value converters to be easily added to the group in XAML. This was not exactly a difficult piece of code to write, it just took me a while to find how to do it ;)
[System.Windows.Markup.ContentProperty("Converters")]
public class ValueConverterGroup : IValueConverter
{
}
Basically, that attribute informs the WPF infrastructure that the Converters
property in this class is the property to add items to when adding child objects in XAML. Adding the ContentPropertyAttribute
to the class makes it possible to use the ValueConverterGroup
class in XAML like so:
<local:ValueConverterGroup x:Key="someConverterGroup">
<local:MyCustomConverter />
<local:YourCustomConverter />
</local:ValueConverterGroup>
Conclusion
By piping together value converters it is much easier to make them reusable in a number of ways.
Every value converter added to a ValueConverterGroup
must be decorated exactly once with the ValueConversionAttribute
.
When the Convert
operation occurs, the value converters in the group are executed in the order that they exist in the Converters
collection. When ConvertBack
is called, they are executed last-to-first.