简单的WPF RadioButton绑定?

78
什么是将一组3个单选按钮绑定到类型为int的属性以进行值1、2或3的最简单方法?

请看这个解决方案在WPF中绑定RadioButton的IsChecked属性,它非常有效。原来的问题已经在WPF 4.0中修复了! - Alexander Zwitbaum
3
更好、更普适的解决方案可以在这个回答中找到:https://dev59.com/zXRC5IYBdhLWcg3wJNif#406798 - Steve Streeting
10个回答

100

我想到了一个简单的解决方案。

我有一个 model.cs 类包含:

private int _isSuccess;
public int IsSuccess { get { return _isSuccess; } set { _isSuccess = value; } }

我有一个Window1.xaml.cs文件,它的DataContext设置为model.cs。xaml包含单选按钮:

<RadioButton IsChecked="{Binding Path=IsSuccess, Converter={StaticResource radioBoolToIntConverter}, ConverterParameter=1}" Content="one" />
<RadioButton IsChecked="{Binding Path=IsSuccess, Converter={StaticResource radioBoolToIntConverter}, ConverterParameter=2}" Content="two" />
<RadioButton IsChecked="{Binding Path=IsSuccess, Converter={StaticResource radioBoolToIntConverter}, ConverterParameter=3}" Content="three" />
这是我的转换器:
public class RadioBoolToIntConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        int integer = (int)value;
        if (integer==int.Parse(parameter.ToString()))
            return true;
        else
            return false;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return parameter;
    }
}

当然,在 Window1 的资源中:

<Window.Resources>
    <local:RadioBoolToIntConverter x:Key="radioBoolToIntConverter" />
</Window.Resources>

5
@MarqueIV 这个问题很容易解决,可以看一下这个问题已被接受回答的第一条评论:https://dev59.com/zXRC5IYBdhLWcg3wJNif - user1151923
2
在这种情况下,@user1151923...不是应该返回'Binding.DoNothing'而不是'DependencyProperty.UnsetValue'吗? - Mark A. Donohoe
7
比接受答案干净得多 - 正是我在寻找的。 - Rob Hardy
2
如何设置默认值。也就是在窗口首次显示时选择第一个单选按钮? - MoonKnight
8
但是,拜托了,请不要使用“if (true) return true; else return false;”这样的结构... :-) - Gábor
显示剩余6条评论

53

我非常惊讶没有人提出这样一种解决方案,将其绑定到布尔数组上。它可能不是最简洁的,但非常容易使用:

private bool[] _modeArray = new bool[] { true, false, false};
public bool[] ModeArray
{
    get { return _modeArray ; }
}
public int SelectedMode
{
    get { return Array.IndexOf(_modeArray, true); }
}

XAML 中的写法:

<RadioButton GroupName="Mode" IsChecked="{Binding Path=ModeArray[0], Mode=TwoWay}"/>
<RadioButton GroupName="Mode" IsChecked="{Binding Path=ModeArray[1], Mode=TwoWay}"/>
<RadioButton GroupName="Mode" IsChecked="{Binding Path=ModeArray[2], Mode=TwoWay}"/>

注意:如果您不想默认选中一个,则不需要双向绑定。双向绑定是此解决方案的最大缺点。

优点:

  • 无需编写后端代码
  • 无需额外的类(IValue Converter)
  • 无需额外的枚举
  • 不需要奇怪的绑定方式
  • 简单直接,易于理解
  • 不违反 MVVM (呵呵,至少我希望如此)

我也感到惊讶。这个解决方案适用于那些刚接触WPF、只知道如何进行绑定并希望快速绑定单选按钮的人。它非常直接和简单。+1 - QuantumHive
这很好,非常快速地投入测试,然后再编写自己的“转换器”。+1 - Forest Kunecke
3
一个小建议:自C# 6版本以来,您将不再需要备份字段。只需使用自动属性:public bool[] ModeArray { get; } = new bool[] { true, false, false }; 您可以直接使用这个自动属性而无需编写备份字段。 - oopbase
2
这怎么能是数据驱动的呢?如果我从数据库加载值,如何通过编程设置选项? - Ray Brennan
@Julien 如果我正确理解你们的意思,你们想在更改时运行一段代码。我认为这并不是一件简单的事情,我建议使用转换器 - 这首先是快速和简单的解决方案。当然,你可以为每个布尔值创建属性或使用带有INotify的包装器,但在我看来,这太复杂了,对于更具功能性的解决方案,我会选择转换器。 - wondra
显示剩余5条评论

41

使用更简单的方法更新答案

