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