Simple WPF RadioButton Binding?

Viewed 140277

What is the simplest way to bind a group of 3 radiobuttons to a property of type int for values 1, 2, or 3?

10 Answers

Aviad P.s answer works very well. However I had to change the equality check to compare strings in OnRadioBindingChanged otherwise the enum was compared to the string value and no radio button was checked initially.

    private static void OnRadioBindingChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        BindableRadioButton rb = (BindableRadioButton) d;
        if (rb.RadioValue.Equals(e.NewValue?.ToString()))
        {
            rb.SetCurrentValue(IsCheckedProperty, true);
        }
    }

I created an attached property based on Aviad's Answer which doesn't require creating a new class

public static class RadioButtonHelper
{
    [AttachedPropertyBrowsableForType(typeof(RadioButton))]
    public static object GetRadioValue(DependencyObject obj) => obj.GetValue(RadioValueProperty);
    public static void SetRadioValue(DependencyObject obj, object value) => obj.SetValue(RadioValueProperty, value);
    public static readonly DependencyProperty RadioValueProperty =
        DependencyProperty.RegisterAttached("RadioValue", typeof(object), typeof(RadioButtonHelper), new PropertyMetadata(new PropertyChangedCallback(OnRadioValueChanged)));

    private static void OnRadioValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is RadioButton rb)
        {
            rb.Checked -= OnChecked;
            rb.Checked += OnChecked;
        }
    }

    public static void OnChecked(object sender, RoutedEventArgs e)
    {
        if (sender is RadioButton rb)
        {
            rb.SetCurrentValue(RadioBindingProperty, rb.GetValue(RadioValueProperty));
        }
    }

    [AttachedPropertyBrowsableForType(typeof(RadioButton))]
    public static object GetRadioBinding(DependencyObject obj) => obj.GetValue(RadioBindingProperty);
    public static void SetRadioBinding(DependencyObject obj, object value) => obj.SetValue(RadioBindingProperty, value);

    public static readonly DependencyProperty RadioBindingProperty =
        DependencyProperty.RegisterAttached("RadioBinding", typeof(object), typeof(RadioButtonHelper), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(OnRadioBindingChanged)));

    private static void OnRadioBindingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is RadioButton rb && rb.GetValue(RadioValueProperty).Equals(e.NewValue))
        {
            rb.SetCurrentValue(RadioButton.IsCheckedProperty, true);
        }
    }
}

usage :

<RadioButton GroupName="grp1" Content="Value 1"
    helpers:RadioButtonHelper.RadioValue="val1" helpers:RadioButtonHelper.RadioBinding="{Binding SelectedValue}"/>
<RadioButton GroupName="grp1" Content="Value 2"
    helpers:RadioButtonHelper.RadioValue="val2" helpers:RadioButtonHelper.RadioBinding="{Binding SelectedValue}"/>
<RadioButton GroupName="grp1" Content="Value 3"
    helpers:RadioButtonHelper.RadioValue="val3" helpers:RadioButtonHelper.RadioBinding="{Binding SelectedValue}"/>
<RadioButton GroupName="grp1" Content="Value 4"
    helpers:RadioButtonHelper.RadioValue="val4" helpers:RadioButtonHelper.RadioBinding="{Binding SelectedValue}"/>

Answer 2.0

While I provided the answer above that is quite powerful being a re-templated ListBox, it's still far from ideal for simple radio buttons. As such, I've come up with a much-simpler solution that instead uses a MarkupExtension subclass that implements IValueConverter and which is armed with the power of Binding.DoNothing, the magic sauce that makes two-way bindings work.

Binding to Scalar Values

Let's take a look at the converter itself for binding to scalars...

public class RadioButtonConverter : MarkupExtension, IValueConverter {

    public RadioButtonConverter(object optionValue)
        => OptionValue = optionValue;

    public object OptionValue { get; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        => value.Equals(OptionValue);

    public object ConvertBack(object isChecked, Type targetType, object parameter, CultureInfo culture)
        => (bool)isChecked
            ? OptionValue
            : Binding.DoNothing; // Only send value back if this is the checked option, otherwise do nothing

    public override object ProvideValue(IServiceProvider serviceProvider)
        => this;
}

The magic sauce is in the use of Binding.DoNothing in the ConvertBack function. Since with RadioButton controls, there can only be one active option per 'group' (i.e. only one with IsChecked set to true), we ensure only that RadioButton's binding updates the source. Those on the other RadioButton instances simply do nothing.

Here's how you use it to bind to an int value as the OP asked (below, 'cv' is the imported namespace where the converter code resides, and the value you pass to the converter is the value that particular RadioButton represents)...

<RadioButton Content="One"   IsChecked="{Binding SomeIntProp, Converter={cv:RadioButtonConverter 1}}" />
<RadioButton Content="Two"   IsChecked="{Binding SomeIntProp, Converter={cv:RadioButtonConverter 2}}" />
<RadioButton Content="Three" IsChecked="{Binding SomeIntProp, Converter={cv:RadioButtonConverter 3}}" />

Simplifying the Binding

While the above works, that's a lot of repeated code and for 90% of the time, you aren't doing anything special with the binding or converter. As such, let's try to simplify things with a RadioButtonBinding that sets up the converter for you. Here's the code...

public class RadioButtonBinding : Binding {