请查看我在同一页上发布的新答案,它提供了一种完全不同且更简单的解决此问题的方法。这种新方法使用自定义的IValueConverterBinding子类,让您可以回到使用真正的RadioButton而不是经过大量样式设计的ListBox,如下所示。

除非您个人使用ListBox子类时有其他好处,否则另一种方法是我现在推荐的方法。

原始答案

实际上,像那样使用转换器会破坏双向绑定,而且正如我上面所说,您也无法将其用于枚举。更好的方法是使用一个简单的样式来对ListBox进行设置,就像这样:

注意:与DrWPF.com在他们的示例中所述相反,不要将ContentPresenter放在RadioButton内部,否则如果您添加具有内容(例如按钮或其他内容)的项目,则无法设置焦点或与其交互。这种技术解决了这个问题。此外,您还需要处理文本的灰度以及标签上的边距删除,否则它将无法正确呈现。此样式可以为您同时处理这两个问题。
<Style x:Key="RadioButtonListItem" TargetType="{x:Type ListBoxItem}" >

    <Setter Property="Template">
        <Setter.Value>

            <ControlTemplate TargetType="ListBoxItem">

                <DockPanel LastChildFill="True" Background="{TemplateBinding Background}" HorizontalAlignment="Stretch" VerticalAlignment="Center" >

                    <RadioButton IsChecked="{TemplateBinding IsSelected}" Focusable="False" IsHitTestVisible="False" VerticalAlignment="Center" Margin="0,0,4,0" />

                    <ContentPresenter
                        Content             = "{TemplateBinding ContentControl.Content}"
                        ContentTemplate     = "{TemplateBinding ContentControl.ContentTemplate}"
                        ContentStringFormat = "{TemplateBinding ContentControl.ContentStringFormat}"
                        HorizontalAlignment = "{TemplateBinding Control.HorizontalContentAlignment}"
                        VerticalAlignment   = "{TemplateBinding Control.VerticalContentAlignment}"
                        SnapsToDevicePixels = "{TemplateBinding UIElement.SnapsToDevicePixels}" />

                </DockPanel>

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

<Style x:Key="RadioButtonList" TargetType="ListBox">

    <Style.Resources>
        <Style TargetType="Label">
            <Setter Property="Padding" Value="0" />
        </Style>
    </Style.Resources>

    <Setter Property="BorderThickness" Value="0" />
    <Setter Property="Background"      Value="Transparent" />

    <Setter Property="ItemContainerStyle" Value="{StaticResource RadioButtonListItem}" />

    <Setter Property="Control.Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBox}">
                <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>

    <Style.Triggers>
        <Trigger Property="IsEnabled" Value="False">
            <Setter Property="TextBlock.Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
        </Trigger>
    </Style.Triggers>

</Style>

<Style x:Key="HorizontalRadioButtonList" BasedOn="{StaticResource RadioButtonList}" TargetType="ListBox">
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel Background="Transparent" Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
</Style>

现在您拥有了单选按钮的外观和感觉,但是您可以进行双向绑定,并且可以使用枚举。以下是方法...

<ListBox Style="{StaticResource RadioButtonList}"
    SelectedValue="{Binding SomeVal}"
    SelectedValuePath="Tag">

    <ListBoxItem Tag="{x:Static l:MyEnum.SomeOption}"     >Some option</ListBoxItem>
    <ListBoxItem Tag="{x:Static l:MyEnum.SomeOtherOption}">Some other option</ListBoxItem>
    <ListBoxItem Tag="{x:Static l:MyEnum.YetAnother}"     >Yet another option</ListBoxItem>

</ListBox>

此外,由于我们明确地将针对 ListBoxItem 的样式与其它内容分开,而不是内联放置,正如其它示例所示,你现在可以新建一个样式以自定义每个项的间距等基础内容。(如果你只是尝试针对 ListBoxItem,覆盖通用控件目标的键控样式就无法起作用。)
这里有一个示例,将每个项上下加6个边距。(请注意,你必须通过 ItemContainerStyle 属性显式应用样式,而不是简单地针对 ListBox 的资源节选中的 ListBoxItem,原因如上所述。)
<Window.Resources>
    <Style x:Key="SpacedRadioButtonListItem" TargetType="ListBoxItem" BasedOn="{StaticResource RadioButtonListItem}">
        <Setter Property="Margin" Value="0,6" />
    </Style>
</Window.Resources>

