Introduction
I have a background in Human Factors in computer systems, and I am a big fan of radio buttons. This is because users tend to prefer radio
buttons to other input options for multiple choices, at least for a small number of choices. There are two good reasons for this: faster input (single
click as opposed to two clicks for a combo box), and the options are readily obvious (which also makes it faster). Of course a list box can be used
for the same thing, but the visual impact is different.
One of the things that is undesirable in WPF is the need for radio buttons to be specified in the View. This is normally done because
there is no control in WPF that allows radio buttons to be defined in the ViewModel. Therefore radio buttons are almost always defined in the View, with
all the associated maintenance headaches of defining them in the View and ensuring that the View and ViewModel are aligned correctly as to what the
purpose of each radio button. Of course, the text associated with a radio button can use binding to either a static value or a property
in the ViewModel (which I believe adds too much to the ViewModel).
Probably the best way to define a group of associated radio buttons that will be combined into a single control is to be represented as an
enumeration. The problems with this approach are the support of only a subset of the enumeration values or a requirement for dynamic behavior. This
immediately means that a single binding cannot be used, and if the options are totally dynamic, the advantages of using an enumeration to drive the control
are lost. For the simplest, and most numerous case, where the control will provide all selections from an enumeration, a control can be created that looks
like radio buttons, and is as generic as a standard ListBox containing RadioButton controls, and only requires a single binding to an enumerated value.
Implementation
I had previously worked on using value converters to do the same thing, but there were some serious limitations having to do with reuse
of value converters in containers. It also required both the value converter and some XAML that either had to be replicated for each use, or using a style.
Creating a control that inherits from a ListBox is far superior and requires about the same amount of code, and still it provides a great deal
of flexibility in customization using XAML.
The trick in creating something that is easy to use without requiring XAML backing is creating the DataTemplate in the constructor. This DataTemplate
is required to define the RadioButton for each enumerated value. Microsoft originally had provided a FrameworkElementFactory class
for building DataTemplates (for some reason, Microsoft in their wisdom, did not see fit to allow DataTemplates to be created using standard
controls). Using a FrameworkElementFactory is cumbersome, and requires all controls within the DataTemplate to be added
as a FrameworkElementFactory. Apparently, this approach could not do everything that could be done in XAML, so Microsoft’s new recommendation is to create the
DataTemplate in XAML and use the XamlReader to parse the string:
public DataTemplate RadioButtonDataTemplate()
{
var xaml = @"<DataTemplate
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">
<Grid>
<RadioButton IsChecked=""{Binding IsChecked}""
Content=""{Binding Text}"" />
</Grid>
</DataTemplate>";
object load = XamlReader.Parse(xaml);
return (DataTemplate)load;
}
So in the control’s constructor, I just set the ItemTemplate to this DataTemplate that was dynamically created
and set the BorderThickness for the ListBox to “0” so that the ListBox border does not appear:
public EnumRadioButtonListBox()
{
ItemTemplate = RadioButtonDataTemplate();
BorderThickness = new Thickness(0);
}
The only other thing I need for this ListBox is the DependencyProperty for the enumerated value. I did not want to use the ItemsSource
or SelectedItem for the EnumerationValue in part because the DependencyProperty would be used for both, so neither was really appropriate,
and using a separate DependencyProperty meant that I would not interfere with the operation of the existing properties:
public object EnumerationValue
{
get { return (object)GetValue(EnumerationValueProperty); }
set { SetValue(EnumerationValueProperty, value); }
}
public static readonly DependencyProperty EnumerationValueProperty =
DependencyProperty.Register("EnumerationValue",
typeof(object), typeof(EnumRadioButtonListBox),
new UIPropertyMetadata(new PropertyChangedCallback(EnumerationChanged)));
Notice that there is a property changed callback defined for the DependencyProperty. This is obviously required since it is necessary
to respond to the changes in the enumerated value to update the list of radio buttons if the enumeration type is changed, and to update the radio buttons if the enumerated value is changed:
private static void EnumerationChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null)
return;
((EnumRadioButtonListBox)d).UpdateEnumerationValue(e.NewValue);
}
private void UpdateEnumerationValue(object value)
{
if (!value.GetType().IsEnum)
throw new Exception(string.Format(
"The type '{0}' is not an Enum type, and is not supported for “ +
"EnumerationValue", value.GetType()));
if (value.GetType() != _listType)
{
_listType = value.GetType();
_list = new List<EnumDrivenRadioButtonBinding>();
foreach (var item in Enum.GetValues(_listType))
_list.Add(new EnumDrivenRadioButtonBinding(item, EnumerationChanged));
ItemsSource = _list;
}
if (_value == null || !value.Equals(_value))
{
foreach (var item in _list)
item.UpdateIsChecked(value);
_value = value;
}
}
The first thing that is done is to ensure that the type of the value is an enumeration since there is no point continuing if it is not.
An exception is thrown if the value is not an enumeration.
Next a check is made to see if the enumeration type has changed so that the IEnumerable for the ItemsSource corresponds
to the values for the enumeration type. So that the options in the list box correspond to the current enumeration, the list has to be updated each time the
enumeration type is changed. To allow the radio buttons to operate correctly, a new class is required (the radio button ViewModel). It has a property for the
state of the radio button, and a property for the text to be associated with each radio button. In the constructor for this class, the particular enumerated
value and a pointer to an event handler are passed. From the value, the class can get the text to associate with the radio button, and also now we will have the
value associated with it so that it can provide this value in the delegate when the radio button is selected. The address of the delegate to handle the event
is the second argument in the constructor. An instance of this class is created for each enumeration value, and is added to the enumeration that
is the ItemsSource for the control.
The last part of this code is responsible for ensuring that the radio button in the list that is selected corresponds to the value
in the DependencyProperty EnumerationValue. It also is the code that responds to user input since when a radio button is clicked, and the class for
the ViewModel for the radio button responds with an Action containing the enumerated value that was clicked, causing the following code to be executed:
private void EnumerationChanged(object newValue)
{
EnumerationValue = newValue;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("EnumerationValue"));
}
This code updates the EnumerationValue with the value provided by the radio button ViewModel, which causes the UpdateEnumerationValue method to be executed,
updating all the radio buttons to be updated to correspond to the new EnumerationValue.
The class that is the radio button ViewModel is as follows:
public class EnumDrivenRadioButtonBinding : INotifyPropertyChanged
{
public string Text { get; private set; }
public bool IsChecked
{
get { return _isChecked; }
set
{
_isChecked = true;
_isCheckedChangedCallback(_enumeration);
}
}
private readonly object _enumeration;
private bool _isChecked;
private readonly Action<object> _isCheckedChangedCallback;
internal EnumDrivenRadioButtonBinding(object value,
Action<object> isCheckedChangedCallback)
{
FieldInfo info = value.GetType().GetField(value.ToString());
var valueDescription = (DescriptionAttribute[])info.GetCustomAttributes
(typeof(DescriptionAttribute), false);
Text = valueDescription.Length == 1 ?
valueDescription[0].Description : value.ToString();
_enumeration = value;
_isCheckedChangedCallback += isCheckedChangedCallback;
}
internal void UpdateIsChecked(object value)
{
if (_enumeration.Equals(value) != IsChecked)
{
_isChecked = _enumeration.Equals(value);
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public override string ToString()
{
if (_isChecked)
return "true - " + Text;
return "false - " + Text;
}
}
A lot of the intelligence is in this class. The constructor determines if the enumeration value has a DescriptionAttribute, and if it does,
uses that for the title, otherwise the enumeration value name is used. An example of an enumeration that includes description attributes is as follows:
public enum SampleEnum
{
[DescriptionAttribute("I like the color blue")]
Blue,
[DescriptionAttribute("I like the color green")]
Green,
[DescriptionAttribute("I like the color yellow")]
Yellow,
Orange,
[DescriptionAttribute("I like the color red")]
Red
}
All the enumeration types above have a DescriptionAttribute except the Orange enumeration type. In this case, the instance of the class for
every enumeration type will have a text value equal to the associated DescriptionAttribute except the instance for the Orange enumeration value,
which will be the value “Orange”. The constructor also saves the enumeration value which is returned as an argument in the saved handler delegate that
is executed whenever the IsChecked value becomes true.
The radio button ViewModel also contains a method UpdateIsChecked that checks if the passed argument is equal to the instance’s enumeration value, and ensures
that the IsChecked value is only true if this is equal.
You will note that I use the Equals method when comparing values. I have found that “==” often does not work.
This may be because I am dealing with what the compiler sees and an object, and not what the object contains. Only the Equals method is reliable.
An interesting note is that the IsChecked property never uses the value when setting the new value since the only time that the IsChecked
will be triggered by the UI is when going from false to true. This is also why the callback delegate only needs an enumeration value argument and not a checked argument,
it will always be true.
Using the Control
The nice thing about this control is that only a single binding of EnumerationValue to the
enumeration in the ViewModel is required. At its simplest, the XAML to use this control is:
<local:EnumRadioButtonListBox EnumerationValue="{Binding SampleEnum,Mode=TwoWay}"/>
Note: The TwoWay binding mode is required for this control to work right.
The example uses slightly different XAML to show how standard XAML changes to the ListBox and RadioButton controls
can be used to customize this control in many ways:
<local:EnumRadioButtonListBox
EnumerationValue="{Binding SampleEnum,Mode=TwoWay}">
<ListBox.Resources>
<Style TargetType="RadioButton">
<Setter Property="Margin" Value="2"/>
</Style>
</ListBox.Resources>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Width="200" IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</local:EnumRadioButtonListBox>
Here I put a style for radio buttons in the Resources for the control, and then set the margin for all radio buttons to “2.” The ListBox is customized the way
any ListBox would be, in this case changed the ItemsPanel to a WrapPanel.
Conclusion
This should be a useful control in many applications since it supports using enumerations to define radio button groups, and the only
binding required is a single binding to the value that the multiple radio buttons will control. There is no need to have a property for each radio button
in the View Model, nor is there a requirement for an ItemsSource binding.
In addition, the DescriptionAttribute associated with each enumeration value is used for the text associated with each radio button if one is defined.
It is nice being able to define the text as something besides the name of the enumeration value since no special characters, including spaces, can be used in the name. Also, enumeration values
may want to follow some naming conventions.