Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF IValueConverter/IMultiValueConverter Helper Class

0.00/5 (No votes)
2 Apr 2018 1  
Presents a class to help create flexible value and multi-value converters that use the ConverterParameter to specify return values for true and false results using the ?: operator

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 strings, 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++)
   {
    // ":" used as false value because it cannot be in the result string
    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 strings 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here