<ListBox Style="{StaticResource RadioButtonList}"
    ItemContainerStyle="{StaticResource SpacedRadioButtonListItem}"
    SelectedValue="{Binding SomeVal}"
    SelectedValuePath="Tag">

    <ListBoxItem Tag="{x:Static l:MyEnum.SomeOption}"     >Some option</ListBoxItem>
    <ListBoxItem Tag="{x:Static l:MyEnum.SomeOtherOption}">Some other option</ListBoxItem>
    <ListBoxItem Tag="{x:Static l:MyEnum.YetAnother}"     >Ter another option</ListBoxItem>

</ListBox>

75
+1,但如果这是“将一组3个单选按钮绑定到整数类型的属性以表示值1、2或3的最简单方法”,那么这并不是WPF的优点。我真不明白为什么他们把最简单的事情搞得让人头疼难懂,变得很复杂。 - Konrad Morawski
2
实际上,这做的不仅如此,因为它会对列表框进行样式设置,该控件是单个控件,这就是为什么绑定工作如此简单(一旦你通过了疯狂的设置)。但是,是的,我同意RadioButton类肯定存在一些绑定缺陷。当然,你可以用代码后台做得更简单,但用这种方式,你只需要做一次样式和类似的东西,然后稍后使用就容易得多。 - Mark A. Donohoe
3
哦,简单?尽管我尊重您的回答和努力,但将其称为“简单风格”有些牵强,不是吗?难道不是WPF相对于Winforms的核心思想和感知优势在于更好的数据绑定吗?一个简单的单选按钮(以及单选按钮分组)在WPF已经存在市场多年后仍然会破坏绑定,这超出了我的理解范围。微软应该感到羞耻。 - Matt
1
这实际上是一个不公平的说法。例如,使用ShowMeTheTemplate查看常规按钮的控件模板。它远非简单,但是它的使用,添加Button标记非常简单。你正在窥探我在这里做什么,但如果你将其添加到资源部分,然后忘记它,它也会变得简单。 - Mark A. Donohoe
1
此外,考虑一下你正在评论什么......不同的无线电控件对单个值进行操作。多个控件意味着多个绑定。这种风格使单个控件 - 因此是单个值 - 看起来像多个控件。这就是为什么这很棒。此外,这也可以用于绑定到IEnumerable并动态创建项目。单选按钮根本不能做到这一点。 - Mark A. Donohoe

22

我知道这已经迟了很久,但是我有一个替代方案,更轻更简单。从 System.Windows.Controls.RadioButton 派生一个类,并声明两个依赖属性 RadioValueRadioBinding。然后在类代码中,重写 OnChecked 方法并将 RadioBinding 属性值设置为 RadioValue 属性值。在另一个方向上,使用回调函数捕获对 RadioBinding 属性的更改,如果新值等于 RadioValue 属性值,则将其 IsChecked 属性设置为 true

以下是代码:

public class MyRadioButton : RadioButton
{
    public object RadioValue
    {
        get { return (object)GetValue(RadioValueProperty); }
        set { SetValue(RadioValueProperty, value); }
    }

    // Using a DependencyProperty as the backing store for RadioValue.
       This enables animation, styling, binding, etc...
    public static readonly DependencyProperty RadioValueProperty =
        DependencyProperty.Register(
            "RadioValue", 
            typeof(object), 
            typeof(MyRadioButton), 
            new UIPropertyMetadata(null));

    public object RadioBinding
    {
        get { return (object)GetValue(RadioBindingProperty); }
        set { SetValue(RadioBindingProperty, value); }
    }

    // Using a DependencyProperty as the backing store for RadioBinding.
       This enables animation, styling, binding, etc...
    public static readonly DependencyProperty RadioBindingProperty =
        DependencyProperty.Register(
            "RadioBinding", 
            typeof(object), 
            typeof(MyRadioButton), 
            new FrameworkPropertyMetadata(
                null, 
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 
                OnRadioBindingChanged));

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

    protected override void OnChecked(RoutedEventArgs e)
    {
        base.OnChecked(e);
        SetCurrentValue(RadioBindingProperty, RadioValue);
    }
}

XAML使用:

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

希望这篇文章经过这么长时间后,有人能够发现它并从中受益 :)


1
非常好的解决方案,感谢您的发布。更加简洁易实现。 - DonBoitnott
当然,我已经做了。第40行应该写成: rb.SetCurrentValue(RadioButton.IsCheckedProperty, rb.RadioValue.Equals(e.NewValue)); 这样,在绑定具有除所指示值以外的其他值时,不会选中任何单选框。(对于格式问题很抱歉,我似乎无法插入换行符)。 - Peter Wilson

