我想到了一个简单的解决方案。
我有一个 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>
我非常惊讶没有人提出这样一种解决方案,将其绑定到布尔数组上。它可能不是最简洁的,但非常容易使用:
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}"/>
注意:如果您不想默认选中一个,则不需要双向绑定。双向绑定是此解决方案的最大缺点。
优点:
请查看我在同一页上发布的新答案,它提供了一种完全不同且更简单的解决此问题的方法。这种新方法使用自定义的IValueConverter
和Binding
子类,让您可以回到使用真正的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>
<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>
我知道这已经迟了很久,但是我有一个替代方案,更轻更简单。从 System.Windows.Controls.RadioButton
派生一个类,并声明两个依赖属性 RadioValue
和 RadioBinding
。然后在类代码中,重写 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}"/>
希望这篇文章经过这么长时间后,有人能够发现它并从中受益 :)
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>
catch (ArgumentException)
部分更改为以下内容,以便在 XAML 中更清楚地显示错误:throw new NotSupportedException($"Enum {this.Type} doesn't contain value '{parameter}'.");
- Sören Kuklau<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;
}
}
}
虽然我在这个页面上提供了被接受的答案,正如评论者们正确指出的那样,尽管它功能强大且非常灵活,但当你只想使用简单的RadioButton
控件时,它的复杂性远非理想。
因此,我提出了一种新的方法,如下所示,它使用了自定义的IValueConverter
和Binding.DoNothing
在其ConvertBack
方法中的力量,这是使双向绑定到RadioButton
控件按预期工作的魔法调味料。
让我们来看看转换器本身:
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}}" />
如果您知道您将在许多场合绑定到特定的枚举类型,您可以通过对早期的RadioButtonValueConverter
和RadioButtonBinding
进行子类化来简化上述过程,以创建特定于枚举的变体。
下面是使用上面定义的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解析器会自动处理字符串到枚举类型的转换,使得你的代码更容易阅读。这就是如此简单!:)
顺便说一下:在那些你需要显式指定枚举值的更冗长的声明版本中,一个好处是你可以获得枚举选项的自动补全功能。而在将字符串转换为枚举类型的特定版本中,你却不能获得该功能。然而,后者在使用无效字符串值时将无法编译,因此需要在简洁性与自动完成方便性之间权衡。
ConverterParameter
而不是OptionValue
属性(这样您就不必每次都创建新的转换器)。 - Scover这个例子可能看起来有点冗长,但它的意图应该很清楚。
它在ViewModel中使用了3个名为FlagForValue1
、FlagForValue2
和FlagForValue3
的布尔属性。每个这3个属性都由一个名为_intValue
的私有字段支持。
视图(xaml)中的3个单选按钮分别绑定到其对应的标志属性。这意味着显示“Value 1”的单选按钮绑定到ViewModel中的FlagForValue1
布尔属性,其他两个单选按钮也是如此。
在设置ViewModel中的其中一个属性(例如FlagForValue1
)时,重要的是还要为其他两个属性(例如FlagForValue2
和FlagForValue3
)引发属性更改事件,以便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");
}
}
<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>
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);
}
}
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}"/>