Introduction
I have a background in Human Factors in computer systems, and I am a big fan of RadioButton
controls. This is because users tend to prefer RadioButton
controls 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 ListBox
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 RadioButton
controls to be specified in the View
. This is normally done because there is no control in the ViewModel
. Therefore RadioButton
controls 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 RadioButton
. Of course, the text associated with a RadioButton
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 RadioButton
controls 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 RadioButton
controls, 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 an IValueConverter
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 DataTemplate
s (for some reason, Microsoft in their wisdom, did not see fit to allow DataTemplate
s 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 RadioButton
controls if the enumeration type is changed, and to update the RadioButton
controls 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 RadioButton
controls to operate correctly, a new class is required (the RadioButton
ViewModel
). It has a property for the state of the radio button, and a property for the text to be associated with each RadioButton
. 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 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 RadioButton
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 RadioButton
is clicked, and the class for the ViewModel
for the RadioButton
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 RadioButton
ViewModel
, which causes the UpdateEnumerationValue
method to be executed, updating all the RadioButton
controls to be updated to correspond to the new EnumerationValue
.
The class that is the RadioButton
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 RadioButton
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 RadioButton
in the Resources
for the control, and then set the margin for the RadioButton
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 RadioButton
groups, and the only binding required is a single binding to the value that the multiple RadioButton
controls will control. There is no need to have a property for each radio button in the ViewModel
, 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 RadioButton
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.