5
我想到了一个解决方案,使用从转换器返回的Binding.DoNothing,这样不会破坏双向绑定。
public class EnumToCheckedConverter : IValueConverter
{
    public Type Type { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null && value.GetType() == Type)
        {
            try
            {
                var parameterFlag = Enum.Parse(Type, parameter as string);

                if (Equals(parameterFlag, value))
                {
                    return true;
                }
            }
            catch (ArgumentNullException)
            {
                return false;
            }
            catch (ArgumentException)
            {
                throw new NotSupportedException();
            }

            return false;
        }
        else if (value == null)
        {
            return false;
        }

        throw new NotSupportedException();
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null && value is bool check)
        {
            if (check)
            {
                try
                {
                    return Enum.Parse(Type, parameter as string);
                }
                catch(ArgumentNullException)
                {
                    return Binding.DoNothing;
                }
                catch(ArgumentException)
                {
                    return Binding.DoNothing;
                }
            }

            return Binding.DoNothing;
        }

        throw new NotSupportedException();
    }
}

使用方法:

<converters:EnumToCheckedConverter x:Key="SourceConverter" Type="{x:Type monitor:VariableValueSource}" />

单选按钮绑定:
<RadioButton GroupName="ValueSource" 
             IsChecked="{Binding Source, Converter={StaticResource SourceConverter}, ConverterParameter=Function}">Function</RadioButton>

1
这很不错(虽然我希望它支持枚举值的 IntelliSense)。顺便说一下,我已经将 catch (ArgumentException) 部分更改为以下内容,以便在 XAML 中更清楚地显示错误:throw new NotSupportedException($"Enum {this.Type} doesn't contain value '{parameter}'."); - Sören Kuklau

2
有时候可以在模型中这样解决: 假设你有三个布尔属性 OptionA、OptionB、OptionC。
XAML:
<RadioButton IsChecked="{Binding OptionA}"/>
<RadioButton IsChecked="{Binding OptionB}"/>
<RadioButton IsChecked="{Binding OptionC}"/>

代码:

private bool _optionA;
public bool OptionA
{
    get { return _optionA; }
    set
    {
        _optionA = value;
        if( _optionA )
        {
             this.OptionB= false;
             this.OptionC = false;
        }
    }
}

private bool _optionB;
public bool OptionB
{
    get { return _optionB; }
    set
    {
        _optionB = value;
        if( _optionB )
        {
            this.OptionA= false;
            this.OptionC = false;
        }
    }
}

private bool _optionC;
public bool OptionC
{
    get { return _optionC; }
    set
    {
        _optionC = value;
        if( _optionC )
        {
            this.OptionA= false;
            this.OptionB = false;
        }
    }
}

你可以理解这个概念。 虽然不够简洁,但很容易理解。

2

改进和现在推荐的答案(例如2.0方法)

虽然我在这个页面上提供了被接受的答案,正如评论者们正确指出的那样,尽管它功能强大且非常灵活,但当你只想使用简单的RadioButton控件时,它的复杂性远非理想。

因此,我提出了一种新的方法,如下所示,它使用了自定义的IValueConverterBinding.DoNothing在其ConvertBack方法中的力量,这是使双向绑定到RadioButton控件按预期工作的魔法调味料。

RadioButtonValueConverter

让我们来看看转换器本身:

public class RadioButtonValueConverter : MarkupExtension, IValueConverter {

    public RadioButtonValueConverter(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        // Is this the checked RadioButton? If so...
            ? OptionValue         // Send 'OptionValue' back to update the associated binding. Otherwise...
            : Binding.DoNothing;  // Return Binding.DoNothing, telling the binding 'ignore this change'

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

魔法之处在于在ConvertBack函数中使用Binding.DoNothing。 由于对于RadioButton控件,每个“组”只能有一个活动选项(即,只有一个IsChecked设置为true),我们确保仅具有该值的特定RadioButton是唯一一个更新源的绑定。其他RadioButton实例上的绑定将什么都不做。

下面是如何将其用于绑定到int值的方法(在下面,“cv”是包含转换器代码的导入命名空间,并且传递给转换器的值是该特定RadioButton表示的值)...

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

通过自定义 Binding 子类进一步简化

虽然上面的代码能够正常工作,但是有很多重复的代码,而且在90%的情况下,你并没有对绑定或转换器进行特殊处理。因此,让我们尝试使用一个RadioButtonBinding 来为你设置转换器,以便进一步简化操作。以下是代码示例...

public class RadioButtonBinding : Binding {

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

通过这个新的绑定,调用点大大简化(这里,'b'是导入名称空间,绑定代码驻留在其中)...

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

注意:确保不要设置Converter参数,否则您将打败使用它的整个目的!

绑定到枚举值

上面的示例处理了基本标量(例如1、2、3)。但是,如果我们想要的值是以下枚举类型呢?

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

语法是一样的,但在调用时,我们需要更加具体地绑定值,从而使其变得更加冗长。 (例如,如果您尝试仅传递“yes”,它将被视为字符串而不是枚举,因此将无法通过相等性检查。)

这是转换器版本的调用站点(这里,“v”是导入枚举值所在的命名空间)...

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

虽然更简单,但这是绑定版本的调用站点,更好,但仍然啰嗦......

<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}}" />

枚举类型特定变体

如果您知道您将在许多场合绑定到特定的枚举类型,您可以通过对早期的RadioButtonValueConverterRadioButtonBinding进行子类化来简化上述过程,以创建特定于枚举的变体。

下面是使用上面定义的TestEnum进行实现的示例...

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

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

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

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

这里是调用站点...

<!- 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}" />

