简单 WPF 单选按钮绑定 WPF? ?

对于值1、2或3,将一组3个单选按钮绑定到 int 类型的属性的最简单方法是什么?

142386 次浏览

我想到了一个简单的解决办法。

我有一个 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;
}
}

当然,在 Windows 1的资源中:

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

实际上,像这样使用转换器会破坏双向绑定,而且正如我上面所说,您也不能将其用于枚举。更好的方法是对 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>

我知道早就该这么做了,但我有另一个解决方案,更轻便更简单。从 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}"/>

希望过了这么久,有人发现这个有用:)

我很惊讶没有人想出这样的解决方案来将它绑定到 bool 数组。它可能不是最干净的,但它可以很容易地使用:

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 (呵,至少我希望如此)

这个示例可能看起来有点冗长,但其意图应该非常清楚。

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

视图(xaml)的3个 Radio 按钮分别绑定到视图模型中对应的 Flag 属性。这意味着显示“ Value 1”的单选按钮绑定到视图模型中的 FlagForValue1 bool 属性,并相应地绑定到其他两个属性。

当设置视图模型中的一个属性(例如 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>

有时可以这样在模型中求解: 假设您有3个布尔属性 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;
}
}
}

你懂的。 不是最干净的,但很简单。

我想出了一个解决方案,使用从转换器返回的 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>

我创建了一个附加属性的基础上艾维亚德的答案,不需要创建一个新的类

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

艾维德的回答非常有效。然而,我必须更改相等性检查,以比较 OnRadioBindingChanged 中的字符串,否则枚举将与字符串值进行比较,并且最初不会检查单选按钮。

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

答案是2.0

虽然我提供了 上面的答案,这是相当强大的是一个重新模板化的 ListBox,它仍然远远不是简单的单选按钮的理想选择。因此,我想出了一个简单得多的解决方案,它使用了一个实现 IValueConverterMarkupExtension子类,它具有 Binding.DoNothing的能力,Binding.DoNothing是使双向绑定工作的神奇武器。

绑定到标量值

让我们看看转换器本身是如何绑定到标量的..。

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;
}

魔法酱汁是在使用 Binding.DoNothing中的 ConvertBack函数。由于使用 RadioButton控件,每个“组”只能有一个活动选项(即只有一个 IsChecked设置为 true) ,因此我们只确保 RadioButton的绑定更新源。其他 RadioButton实例上的实例什么也不做。

下面是如何使用它绑定到 OP 要求的 int值(下面,‘ cv’是转换器代码所在的导入名称空间,传递给转换器的值是特定 RadioButton表示的值) ..。

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

简化绑定

虽然上面的代码可以工作,但是需要大量的重复代码,而且在90% 的情况下,您不需要对绑定或转换器进行任何特殊的操作。因此,让我们尝试使用 RadioButtonBinding为您设置转换器来简化事情。这是密码。

public class RadioButtonBinding : Binding {


public RadioButtonBinding(string path, object optionValue)
: base(path)
=> Converter = new RadioButtonConverter(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: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}}}" />

虽然更简单,但这里是绑定版本的调用站点,更好,但仍然冗长..。

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

枚举类型特定的变体

如果您知道将在许多场合绑定到特定的枚举类型,那么可以通过将早期的转换器子类化并绑定为特定于枚举的变体来简化上述操作。

下面正是用上面定义的 TestEnum做的,像这样..。

// 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) { }
}

这是呼叫网站。

<!- 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 解析器会自动处理字符串到枚举的转换,从而使代码更易于阅读。没有比这更简单的了!:)

旁注: 在这些版本中,您可以在其更详细的声明中显式地指定枚举值,这是一个很好的事情,您可以为枚举的情况获得自动完成。对于为您转换字符串的枚举类型特定的版本,您不会得到这样的结果。但是,如果您使用无效的字符串值,后者将无法编译,因此折衷的方法是简洁与自动完成方便。