    public RadioButtonBinding(string path, object optionValue)
    : base(path)
        => Converter = new RadioButtonConverter(optionValue);
}

With this new binding, the call site is greatly simplified (here, 'b' is the imported namespace where the binding code resides)...

<RadioButton Content="One"   IsChecked="{b:RadioButtonBinding SomeIntProp, 1}" />
<RadioButton Content="Two"   IsChecked="{b:RadioButtonBinding SomeIntProp, 2}" />
<RadioButton Content="Three" IsChecked="{b:RadioButtonBinding SomeIntProp, 3}" />

Note: Make sure you don't also set the Converter argument or you will defeat the entire point of using this!

Binding to Enum Values

The above example dealt with basic scalars (e.g. 1, 2, 3.) However, what if the value we want to is an enumeration such as the following?

public enum TestEnum {
    yes,
    no,
    maybe,
    noIdea
}

The syntax is the same, but at the call-site, we need to be more specific about the value we're binding to making it much more verbose. (For instance, if you try and pass 'yes' by itself, it will be treated as a string, not an enum, so it will fail the equality check.)

Here's the converter version's call-site (here, 'v' is the imported namespace where the enum values reside)...

<RadioButton Content="Yes"     IsChecked="{Binding SomeEnumProp, Converter={cv:RadioButtonConverter {x:Static v:TestEnum.yes}}}" />
<RadioButton Content="No"      IsChecked="{Binding SomeEnumProp, Converter={cv:RadioButtonConverter {x:Static v:TestEnum.no}}}" />
<RadioButton Content="Maybe"   IsChecked="{Binding SomeEnumProp, Converter={cv:RadioButtonConverter {x:Static v:TestEnum.maybe}}}" />
<RadioButton Content="No Idea" IsChecked="{Binding SomeEnumProp, Converter={cv:RadioButtonConverter {x:Static v:TestEnum.noIdea}}}" />

And while simpler, here's the binding version's call-site, better, but still verbose...

<RadioButton Content="Yes"     IsChecked="{b:RadioButtonBinding SomeEnumProp, {x:Static v:TestEnum.yes}}" />
<RadioButton Content="No"      IsChecked="{b:RadioButtonBinding SomeEnumProp, {x:Static v:TestEnum.no}}" />
<RadioButton Content="Maybe"   IsChecked="{b:RadioButtonBinding SomeEnumProp, {x:Static v:TestEnum.maybe}}" />
<RadioButton Content="No Idea" IsChecked="{b:RadioButtonBinding SomeEnumProp, {x:Static v:TestEnum.noIdea}}" />

Enum-Type-Specific variants

If you know you will be binding to a particular enum type on many occasions, you can simplify the above by subclassing the earlier converter and binding to be enum-specific variants.

Below is doing exactly that with TestEnum defined above, like so...

// TestEnum-specific Converter
public class TestEnumConverter : RadioButtonConverter {

    public TestEnumConverter(TestEnum optionValue)
    : base(optionValue) {}
}

// TestEnum-specific Binding
public class TestEnumBinding : RadioButtonBinding {

    public TestEnumBinding(string path, TestEnum value)
    : base(path, value) { }
}

And here are the call sites...

<!- Converter Variants -->
<RadioButton Content="Yes"     IsChecked="{Binding SomeTestEnumProp, Converter={cv:TestEnumConverter yes}}" />
<RadioButton Content="No"      IsChecked="{Binding SomeTestEnumProp, Converter={cv:TestEnumConverter no}}" />
<RadioButton Content="Maybe"   IsChecked="{Binding SomeTestEnumProp, Converter={cv:TestEnumConverter maybe}}" />
<RadioButton Content="No Idea" IsChecked="{Binding SomeTestEnumProp, Converter={cv:TestEnumConverter noIdea}}" />

<!- Binding Variants -->
<RadioButton Content="Yes"     IsChecked="{b:TestEnumBinding SomeTestEnumProp, yes}" />
<RadioButton Content="No"      IsChecked="{b:TestEnumBinding SomeTestEnumProp, no}" />
<RadioButton Content="Maybe"   IsChecked="{b:TestEnumBinding SomeTestEnumProp, maybe}" />
<RadioButton Content="No Idea" IsChecked="{b:TestEnumBinding SomeTestEnumProp, noIdea}" />

As you can see, the XAML parser automatically handles the string-to-enum conversion for you making your code much simpler to read. Can't get much simpler than that! :)

Sidenote: One nice thing about the versions where you explicitly specify the enum value in its more-verbose declaration is you get auto-complete for the enum's cases. You don't get that with the enum-type-specific versions that convert the string for you. However, the latter will fail to compile if you use an invalid string value so the tradeoff is brevity vs auto-complete convenience.

Related