Click here to Skip to main content
15,886,199 members
Articles / Desktop Programming / WPF

Using the ConditionalValueConverter to evaluate comparisons in WPF Binding expressions

Rate me:
Please Sign up or sign in to vote.
4.43/5 (4 votes)
8 Feb 2009Public Domain5 min read 43.9K   363   16   3
Output values of one type, based on a comparison of values of another type.

Introduction

In WPF, you will end up doing a lot of data binding. For example, you can bind the IsChecked of a menu item to the Enabled state of some feature in your product, or you can bind the Foreground brush of a TextBlock to a static resource of a red brush.

But, what if you want to bind the Foreground brush of that text to Red for negative and Black for positive? Or, what if you want to bind IsChecked to "!Enabled"? Well, you have to write a TypeConverter. Even for something as simple as negating a boolean, you have to write custom code and reference it in your XAML file.

The problem

After having written a few of those type converters, I quickly realized that 90% of them boil down to the same thing: "If the input has a certain value, return one output value, else return another output value." The output values are generally known, and often not of the same type as the input value. Wouldn't it be cool if that behavior could be written just once, and re-used in your entire application? That would cut down on code development and testing time, and lead to fewer code bugs.

The solution

Enter ConditionalValueConverter. It accepts a reference value (expressed as a string, called Reference), and two output values (expressed as string, with a specified value type, the ValueType). It returns one of the values (TrueValue) if the input value is equal to the reference value, and the other value otherwise (FalseValue). In your XAML, you would bind it something like this:

XML
<Button Foreground="{Bind Path=Some.Path.To.A.Boolean, TypeConverter= 
    {local:ConditionalValueConverter Reference=true, ValueType=Brush, 
        TrueValue=Green, FalseValue=Red}}"
    Name="theButton" Click="OnClick">Click Me</Button>

For this to work, the "local" XML namespace needs to be defined as the namespace that ConditionalValueConverter lives in. But, if you're already using custom bindings, you probably already know that.

Now, how does this work?

When a binding is evaluated, the value can optionally be passed through a ValueConverter. When you bind a text box to a slider, the value will automatically be converted between double and string -- you don't need to do anything special for that. However, for custom types, automatic conversion may not be available, and you have to write your own ValueConverter. That's what ValueConverter was originally intended to solve.

Because you can specify the ValueConverter to be used on a per-binding level, you can start playing tricks with the input value. All that a ValueConverter has to do is accept an input value of a specific type (the type of the input property) and provide an output of another type (the type of the destination of the binding). As long as those rules are followed, anything goes, and developers have been quite creative in overloading this mechanism to put user interface presentation code into a combination of Binding and code-behind.

Implementation details

You configure ValueConverter with a Reference value (to compare to the input), a desired ValueType of the outputs, and the TrueValue and FalseValue that will be returned (coerced to the ValueType) when needed. This is all implemented as basic C# properties:

C#
public string Reference { get; set; } 
object trueValue_;
object setTrueValue_;
public object TrueValue { get { return trueValue_; } 
                          set { setTrueValue_ = value; MakeTrue(); } }
object falseValue_;
object setFalseValue_;
public object FalseValue { get { return falseValue_; } 
                           set { setFalseValue_ = value; MakeFalse(); } }
Type valueType_;

public string ValueType { get { return valueType_.Name; } 
                          set { valueType_ = GetValueType(value); 
                                MakeTrue(); MakeFalse(); } }

What is the output type?

You will note that a few helper functions are invoked to set up the right values and types when you change one of these properties. Because the order of property setting is not guaranteed, you have to attempt the conversion of the values both when changing the actual value (as string) and when changing the expected value type. Let's start with the type. We simply take the string, and return the type for that string. Unfortunately, Type.GetType(string) does not recognize basic types like System.Boolean or System.Double, so we have to first test for those explicitly.

