Introduction
I have found that there are many cases in WPF binding when I want to do an evaluation that results in a true
or false
value and returns a value that is different depending on this result. This is similar to the conditional operator of C#. A common requirement WPF will be making a control Visible
or Collapsed
dependent on a flag value.
There are a number of options. One is to build a ValueConverter
for each case, but this keeps adding converters, and that takes time, and adds modules and code to the solution. Another is to add properties to the ViewModel
, but this adds what is View specific information to the ViewModel
(such as the Visibility.Visible
and Visibility.Collasped
) and adds complexity to the ViewModel
—I considered having two bool
values, one the inverse of the other, or the conversion of an enumeration to a binary value in a ViewModel
to be a bad code smell.
I have created a lot of ValueConverters
that basically evaluated a passed value Boolean value and returned a value dependent on this result; a converter will normally be designed for only a specific conversion. There are a number of frameworks that include a converter that will convert a bound Boolean value to Visibilbiy.Visible
if true
, otherwise Visibility.Collapsed
. However, there are cases where visibility will be different for different controls, dependent on what is effectively the attribute, or it may be that want Visibility.Hidden
instead. That means that either multiple converters must be created, or multiple dependent properties are needed in the ViewModel
.
To try to reduce the number of converters required, I first used added the ability to pass “Reverse” as the ConverterParameter
to reverse the result. This reduced the number of required converters; however this still meant there were still a lot of converters. So I came up with even a better idea—pass the desired values in the ConverterParameter
. Then the question was how to format the two values in the ConverterParameter
. It seemed obvious that should borrow for C# and so I used the “:
” as with the “?:
” operator. Among other things, this played nicely with XAML. Although my current requirement did not require a condition, I later added support for the condition parameter.
I initially put the code in each Converter, but decided quickly that I wanted to create a helper that meant that each Converter shared a common code base. This proved to make each converter very simple, and supported easy upgrades, and bug fixes. The default was for the value was boolean true
and false
if a ConverterParameter
was not included, but also left in the ability to specify “Reverse
” in the ConverterParameter
to make it like “false:true
” was set in the ConverterParameter
. This quickly paid dividends.
Initially, I was just returning string
s, depending on the TypeConverter
for each class to convert the string
to the required Type
. However, this broke for the Telerik controls, so had to add using the TypeConverter
in the converter to do the conversion to the correct type.
I also later added the ability to add a third value in the expression list for a null
value, which was added to the end of the ConverterParameter
with a “:
” separating it from the rest.
A later addition was building flexible IMultiValueConverters
. There were only three that seemed to make sense and that was for the IsEqualConverter
, IsFalseConverter
and IsTrueConverter
. The difference between the IsFalseConverter
and IsTrueConverter
is that for the IsFalseConverter
to be true
, all values must be false
... not null
, or something else, but false
. The IsTrueConverter
is exactly the opposite. These converters can be used with converters for each of the bindings, which is the reason that the true
or false
values are checked not only for the bool
true
or false
, but the string
"true
" or "false
." Makes it really easy to have a control's Visibility
or Enabled
properties be dependent on multiple values without having multiple enclosures in XAML, or special properties in the ViewModel
.
The IsEqualConverter
when used as an IMultiValueConverter
returns true
only if all values are the same, and equal to the ConverterParameter
string
before the "?" if there is a "?" in the ConverterParameter
.
Another enhancement I did for the IsEqualConverter
is allowing multiple match values with associated return values:
ConverterParameter="matchValue1?returnValue1: matchValue2?returnValue2:returnValueDefault"
Creating a Converter
I personally like to create all my ValueConverters
with the MarkupExtension
since it means I do not need to create a static
resource in the XAML. I also include the default constructor so that issues are not shown in the XAML because I use the MarkupExtension
and for some reason, Resharper (I think) does not recognize that there is an inherent default constructor, so one has to be there to keep a warning from being displayed. Prefer having warning in one place instead every time the converter is used.
The converter only needs one line of code, the code to call the helper method. Only the second argument for the helper method is not directly passed from the arguments in the Convert
method call. The second argument is specific to the purpose of the converter, and this is the condition function delegate (Func<T, bool?>
) that is used to determine which true
or false
(or null
) value specified in the parameter argument should be returned.
public sealed class IsFalseConverter : MarkupExtension, IValueConverter
{
public IsFalseConverter() { }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return NullableBooleanConverterHelper.Result<bool?>
(value, i => !i, targetType, parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
Using IValueConverter
Since I use the MarkupExtension
, the converter parameter for the binding specifies the namespace
prefix associated with the namespace
for the converter with the class name for the converter. Just need to do normal binding syntax for a binding with a converter, and includes a converter parameter that specifies the values to return for true
and false
results. In this code, a converter sets the title for the ToggleButton
title dependent on whether the ToggleButton
is checked or not. Also, there is a converter that will disable the button if a TextBox
does not have content. In this case, use "reverse
” in the ConverterParameter
to flip the result.
<ToggleButton Name="ShowHideButton" HorizontalAlignment="Center"
IsEnabled="{Binding Text,
ElementName=ReasonTextBox,
Converter={local:IsNullOrWhiteSpaceConverter},
ConverterParameter=reverse}"
Content="{Binding IsChecked,
ElementName=ShowHideButton,
Converter={local:IsFalseConverter},
ConverterParameter=Show:Hide}"/>
Since I use the MarkupExtension
, the converter parameter for the binding specifies the namespace
prefix associated with the namespace
for the converter with the class name for the converter. Just need to do normal binding syntax for a binding with a converter, and includes a converter parameter that specifies the values to return for true
and false
results. In this code, a converter sets the title for the ToggleButton
title dependent on whether the ToggleButton
is checked or not. Also, there is a converter that will disable the button if a TextBox
does not have content. In this case, use ‘reverse
” in the ConverterParameter
to flip the result.
IsFalseConverter & IsTrueConverter IMultivalueConverter
In several of the converters, I have included an implementation for IMultiValueConverter
. In the case of the IMultiValueConverter
implementation for the IsFalseConverter
, the implementation requires that all the values resolve to false
:
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var isFalse = values.All(i => (i != null) &&
(i as bool? == false || i.ToString() == "false"));
return ConverterHelper.Result<bool?>(isFalse, i => i, targetType, parameter);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
As can be seen from the code, this is not nearly as flexible as the IValueConverter
, although it can be made more so, but the benefits are probably minimal since a IValueConverter
can be used on each Binding
within a MultiBinding
. I did change the calculation of the first line so that first a check is made for null
, avoiding any of the later checking. Originally, I let null
values propagate, and let a null
check on the ToString()
method catch the null
, value, but suspect that early null
check will improve performance.
The following line should be explained:
var isFalse = values.All(i => (i != null) &&
(i as bool? == false || i.ToString() == "false"));
Normally, it would not be a problem to just use the (i as bool?) == false
, since automatic conversion would normally convert to the required bool
type, but when using a converter within a Binding
within a MultiBinding
, the value is converted to a string
, so also need the i.ToString() == "false"
. I did change the calculation to first a check for null
, avoiding any of the later checking. Originally, I let null
values propagate, and let a null
check on the ToString()
method catch the null
, value, but suspect that early null
check will improve performance.
Here is an example of using the IsTrueConverter
in a MultiBinding
:
<RowDefinition.Height>
<MultiBinding Converter="{converterHelperSample:IsTrueConverter}"
ConverterParameter="*:0">
<Binding Converter="{converterHelperSample:IsTrueConverter}"
ElementName="ShowHideButton"
Path="IsChecked" />
<Binding Converter="{converterHelperSample:IsNullOrEmptyConverter}"
ConverterParameter="reverse"
ElementName="ReasonTextBox"
Path="Text" />
</MultiBinding>
</RowDefinition.Height>
IsEqualConverter Multiple Value Comparison
An extension that was added is the ability to return a specific value for a specific input value:
<TextBlock Grid.Row="4"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding ElementName=TestComboBox,
Path=Text,
Converter={converterHelperSample:IsEqualConverter},
ConverterParameter='a?a selected:b?b selected:other value selected'}" />
Thus, in this sample, when the input value is 'a
', the returned value is 'a selected
', and when the input value is 'b
', the returned value is 'b selected
', and otherwise the value is 'other value selected
'.
The IsEqualConverter
code is as follows:
public sealed class IsEqualConverter : MarkupExtension, IValueConverter, IMultiValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null || parameter == null) return DependencyProperty.UnsetValue;
return ConverterHelper.ResultWithParameterValue(
p => String.Equals(value.ToString(), p, StringComparison.CurrentCultureIgnoreCase),
targetType, parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{ throw new NotImplementedException(); }
}
The code in the ConverterHelper
class that supports this is:
public static object ResultWithParameterValue(Func<string, bool?> comparer, Type targetType,
object parameter, object nullValue = null, object trueValue = null, object falseValue = null)
{
var parameterString = parameter.ToString();
var compareItems = parameterString.Split(':').Select(i => i.Split('?').ToArray()).ToArray();
if (compareItems.Length > 1)
{
for (int i = 0; i < compareItems.Length - 1; i++)
{
for (int j = 0; j < compareItems[i].Length - 1; j++)
{
var returnValue = Result(comparer(compareItems[i][j]), typeof(string), compareItems[i][j],
null, compareItems[i][compareItems[i].Length - 1], ":");
if (returnValue.ToString().ToLower() != ":") return ConvertToType(returnValue, targetType);
}
}
return ConvertToType(compareItems.Last().First(), targetType);
}
return ConvertToType(parameterString, targetType);
}
Basically, what happens is create an array of string
that using the Split
method on the ":
" and then on the "?
". Then, it would assume that all elements of the top level array had at least two elements with the last element having only one. Then, it can iterate through the arrays looking for a match.
Had issues with using the Result
method, and the fix was that if a false
value is not specified for the Result
method, then the string
":
" is returned. This may cause some issues in some cases, but considering that it is not possible to use this in one of the string
s since the ":
" is used as a delimiter, not very likely. The ResultWithParameterValue
is only used with the IsEqualConverter
:
Future
I have created only a few of the many converters possible using this helper. I have included the following converters in the sample:
IsCollectionNotEmptyConverter
: Will return true
value if the type is of IEnumerable
and there are any values. IsEqualConverter
: Returns true
value if the ToString()
of the value argument (Path
) is equal to the string before the "?
" in the parameter argument (ConveterParameter
). It can also be used as a IMultiValueConverter
, in which as all values have to be equal to the value specified in the ConverterParameter
, or to each other. IsTrueConverter
: As with the IsFalseConverter
, but reversed IsNullConverter
: Returns the true
value if the value argument is null
IsNullOrEmptyConverter
: Returns the true
value if the value argument is null
, or the ToString()
value returns true
for the IsNullOrWhiteSpace
method. SignConverter
: Will return the first value in the ConverterParameter
if value is positive, the second value if negative, and the final value is 0
I believe that this implementation is a big advance over using many converters, being more flexible, and easy for developers to use and understand in the code since it uses something similar to what is in the C# language. It is also very easy to extend.
Ideally, I would like to be able to pass a lambda expression as the ConverterParameter
. This would not add a lot of text in the simple case since I can use the conditional operator similar to the current implementation, but it would require a lot more time to develop than I have put into this implementation. What could be done with a lambda expression would be so much more powerful since I could use it to do calculations.
Personally, I think Microsoft should have continued enhancement of the XAML infrastructure, including being able to use a lambda expression in the binding. This would have eliminated the need many times for using the IValueConverter
and triggers. Really too bad because I think the XAML technology is so great and could be so much better.
Converter for Items in Collection
There is a tip I have created which shows how to use this helper class and a Binding
to the Count
property of the ObservableCollection
to determine if a collection has items in it, and then that can be used to control elements of a View
--IValueConverter to determine if Collection has items.
History
- 08/09/15: Initial version
- 05/11/15: 2015 version
- 07/20/16: Bug fixes
- 07/29/16: Improved the
IsEqualConverter
to handle more options - 10/05/16: Added XML comments to several converters
- 08/11/17: Included reference to tip on
Binding
to ObervableCollection
Count
property