Converting data bound values

There are many times when developing a WPF application, when we need to convert a data bound property value to a different type. For example, we might want to control the visibility of some UI elements with a bool property in our View Model, so that we can avoid having the UI-related Visibility enumeration instance in it.

We might want to convert different enumeration members to different Brush objects, or collections to string representations of the contained collection items. We've already seen a number of examples of the IValueConverter interface, but let's now take a bit more of a thorough look:

public interface IValueConverter 
{ 
  object Convert(object value, Type targetType, object parameter,  
    CultureInfo culture); 
  object ConvertBack(object value, Type targetType, object parameter,  
    CultureInfo culture); 
} 

As we've already seen, the value input parameter of type object is the data bound value of the binding. The object return type relates to the converted value that we want to return. The targetType input parameter specifies the type of the binding target property and is typically used to validate the input value to ensure that the converter is being used with the expected type of data.

The parameter input parameter is optionally used to pass an additional value through to the converter. If used, its value can be set using the Binding.ConverterParameter property. Finally, the culture input parameter provides us with a CultureInfo object to correctly format textual output, when working in a culturally-sensitive application. We'll return to this in a moment, but let's first look at an example of a converter that uses the parameter input parameter:

using System; 
using System.Globalization; 
using System.Windows; 
using System.Windows.Data; 
 
namespace CompanyName.ApplicationName.Converters 
{ 
  [ValueConversion(typeof(Enum), typeof(bool))] 
  public class EnumToBoolConverter : IValueConverter 
  { 
    public bool IsInverted { get; set; } 
 
    public object Convert(object value, Type targetType, object parameter,
      CultureInfo culture) 
    { 
      if (value == null || parameter == null || (value.GetType() !=
        typeof(Enum) && value.GetType().BaseType != typeof(Enum)))  
        return DependencyProperty.UnsetValue; 
      string enumValue = value.ToString(); 
      string targetValue = parameter.ToString(); 
      bool boolValue = enumValue.Equals(targetValue,  
        StringComparison.InvariantCultureIgnoreCase); 
      return IsInverted ? !boolValue : boolValue; 
    } 
 
    public object ConvertBack(object value, Type targetType, 
      object parameter, CultureInfo culture)
    { 
      if (value == null || parameter == null) 
        return DependencyProperty.UnsetValue; 
      bool boolValue = (bool)value; 
      string targetValue = parameter.ToString(); 
      if ((boolValue && !IsInverted) || (!boolValue && IsInverted))  
        return Enum.Parse(targetType, targetValue); 
      return DependencyProperty.UnsetValue; 
    } 
  } 
} 

The idea of this converter is that we can data bind an enumeration property to a RadioButton or CheckBox control that specifies the name of a particular member. If the value of the data bound property matches the specified member, then the converter will return true and check the control. For all other enumeration members, the control will be unchecked. We could then specify a different member in each of a group of RadioButton controls, so that each member could be set.

In the class, we start by specifying the data types that are involved in the implementation of the converter in the ValueConversion attribute. Next, we see the IsInverted property that we saw in the BaseVisibilityConverter class that enables us to invert the output of the converter.

In the Convert method, we first check the validity of our value and parameter input parameters, and return the DependencyProperty.UnsetValue value if either are invalid. For valid values, we convert both parameters to their string representations. We then create a bool value by comparing the two string values. Once we have our bool value, we use it in conjunction with the IsInverted property to return the output value.

As with our other enumeration converter example, the ConvertBack method implementation is a little different again, as we are unable to return the correct enumeration instance for a false value; it could be any value except the value specified by the parameter input parameter.

As such, we are only able to return the specified enumeration instance if the data bound value is true and the IsInverted property is false, or if it is false and the IsInverted property is true. For all other input values, we simply return the DependencyProperty.UnsetValue property, which is preferred by the property system rather than the null value.

Let's see an example of this in use, with the BitRate enumeration that we saw in the previous chapter. Let's first look at the simple View Model:

using System.Collections.ObjectModel; 
using CompanyName.ApplicationName.DataModels.Enums;  
using CompanyName.ApplicationName.Extensions; 
 
namespace CompanyName.ApplicationName.ViewModels 
{ 
  public class BitRateViewModel : BaseViewModel 
  { 
    private ObservableCollection<BitRate> bitRates = 
      new ObservableCollection<BitRate>();
    private BitRate bitRate = BitRate.Sixteen; 
 