C#
Type GetValueType(string name) 
{
  if (name == "float" || name == "System.Single")
    return typeof(float);
  if (name == "double" || name == "System.Double")
    return typeof(double);
  if (name == "int" || name == "System.Int32")
    return typeof(int);
  if (name == "string" || name == "System.String")
    return typeof(string);
  if (name == "bool" || name == "System.Boolean")
    return typeof(bool);
  return Type.GetType(name);
}

Configuring the return values (for true and false)

Then, when we set the TrueValue (or FalseValue) property, we have to convert the value to the expected type. This requires a few initial checks before actually doing the conversion:

C#
void MakeTrue() 
{
  if (setTrueValue_ == null || valueType_ == null)
    return;
  if (setTrueValue_.GetType() == valueType_)
  {
    trueValue_ = setTrueValue_;
    return;
  }
  if (setTrueValue_.GetType() != typeof(string))
  {
    throw new InvalidOperationException(
        String.Format("Set type must be ValueType ({0}) or string " + 
                      "for ConditionalValueConverter.TrueValue. Got type {1}.",
                      valueType_.Name, setTrueValue_.GetType().Name));
  }
  trueValue_ = TypeDescriptor.GetConverter(valueType_).
                  ConvertFromInvariantString((string)setTrueValue_);
}

We don't do conversion until we have both a value (typically a string) and a type. Additionally, if the value that is set (in setTrueValue_) is already of the correct type, we don't need to convert; just remember that it's the value to use (stored in trueValue_). Finally, if the set value is not a string, and not of the expected type, we decide that the user has done something wrong and report it through an exception. After that, we use the TypeDescriptor.GetConverter() function to find the appropriate type converter, and convert from string to value. Because programming in C# (and XAML) typically is done in the "invariant" culture (where decimals use periods, etc.), we use the ConvertFromInvariantString() function.

Actually converting types

Finally, the actual work of the type converter:

C#
public object Convert(object value, Type targetType, 
              object parameter, System.Globalization.CultureInfo culture)
{
  if (targetType != valueType_)
    throw new System.NotSupportedException();
  if (value == null || Reference == null)
    return ((object)value == (object)Reference) ? trueValue_ : falseValue_;
  object r = Reference;
  if (value.GetType() != Reference.GetType())
    r = TypeDescriptor.GetConverter(value).ConvertFrom(r);
  if (value.Equals(r))
    return trueValue_;
  return falseValue_;
}

Again, there are some sanity checks. For example, we can't convert to a type other than the type that's configured. If either the reference, or the value to convert, is null, then we return true only if both are null. We also may need to convert the reference (which is string) to the type of the value we're converting -- for example, if the input property is a boolean. Once all of that is done, we compare to the (converted) reference value, and return the "true" value or the "false" value, depending on the outcome.

Advanced usage

There are a few additional advanced options. Because TrueValue and FalseValue are properties of the Type object, you can actually use a StaticResource binding to set them, rather than string values. This is why we're testing the type of the property inside the MakeTrue() (and MakeFalse()) functions.

Also, you can instantiate a ConditionalValueConverter as a Resource, and use a StaticResource reference when specifying the ValueConverter for your Binding object. This allows you to re-use the same object (say, one that colors text by sign) across multiple bindings, saving parsing type and memory in your application.

XML
<Window.Resources> 
  <local:ConditionalValueConverter Reference="true" TrueValue="Black" 
      FalseValue="Red" ValueType="Brush" x:Key=redOrBlack />
...
  <Button Background="{Bind Path=Some.Boolean, 
                       ValueConverter={StaticResource redOrBlack}}" />

Well, that is it. This article probably took longer to write than the code, but given that I haven't found the same code elsewhere, I figured that others might find it as useful as I do!

Version history

  • Version 1.0 -- February 9, 2009.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionstatic Type GetValueType(string name) { Pin
Taneth15-May-12 8:58
professionalTaneth15-May-12 8:58 
GeneralWhy use reference instead of parameter Pin
Dave Midgley18-Apr-11 0:45
Dave Midgley18-Apr-11 0:45 
GeneralA nice little utility class Pin
Colin Eberhardt8-Feb-09 21:30
Colin Eberhardt8-Feb-09 21:30 

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.