Attached Behaviors Part 6: EnumGroup
- Attached Behaviors Part 1: BooleanVisibility
- Attached Behaviors Part 2: Framework
- Attached Behaviors Part 3: NullVisibility
- Attached Behaviors Part 4: EnumVisibility
- Attached Behaviors Part 5: EnumIsEnabled
- Attached Behaviors Part 6: EnumGroup
- Attached Behaviors Part 7: EnumSelector
- Our framework for matching enumeration values has helped us easily manage the Visibility and IsEnabled properties of individual controls. Now, we will write an attached behavior to select a value from a set of radio buttons.
Scenario
Radio buttons naturally represent an enumeration: as a related set of controls, known as a group, each is a value in a mutually exclusive selection. For example, a checkout form might offer multiple payment methods, such a credit card or PayPal, and allow the user to choose with radio buttons. Here, we can use enumeration matching to synchronize a property with the selected radio button:<RadioButton
Content="Credit card"
local:EnumGroup.Value="{Binding PaymentType, Mode=TwoWay}"
local:EnumGroup.TargetValue="CreditCard"
/>
<RadioButton
Content="PayPal"
local:EnumGroup.Value="{Binding PaymentType, Mode=TwoWay}"
local:EnumGroup.TargetValue="PayPal"
/>
Each radio button is associated with an enumeration value. When the user selects an option, it writes that value to the PaymentType property through a two-way binding.
EnumGroup
If you have read the rest of the series, there isn’t much new here. We define the behavior as a static class:
public static class EnumGroup
Next, we register the attached properties which govern the behavior. First is the Value property, which can contain any enumeration value and thus needs to be typed as object. We also create static accessors to facilitate XAML usage:
public static readonly DependencyProperty ValueProperty =
DependencyProperty.RegisterAttached(
"Value",
typeof(object),
typeof(EnumGroup),
new PropertyMetadata(OnArgumentsChanged));
public static object GetValue(RadioButton radioButton)
{
return radioButton.GetValue(ValueProperty);
}
public static void SetValue(RadioButton radioButton, object value)
{
radioButton.SetValue(ValueProperty, value);
}
Then, we register the TargetValue property, which is a simple string we will parse later:
public static readonly DependencyProperty TargetValueProperty =
DependencyProperty.RegisterAttached(
"TargetValue",
typeof(string),
typeof(EnumGroup),
new PropertyMetadata(OnArgumentsChanged));
public static string GetTargetValue(RadioButton radioButton)
{
return (string) radioButton.GetValue(TargetValueProperty);
}
public static void SetTargetValue(RadioButton radioButton, string value)
{
radioButton.SetValue(TargetValueProperty, value);
}
Behavior
The integration of the behavior into the static class is exactly the same as the previous behaviors. First, we declare the behavior and tell it how to create instances for individual host objects:
private static readonly AttachedBehavior Behavior =
AttachedBehavior.Register(host => new EnumGroupBehavior(host));
Then, we update it when either of the Value or TargetValue dependency properties changes:
private static void OnArgumentsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Behavior.Update(d);
}
The behavior class is more interesting than those for other behaviors because it overrides the Attach and Detach methods to manage a handler for the Checked event:
private sealed class EnumGroupBehavior : Behavior<RadioButton>
{
private readonly EnumCheck _enumCheck = new EnumCheck();
internal EnumGroupBehavior(DependencyObject host) : base(host)
{}
protected override void Attach(RadioButton host)
{
host.Checked += OnChecked;
}
protected override void Detach(RadioButton host)
{
host.Checked -= OnChecked;
}
protected override void Update(RadioButton host)
{
_enumCheck.Update(GetValue(host), GetTargetValue(host));
host.IsChecked = _enumCheck.IsMatch;
}
private void OnChecked(object sender, RoutedEventArgs e)
{
TryUpdate(host => SetValue(host, _enumCheck.ParsedTargetValue));
}
}
The Update method, called whenever Value or TargetValue changes (via OnArgumentsChanged), is straightforward. We leverage the EnumCheck class to manage the parsing, caching, and matching. Then, we set the IsChecked property based on whether Value matches TargetValue.
Another interesting aspect of this behavior is the handler for the radio button’s Checked event. This is the first time we have a chance to use the TryUpdate method, which executes the specified lambda expression only if the host has not been garbage-collected.
The Checked event occurs when the radio button becomes the selected value in its group. When that happens, we set the Value property to the associated target value. Voila!
Sample Project
This WPF project shows EnumGroup in action:
Attached Behaviors Part 6 EnumGroup.zip
It allows you to set the value of the EnumGroup.Value attached property and see the results when applied to two radio buttons. You can also check the radio buttons and see EnumGroup.Value change, showing the effect of the two-way binding.
As a side note, the EnumCheck class evolved since the last post. It now performs the duties of EnumTargetValue, which no longer exists. It also has a new member, ParsedTargetValue, which we used in the OnChecked method above. It returns the first value from ParsedTargetValues, which may contain multiple values if TargetValue is set to a comma-separated string.
Summary
We used enumeration matching to put another fundamental building block in place, the radio button group. Their mutually exclusive nature is a seamless fit for selecting one value from many. By associating each radio button with an enumeration value, we easily and intuitively described a choice.
Next time, we will use selectors, such as a combo boxes and list boxes, to select enumeration values.