正如你所看到的,XAML解析器会自动处理字符串到枚举类型的转换,使得你的代码更容易阅读。这就是如此简单!:)

顺便说一下:在那些你需要显式指定枚举值的更冗长的声明版本中,一个好处是你可以获得枚举选项的自动补全功能。而在将字符串转换为枚举类型的特定版本中,你却不能获得该功能。然而,后者在使用无效字符串值时将无法编译,因此需要在简洁性与自动完成方便性之间权衡。


1
我很困惑,为什么你提供了一个看起来像是'RadioButtonConverter'构造函数的东西,用于名为'RadioButtonValueConverter'的类。这是打字错误还是我漏掉了什么? - David Bowser
1
啊!好眼力!我在 Stack Overflow 这里实际上正在重命名它,因为它是一个值转换器,而不是一个单选按钮转换器,而我错过了手动重命名。谢谢!已编辑 :) - Mark A. Donohoe
1
这太棒了。您还可以使用ConverterParameter而不是OptionValue属性(这样您就不必每次都创建新的转换器)。 - Scover

2

这个例子可能看起来有点冗长,但它的意图应该很清楚。

它在ViewModel中使用了3个名为FlagForValue1FlagForValue2FlagForValue3的布尔属性。每个这3个属性都由一个名为_intValue的私有字段支持。

视图(xaml)中的3个单选按钮分别绑定到其对应的标志属性。这意味着显示“Value 1”的单选按钮绑定到ViewModel中的FlagForValue1布尔属性,其他两个单选按钮也是如此。

在设置ViewModel中的其中一个属性(例如FlagForValue1)时,重要的是还要为其他两个属性(例如FlagForValue2FlagForValue3)引发属性更改事件,以便UI(WPF INotifyPropertyChanged基础结构)可以正确地选择/取消选择每个单选按钮。

    private int _intValue;

    public bool FlagForValue1
    {
        get
        {
            return (_intValue == 1) ? true : false;
        }
        set
        {
            _intValue = 1;
            RaisePropertyChanged("FlagForValue1");
            RaisePropertyChanged("FlagForValue2");
            RaisePropertyChanged("FlagForValue3");
        }
    }

    public bool FlagForValue2
    {
        get
        {
            return (_intValue == 2) ? true : false;
        }
        set
        {
            _intValue = 2;
            RaisePropertyChanged("FlagForValue1");
            RaisePropertyChanged("FlagForValue2");
            RaisePropertyChanged("FlagForValue3");
        }
    }

    public bool FlagForValue3
    {
        get
        {
            return (_intValue == 3) ? true : false;
        }
        set
        {
            _intValue = 3;
            RaisePropertyChanged("FlagForValue1");
            RaisePropertyChanged("FlagForValue2");
            RaisePropertyChanged("FlagForValue3");
        }
    }

xaml代码如下:
                <RadioButton GroupName="Search" IsChecked="{Binding Path=FlagForValue1, Mode=TwoWay}"
                             >Value 1</RadioButton>

                <RadioButton GroupName="Search" IsChecked="{Binding Path=FlagForValue2, Mode=TwoWay}"
                             >Value 2</RadioButton>

                <RadioButton GroupName="Search" IsChecked="{Binding Path=FlagForValue3, Mode=TwoWay}"
                             >Value 3</RadioButton>

请添加一些解释。 - Mohit Jain
2
丑陋如罪恶,但基本上这也是我所做的。 - Jonathan Allen

1

Aviad P.的回答非常有效。然而,我不得不在OnRadioBindingChanged中将等式检查更改为比较字符串,否则枚举将与字符串值进行比较,最初没有选中任何单选按钮。

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

0
我基于Aviad的回答创建了一个附加属性,它不需要创建一个新类。
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);
        }
    }
}

用法:

<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}"/>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接