    public BitRateViewModel() 
    { 
      bitRates.FillWithMembers(); 
    } 
 
    public ObservableCollection<BitRate> BitRates 
    { 
      get { return bitRates; } 
      set { if (bitRates != value) { bitRates = value; 
        NotifyPropertyChanged(); } } 
    } 
 
    public BitRate BitRate 
    { 
      get { return bitRate; } 
      set { if (bitRate != value) { bitRate = value;  
        NotifyPropertyChanged(); } } 
    } 
  } 
}

This class just contains a collection of type BitRate, which will hold all possible members and a selection property of type BitRate, which we will data bind to the various RadioButton elements using our new converter.

Note the use of the FillWithMembers Extension Method in the constructor. Let's see that first:

public static void FillWithMembers<T>(this ICollection<T> collection) 
{ 
  if (typeof(T).BaseType != typeof(Enum)) 
    throw new ArgumentException("The FillWithMembers<T> method can only be
    called with an enum as the generic type.");
  collection.Clear(); 
  foreach (string name in Enum.GetNames(typeof(T)))  
    collection.Add((T)Enum.Parse(typeof(T), name)); 
} 

In the FillWithMembers Extension Method, we first check that the collection that the method is called on is of an enumeration type and throw an ArgumentException if it's not. We then clear the collection, in case it has any pre-existing items in it. Finally, we iterate through the result of the Enum.GetNames method, parsing each string name to the relevant enumeration member and casting it to the correct type, before adding it to the collection.

Let's now see the XAML for the View:

<UserControl x:Class="CompanyName.ApplicationName.Views.BitRateView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  xmlns:Converters="clr-namespace:CompanyName.ApplicationName.Converters; 
    assembly=CompanyName.ApplicationName.Converters"> 
  <UserControl.Resources> 
    <Converters:EnumToBoolConverter x:Key="EnumToBoolConverter" /> 
  </UserControl.Resources> 
  <GroupBox Header="Audio Quality" HorizontalAlignment="Left" 
    VerticalAlignment="Top" Padding="5"> 
    <StackPanel> 
      <RadioButton Content="16 bits" IsChecked="{Binding BitRate,  
        Converter={StaticResource EnumToBoolConverter},  
        ConverterParameter=Sixteen}" VerticalContentAlignment="Center" /> 
      <RadioButton Content="24 bits" IsChecked="{Binding BitRate,  
        Converter={StaticResource EnumToBoolConverter}, ConverterParameter=
        TwentyFour}" VerticalContentAlignment="Center" /> 
      <RadioButton Content="32 bits" IsChecked="{Binding BitRate,  
        Converter={StaticResource EnumToBoolConverter},  
        ConverterParameter=ThirtyTwo}" VerticalContentAlignment="Center" /> 
    </StackPanel> 
  </GroupBox> 
</UserControl> 

In this View, we set up the Converters XAML namespace prefix and then declare an instance of the EnumToBoolConverter class in the Resources section. We then declare a StackPanel containing three RadioButton elements inside a GroupBox. Each RadioButton element is data bound to the same BitRate property from our View Model, using the converter from the resources.

Each button specifies a different enumeration member in its binding's ConverterParameter property and this is passed through to the converter in the parameter input parameter. If a RadioButton is checked, its true value is passed to the converter and converted to the value specified by its ConverterParameter value and the BitRate property is updated with that value. The output of this code looks like the following figure:

Note that if we had a large number of enumeration members, or the members were changed regularly, declaring each one manually in the UI like this example might not be such a good idea. In these cases, we could generate the same UI with less work, utilizing a DataTemplate object. We'll see an example of this later in this chapter, but for now, let's return to the input parameters of our converter.

The final input parameter in the Convert and ConvertBack methods is the culture parameter of type CultureInfo. In non-international applications, we can simply ignore this parameter, however if globalization plays a part in your application, then using this parameter is essential.

It enables us to correctly format any textual output that we may have in our converter using the object.ToString method and keep it in line with the rest of the text in the application. We can also use it in the various Convert class methods to ensure that numerals are also correctly output in the right format. Globalization is beyond the scope of this book and so we'll move on now.