GenericEnumConverter for WPF / Silverlight - Convert Enums to Bool or Visibility

by OSgAgA 22. March 2011 00:02

Now here is a problem i stumbled upon in at least three different projects in slightly different variations. So I tried to make a generic approach, which can be reused in the future.

What is the problem, we are talking about? Well, lets say you have a property in your data context that is of type MyEnum:

public enum MyEnum : int
{
Value1 = 0,
Value2 = 1,
Value3 = 2
}

Now you want to bind this enum to one of the following:

  1. A radio button group.
  2. Checkboxes.
  3. A visibility property (like in: this control is only visible if the property has value: Value2).

There are some solutions for this problem that have been presented in different blogs, but each of them covers only a part of the solution that is presented here. The goal of this post is to create a flexible "generic" solution that can be reused in different scenarios.

1. Basic Concept

Obviously what we need here is a value converter. A value converter can be part of a binding and converts a value from a source data type to a destination data type and back. First we will have a look at the "Convert" method, which will convert the value from the property type (=MyEnum) to the source type (=bool or Visibility). Later in this article we will have a look at the "ConvertBack" method.

We first need a class that derives from value converter and defines some type checks (we only support enum as a source and boolean or visibility as a destination type).

 

public class GenericEnumConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null)
{
throw new ArgumentNullException("value");
}

if (!(value is Enum))
{
throw new ArgumentException("Value must be an enum", "value");
}

if (parameter == null)
{
throw new ArgumentNullException("parameter");
}

bool result = false;

// ... enter code here.

if (targetType == typeof(bool) || targetType == typeof(bool?))
{
return result;
}
else if (targetType == typeof(Visibility))
{
if (result)
{
return Visibility.Visible;
}
else
{
return Visibility.Collapsed;
}
}
else
{
throw new ArgumentException("Epected target type bool", "targetType");
}
}

 

One word about the exceptions. I'm throwing an exception if the converter is in a situation,where it is not prepared for, because I like my routines to crash early, if an error occurs.

You may argue, that this is not the only possible way to deal with this situation and you are right. The alternatives are:

 

  1. Return value: This will return the value, that is given as a parameter to the convert method, resulting in the same behaviour that your are used to in ordinary databinding (an error message is printed to the output window).
  2. Return DependencyProperty.UnsetValue: Just ignores any changes that the converter cannot deal with.

You must decide for yourself, which approach you would like to use.

2. The string approach

Now that we have the basics done we are getting to the more exciting part. We now want to implement the following scenarios:

<RadioButton GroupName="MyGroup" HorizontalAlignment="Center" 
IsChecked="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter=Value1}">
Value 1
</RadioButton>
<RadioButton GroupName="MyGroup" HorizontalAlignment="Center"
IsChecked="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter=Value2}">
Value 2
</RadioButton>
<RadioButton GroupName="MyGroup" HorizontalAlignment="Center"
IsChecked="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter=Value3}">
Value 3
</RadioButton>

<TextBlock Margin="10" HorizontalAlignment="Center"
Visibility="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter=Value3}">
Visible
</TextBlock>

As you can see, the Binding is done using our converter and a converter parameter providing a value as a string. So we need to parse that string and map it to the corresponding enum value. This is pretty simple: Just place the following code in the "enter code here" section of the previous example:

if (parameter is string)
{
result = Enum.Equals(value, Enum.Parse(value.GetType(), parameter.ToString().Trim(), true));
}

This will do the trick. If you try this example you will see, that the radio button is set according to the value of the property. This approach is pretty nice as it works everywhere in the WPF universe, be it WPF, Surface, Silverlight or WP7. But it has one drawback: You don't have a compile time check if the provided converter parameter is correct.

3. The Enum approach

To get a compile time check for the provided Converter parameter, we must use an enum as the parameter type. This can be done using the static keyword.

// ...
xmlns:local="clr-namespace:EnumConverter"
// ...

<RadioButton GroupName="MyGroup" HorizontalAlignment="Center"
IsChecked="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter={x:Static local:MyEnum.Value1}}">
Value 1
</RadioButton>
<RadioButton GroupName="MyGroup" HorizontalAlignment="Center"
IsChecked="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter={x:Static local:MyEnum.Value2}}">
Value 1
</RadioButton>
<RadioButton GroupName="MyGroup" HorizontalAlignment="Center"
IsChecked="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter={x:Static local:MyEnum.Value3}}">
Value 1
</RadioButton>

<TextBlock Margin="10" HorizontalAlignment="Center"
Visibility="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter={x:Static local:MyEnum.Value1}}">
Visible
</TextBlock>

You will notice that when you change the enum (for example you rename Value1 to Value42) that this will result in a compile time error. In the previous section that would have compiled without issues.

As we now have an enum the code for the converter is pretty simple: (place at "enter code here").

if (parameter is Enum)
{
result = Enum.Equals(value, parameter);
}

This is pretty nice, but - again - has a drawback: The static keyword used here is not available in Silverlight and WP7. The "String approach" must be used in these scenarios.

4. The underlying Type approach

The enum we are looking at has an underlying type (int). So each value of the enum is associated with an int value. We can use this as a parameter as well. But be careful what you are doing here, as the value may not be distinct as can be seen in the following example:

public enum MyEnum : int
{
Value1 = 0,
Value2 = 0,
Value3 = 2
}

If you are dealing with anything like that you should really know what you are doing. You have been warned.

So for now, just assume, that we have an enum with distinct values. What we want to do is something like:

