Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF
Article

Piping Value Converters in WPF

Rate me:
Please Sign up or sign in to vote.
4.90/5 (48 votes)
14 Nov 2006CPOL8 min read 241.5K   2.6K   76   45
Demonstrates how to chain together value converters used in WPF data binding.

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:

XML
<?xml version="1.0" encoding="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:

C#
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:

C#
[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):

<PRE lang=xml><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> 
    <!-- Converts the Status attribute text to a SolidColorBrush used to draw 
         the output of statusDisplayNameGroup. -->
    <local:ValueConverterGroup x:Key="statusForegroundGroup">
      <local:IntegerStringToProcessingStateConverter  />
      <local:ProcessingStateToColorConverter />
      <local:ColorToSolidColorBrushConverter />
    </local:ValueConverterGroup> 
<!-- Converts the Status attribute to the tooltip message for
 that processing state. -->
<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):

 Sample screenshot

As you can see in the XAML above, the demo project creates several ValueConverterGroups. 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:

C#
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 the converter returns 'DoNothing' 
  // then the binding operation should terminate.
  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:

C#
/* In the ValueConverterGroup class */

// Fields
private readonly ObservableCollection<IValueConverter> converters = 
    new ObservableCollection<IValueConverter>();

private readonly Dictionary<IValueConverter,ValueConversionAttribute> 
cachedAttributes = new Dictionary<IValueConverter,ValueConversionAttribute>(); 

// Constructor
public ValueConverterGroup()
{
 this.converters.CollectionChanged += 
  this.OnConvertersCollectionChanged;
} 

// Callback
void OnConvertersCollectionChanged( 
    object sender, NotifyCollectionChangedEventArgs e )
{
 // The 'Converters' collection has been modified, so validate that each 
 // value converter it now contains is decorated with ValueConversionAttribute
 // and then cache the attribute value.
 
 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:

C#
protected virtual Type GetTargetType( 
    int converterIndex, Type finalTargetType, bool convert )
{
 // If the current converter is not the last/first in the list, 
 // get a reference to the next/previous converter.
 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]; 

  // If the Convert method is going to be called, 
  // we need to use the SourceType of the next 
  // converter in the list.  If ConvertBack is called, use the TargetType.
  return convert ? attr.SourceType : attr.TargetType; 
 } 
 // If the current converter is the last one to be executed return the target 
 // type passed into the conversion method.
 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 ;)

C#
[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:

XML
<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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Josh creates software, for iOS and Windows.

He works at Black Pixel as a Senior Developer.

Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.

Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.

Check out his Advanced MVVM[^] book.

Visit his WPF blog[^] or stop by his iOS blog[^].

See his website Josh Smith Digital[^].

Comments and Discussions

 
GeneralMy vote of 5 Pin
DotNetMastermind5-Dec-12 23:29
DotNetMastermind5-Dec-12 23:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.