<RadioButton GroupName="MyGroup" HorizontalAlignment="Center" 
IsChecked="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter=0}">
Value 1
</RadioButton>
<RadioButton GroupName="MyGroup" HorizontalAlignment="Center"
IsChecked="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter=1}">
Value 2
</RadioButton>
<RadioButton GroupName="MyGroup" HorizontalAlignment="Center"
IsChecked="{Binding Path=MyValue, Converter={StaticResource GenericEnumConverter}, ConverterParameter=2}">
Value 3
</RadioButton>

Interesting enough this will allready work when the string approach is being used, as the Enum.Parse method is able to handle this situation.

To make it a little bit more interesting it may be fun to really use the underlying type, instead of a string. For that we need a static class like this:

public static class IntValues
{
public static int Zero { get { return 0; } }

public static int One { get { return 1; } }

public static int Two { get { return 2; } }

}

With that we want to do the following:

<RadioButton GroupName="MyGroup1" HorizontalAlignment="Center" 
IsChecked="{Binding Path=MyValue2, Converter={StaticResource GenericEnumConverter}, ConverterParameter={x:Static local:IntValues.Zero}}">
Value 1
</RadioButton>
<RadioButton GroupName="MyGroup1" HorizontalAlignment="Center"
IsChecked="{Binding Path=MyValue2, Converter={StaticResource GenericEnumConverter}, ConverterParameter={x:Static local:IntValues.One}}">
Value 2
</RadioButton>
<RadioButton GroupName="MyGroup1" HorizontalAlignment="Center"
IsChecked="{Binding Path=MyValue2, Converter={StaticResource GenericEnumConverter}, ConverterParameter={x:Static local:IntValues.Two}}">
Value 3
</RadioButton>

<TextBlock Margin="10" HorizontalAlignment="Center"
Visibility="{Binding Path=MyValue2, Converter={StaticResource GenericEnumConverter}, ConverterParameter={x:Static local:IntValues.Two}}">
Visible
</TextBlock>

To do this, lets use this code in the converter:

if (parameter.GetType() == Enum.GetUnderlyingType(value.GetType()))
{
object lhs = System.Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), null);
result = lhs.Equals(parameter);
}

5. ConvertBack

To support two way binding we need to provide a convert back method. So lets start again with a simple skeleton for this:

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null)
{
throw new ArgumentNullException("value");
}

if (!targetType.IsSubclassOf(typeof(Enum)))
{
throw new ArgumentException("Epected target type Enum", "targetType");
}

if (parameter == null)
{
throw new ArgumentNullException("parameter");
}

// ..enter code here.

throw new ArgumentException("Unknown paramater type.");
}

There is one big problem here. It is pretty easy to convert a true value to the corresponding enum, but how should a false value be converted? So lets say we have a converter parameter with the value: Value2 and the control sets a true value (radio button is checked) which should be converted to the enum then the method simply would return Value2. But what should be done, when the value is set to false (radio button is unchecked)? There are two solutions for that:

  1. Set the value to null.
    • Pro: For example radio buttons in a radio group may be in a state, where no button is selected. This would be the same as setting the property to null.
    • Con: If a (custom) radio button first sets a value to true and afterwards the previously selected to false (not the behaviour of the standard radio button!) then this strategy would lead to a situation where no value is set.
  2. Do not change the value. (= return DependencyProperty.UnsetValue)
    • Pro: If a (custom) radio button first sets a value to true and afterwards the previously selected to false (not the behaviour of the standard radio butto!n) then the correct value would be set.
    • Con: For example radio buttons in a radio group may be in a state, where no button is selected. This state would never be reached, as a false value will not change the property.

As can be seen both strategies have their pros and cons. Here we are using strategy 2, but that can easily be adjusted to strategy 1.

One additional word to two way bindings: Be very careful of what you are doing here. The approach presented will work pretty well with radion buttons, but will, i.e. not work with check boxes. I think there really is no generic approach for using two way bindings to an enum for checkboxes. What does it mean, if two checkboxes are checked? There may be answers to that in certain situations, like you can use the underlying type and apply an "or"-operation on it, but these are special scenarios which have to be decided on a case-by-case basis.

Finally the ConvertBack method:

if ((value is bool) || (value is bool?))
{
if ((bool?)value == false || (bool?)value == null)
{
return DependencyProperty.UnsetValue;
}
}
else if (value is Visibility)
{
if ((Visibility)value == Visibility.Collapsed)
{
return DependencyProperty.UnsetValue;
}
}
else
{
throw new ArgumentException("Value must be bool, bool? or Visibility", "value");
}


And the conversion:

if (parameter is Enum)
{
return parameter;
} else if (parameter is string || parameter.GetType() == Enum.GetUnderlyingType(targetType))
{
return Enum.Parse(targetType, parameter.ToString().Trim(), false);
}

 

6. Conclusion

Which approach will be best for you depends on many things like what framework you are using and if you are using the underlying type of the enum. You will find an overview in the following tables.

Framework compatibility
Framework String approach Enum approach Underlying type approach
WPF + + +
Surface + + +
Silverlight 4 + - +
WP7 (Silverlight 3) + - +
Feature comparison
Approach Compile Time Check Two way binding
String approach - +
Enum approach + +
Underlying type approach - Dangerous if values of underlying type are not distinct.

 

7. Download

The source code for the converter can be found under:

WPF/Surface: GenericEnumConverter.cs (6.06 kb) [Downloads: 503]

Silverlight: SLGenericEnumConverter.cs (6.06 kb) [Downloads: 468]

Tags: , , , , ,

Add comment




biuquote
  • Comment
  • Preview
Loading


Month List