MVVM:将单选button绑定到视图模型?

编辑:问题已在.NET 4.0中修复。

我一直在尝试使用IsCheckedbutton将一组单选button绑定到视图模型。 在审查其他职位后,似乎IsChecked属性根本不起作用。 我已经制作了一个简短的演示文稿,重现了我在下面提到的问题。

这里是我的问题:是否有一个简单而可靠的方式来绑定单选button使用MVVM? 谢谢。

其他信息: IsChecked属性不起作用有两个原因:

  1. 当select一个button时,组中其他button的IsChecked属性不会被设置为false

  2. 当select一个button时,在第一次select该button后,它自己的IsChecked属性不会被设置。 我猜测绑定被第一次点击WPF丢弃。

演示项目:这是一个简单演示代码和标记,重现了这个问题。 创build一个WPF项目,并使用以下代码replaceWindow1.xaml中的标记:

 <Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300" Loaded="Window_Loaded"> <StackPanel> <RadioButton Content="Button A" IsChecked="{Binding Path=ButtonAIsChecked, Mode=TwoWay}" /> <RadioButton Content="Button B" IsChecked="{Binding Path=ButtonBIsChecked, Mode=TwoWay}" /> </StackPanel> </Window> 

将Window1.xaml.cs中的代码replace为以下用于设置视图模型的代码(hack):

 using System.Windows; namespace WpfApplication1 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } private void Window_Loaded(object sender, RoutedEventArgs e) { this.DataContext = new Window1ViewModel(); } } } 

现在将下面的代码添加到Window1ViewModel.cs项目中:

 using System.Windows; namespace WpfApplication1 { public class Window1ViewModel { private bool p_ButtonAIsChecked; /// <summary> /// Summary /// </summary> public bool ButtonAIsChecked { get { return p_ButtonAIsChecked; } set { p_ButtonAIsChecked = value; MessageBox.Show(string.Format("Button A is checked: {0}", value)); } } private bool p_ButtonBIsChecked; /// <summary> /// Summary /// </summary> public bool ButtonBIsChecked { get { return p_ButtonBIsChecked; } set { p_ButtonBIsChecked = value; MessageBox.Show(string.Format("Button B is checked: {0}", value)); } } } } 

要重现此问题,请运行该应用程序并单击buttonA.将出现一个消息框,指出Button A的IsChecked属性已设置为true 。 现在selectbuttonB.另一个消息框将出现,说buttonB的IsChecked属性已被设置为true ,但没有消息框指示buttonA的IsChecked属性已被设置为false – 属性尚未更改。

现在再次点击buttonA. 该button将在窗口中被选中,但不会出现消息框 – IsChecked属性尚未更改。 最后,再次点击buttonB – 结果相同。 第一次单击button后, IsChecked属性根本不会更新。

如果你从Jason的build议开始,那么问题就变成了一个列表中的一个单一的界限select,这个列表很好地转换成一个ListBox 。 在这一点上,将样式应用到ListBox控件,以便它显示为RadioButton列表是微不足道的。

 <ListBox ItemsSource="{Binding ...}" SelectedItem="{Binding ...}"> <ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <RadioButton Content="{TemplateBinding Content}" IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected}"/> </ControlTemplate> </Setter.Value> </Setter> </Style> </ListBox.ItemContainerStyle> </ListBox> 

看起来像他们固定绑定到.NET 4中的IsChecked属性。在VS2008中打破的项目在VS2010中工作。

为了所有研究这个问题的人的利益,这里是我最终实现的解决scheme。 它build立在John Bowen的答案上,我select这个答案作为解决问题的最佳scheme。

首先,我创build了一个包含单选button作为项目的透明列表框的样式。 然后,我创build了button进入列表框 – 我的button是固定的,而不是作为数据读入应用程序,所以我把它们硬编码到标记中。

我在视图模型中使用一个名为ListButtons的枚举来表示列表框中的button,我使用每个button的Tag属性来传递该button所使用的枚举值的string值。 ListBox.SelectedValuePath属性允许我将Tag属性指定为所选值的源,我使用SelectedValue属性将其绑定到视图模型。 我想我需要一个值转换器来转换string和枚举值,但WPF的内置转换器处理转换没有问题。

这里是Window1.xaml的完整标记:

 <Window x:Class="RadioButtonMvvmDemo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <!-- Resources --> <Window.Resources> <Style x:Key="RadioButtonList" TargetType="{x:Type ListBox}"> <Setter Property="Background" Value="Transparent"/> <Setter Property="ItemContainerStyle"> <Setter.Value> <Style TargetType="{x:Type ListBoxItem}" > <Setter Property="Margin" Value="5" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <Border BorderThickness="0" Background="Transparent"> <RadioButton Focusable="False" IsHitTestVisible="False" IsChecked="{TemplateBinding IsSelected}"> <ContentPresenter /> </RadioButton> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </Setter.Value> </Setter> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBox}"> <Border BorderThickness="0" Padding="0" BorderBrush="Transparent" Background="Transparent" Name="Bd" SnapsToDevicePixels="True"> <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <!-- Layout --> <Grid> <!-- Note that we use SelectedValue, instead of SelectedItem. This allows us to specify the property to take the value from, using SelectedValuePath. --> <ListBox Style="{StaticResource RadioButtonList}" SelectedValuePath="Tag" SelectedValue="{Binding Path=SelectedButton}"> <ListBoxItem Tag="ButtonA">Button A</ListBoxItem> <ListBoxItem Tag="ButtonB">Button B</ListBoxItem> </ListBox> </Grid> </Window> 

视图模型有一个属性SelectedButton,它使用ListButtons枚举来显示哪个button被选中。 该属性在我用于视图模型的基类中调用一个事件,这引发了PropertyChanged事件:

 namespace RadioButtonMvvmDemo { public enum ListButtons {ButtonA, ButtonB} public class Window1ViewModel : ViewModelBase { private ListButtons p_SelectedButton; public Window1ViewModel() { SelectedButton = ListButtons.ButtonB; } /// <summary> /// The button selected by the user. /// </summary> public ListButtons SelectedButton { get { return p_SelectedButton; } set { p_SelectedButton = value; base.RaisePropertyChangedEvent("SelectedButton"); } } } } 

在我的产品应用程序中, SelectedButton设置器将调用服务类方法,该方法将在selectbutton时采取所需的操作。

要完成,这里是基类:

 using System.ComponentModel; namespace RadioButtonMvvmDemo { public abstract class ViewModelBase : INotifyPropertyChanged { #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion #region Protected Methods /// <summary> /// Raises the PropertyChanged event. /// </summary> /// <param name="propertyName">The name of the changed property.</param> protected void RaisePropertyChangedEvent(string propertyName) { if (PropertyChanged != null) { PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName); PropertyChanged(this, e); } } #endregion } } 

希望有所帮助!

一种解决scheme是更新属性设置器中单选button的ViewModel。 当buttonA设置为True时,将buttonB设置为false。

绑定到DataContext中的对象的另一个重要因素是该对象应该实现INotifyPropertyChanged。 当任何绑定的属性发生变化时,该事件应该被触发,并包含已更改属性的名称。 (为简洁起见,在样本中省略了空的检查)。

 public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected bool _ButtonAChecked = true; public bool ButtonAChecked { get { return _ButtonAChecked; } set { _ButtonAChecked = value; PropertyChanged(this, new PropertyChangedEventArgs("ButtonAChecked")); if (value) ButtonBChecked = false; } } protected bool _ButtonBChecked; public bool ButtonBChecked { get { return _ButtonBChecked; } set { _ButtonBChecked = value; PropertyChanged(this, new PropertyChangedEventArgs("ButtonBChecked")); if (value) ButtonAChecked = false; } } } 

编辑:

问题是,当第一次点击buttonB时,IsChecked值改变,并且绑定直通,但是buttonA并没有通过未经检查的状态进入ButtonAChecked属性。 通过在代码中手动更新,ButtonAtheck属性设置器将在下次按下buttonA时被调用。

不确定任何IsChecked错误,可以对视图模型做出一个可能的重构:视图有许多由一系列RadioButton表示的互斥状态,在任何给定时间只能select其中一个。 在视图模型中,只有1个属性(例如一个枚举)代表了可能的状态:stateA,stateB等这样你就不需要所有的个人ButtonAIsChecked等

这是另一种方法,你可以做到这一点

视图:

 <StackPanel Margin="90,328,965,389" Orientation="Horizontal"> <RadioButton Content="Mr" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/> <RadioButton Content="Mrs" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/> <RadioButton Content="Ms" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/> <RadioButton Content="Other" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}}" GroupName="Title"/> <TextBlock Text="{Binding SelectedTitle, Mode=TwoWay}"/> </StackPanel> 

视图模型:

  private string selectedTitle; public string SelectedTitle { get { return selectedTitle; } set { SetProperty(ref selectedTitle, value); } } public RelayCommand TitleCommand { get { return new RelayCommand((p) => { selectedTitle = (string)p; }); } } 

John Bowen 答案的一个小扩展:当值不实现ToString()时,它不起作用。 你需要什么,而不是将RadioButton的Content设置为TemplateBinding,只需将一个ContentPresenter放入它,就像这样:

 <ListBox ItemsSource="{Binding ...}" SelectedItem="{Binding ...}"> <ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <RadioButton IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected}"> <ContentPresenter/> </RadioButton> </ControlTemplate> </Setter.Value> </Setter> </Style> </ListBox.ItemContainerStyle> </ListBox> 

这样,您可以根据需要另外使用DisplayMemberPathItemTemplate 。 RadioButton只是“包装”项目,提供select。

您必须添加单选button的组名称

  <StackPanel> <RadioButton Content="Button A" IsChecked="{Binding Path=ButtonAIsChecked, Mode=TwoWay}" GroupName="groupName" /> <RadioButton Content="Button B" IsChecked="{Binding Path=ButtonBIsChecked, Mode=TwoWay}" GroupName="groupName" /> </StackPanel> 

我在VS2015和.NET 4.5.1中有一个非常类似的问题

XAML:

  <ListView.ItemsPanel> <ItemsPanelTemplate> <UniformGrid Columns="6" Rows="1"/> </ItemsPanelTemplate> </ListView.ItemsPanel> <ListView.ItemTemplate> <DataTemplate > <RadioButton GroupName="callGroup" Style="{StaticResource itemListViewToggle}" Click="calls_ItemClick" Margin="1" IsChecked="{Binding Path=Selected,Mode=TwoWay}" Unchecked="callGroup_Checked" Checked="callGroup_Checked"> 

….

正如你在这段代码中看到的,我有一个listview,而模板中的项目是属于一个groupname的单选button。

如果我添加一个新的项目的集合与属性select设置为True它会出现选中,其余的button保持检查。

我通过首先获取checkedbutton并手动设置为false来解决它,但这不是它应该完成的方式。

代码背后:

 `.... lstInCallList.ItemsSource = ContactCallList AddHandler ContactCallList.CollectionChanged, AddressOf collectionInCall_change ..... Public Sub collectionInCall_change(sender As Object, e As NotifyCollectionChangedEventArgs) 'Whenever collection change we must test if there is no selection and autoselect first. If e.Action = NotifyCollectionChangedAction.Add Then 'The solution is this, but this shouldn't be necessary 'Dim seleccionado As RadioButton = getCheckedRB(lstInCallList) 'If seleccionado IsNot Nothing Then ' seleccionado.IsChecked = False 'End If DirectCast(e.NewItems(0), PhoneCall).Selected = True ..... End sub 

`

 <RadioButton IsChecked="{Binding customer.isMaleFemale}">Male</RadioButton> <RadioButton IsChecked="{Binding customer.isMaleFemale,Converter= {StaticResource GenderConvertor}}">Female</RadioButton> 

以下是IValueConverter的代码

 public class GenderConvertor : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return !(bool)value; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return !(bool)value; } } 

这对我工作。 甚至根据单选button的点击,视图和视图模型上的值都被绑定。 真 – >男与女 – >女

我知道这是一个古老的问题,原来的问题已经在.NET 4中解决了,并且诚实地说这是一个稍微偏离主题的问题。

在大多数情况下,我想要在MVVM中使用RadioButtons,它是在一个枚举的元素之间进行select,这需要将虚拟机空间中的一个bool属性绑定到每个button,并使用它们来设置反映实际select的总体枚举属性,这非常繁琐很快。 所以我想出了一个可重用且易于实现的解决scheme,并且不需要ValueConverters。

视图几乎是一样的,但一旦你有你的枚举到位的虚拟机端可以完成一个单一的属性。

MainWindowVM

 using System.ComponentModel; namespace EnumSelectorTest { public class MainWindowVM : INotifyPropertyChanged { public EnumSelectorVM Selector { get; set; } private string _colorName; public string ColorName { get { return _colorName; } set { if (_colorName == value) return; _colorName = value; RaisePropertyChanged("ColorName"); } } public MainWindowVM() { Selector = new EnumSelectorVM ( typeof(MyColors), MyColors.Red, false, val => ColorName = "The color is " + ((MyColors)val).ToString() ); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } } 

做所有工作的类从DynamicObjectinheritance。 从外部看来,它为以'Is','IsRed','IsBlue'等为前缀的枚举中的每个元素创build一个bool属性,这些属性可以从XAML绑定。 与一个Value属性一起保存实际的枚举值。

 public enum MyColors { Red, Magenta, Green, Cyan, Blue, Yellow } 

EnumSelectorVM

 using System; using System.ComponentModel; using System.Dynamic; using System.Linq; namespace EnumSelectorTest { public class EnumSelectorVM : DynamicObject, INotifyPropertyChanged { //------------------------------------------------------------------------------------------------------------------------------------------ #region Fields private readonly Action<object> _action; private readonly Type _enumType; private readonly string[] _enumNames; private readonly bool _notifyAll; #endregion Fields //------------------------------------------------------------------------------------------------------------------------------------------ #region Properties private object _value; public object Value { get { return _value; } set { if (_value == value) return; _value = value; RaisePropertyChanged("Value"); _action?.Invoke(_value); } } #endregion Properties //------------------------------------------------------------------------------------------------------------------------------------------ #region Constructor public EnumSelectorVM(Type enumType, object initialValue, bool notifyAll = false, Action<object> action = null) { if (!enumType.IsEnum) throw new ArgumentException("enumType must be of Type: Enum"); _enumType = enumType; _enumNames = enumType.GetEnumNames(); _notifyAll = notifyAll; _action = action; //do last so notification fires and action is executed Value = initialValue; } #endregion Constructor //------------------------------------------------------------------------------------------------------------------------------------------ #region Methods //--------------------------------------------------------------------- #region Public Methods public override bool TryGetMember(GetMemberBinder binder, out object result) { string elementName; if (!TryGetEnumElemntName(binder.Name, out elementName)) { result = null; return false; } try { result = Value.Equals(Enum.Parse(_enumType, elementName)); } catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException) { result = null; return false; } return true; } public override bool TrySetMember(SetMemberBinder binder, object newValue) { if (!(newValue is bool)) return false; string elementName; if (!TryGetEnumElemntName(binder.Name, out elementName)) return false; try { if((bool) newValue) Value = Enum.Parse(_enumType, elementName); } catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException) { return false; } if (_notifyAll) foreach (var name in _enumNames) RaisePropertyChanged("Is" + name); else RaisePropertyChanged("Is" + elementName); return true; } #endregion Public Methods //--------------------------------------------------------------------- #region Private Methods private bool TryGetEnumElemntName(string bindingName, out string elementName) { elementName = ""; if (bindingName.IndexOf("Is", StringComparison.Ordinal) != 0) return false; var name = bindingName.Remove(0, 2); // remove first 2 chars "Is" if (!_enumNames.Contains(name)) return false; elementName = name; return true; } #endregion Private Methods #endregion Methods //------------------------------------------------------------------------------------------------------------------------------------------ #region Events public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #endregion Events } } 

要响应更改,您可以订阅NotifyPropertyChanged事件,或者像上面那样向构造函数传递一个匿名方法。

最后是MainWindow.xaml

 <Window x:Class="EnumSelectorTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Grid> <StackPanel> <RadioButton IsChecked="{Binding Selector.IsRed}">Red</RadioButton> <RadioButton IsChecked="{Binding Selector.IsMagenta}">Magenta</RadioButton> <RadioButton IsChecked="{Binding Selector.IsBlue}">Blue</RadioButton> <RadioButton IsChecked="{Binding Selector.IsCyan}">Cyan</RadioButton> <RadioButton IsChecked="{Binding Selector.IsGreen}">Green</RadioButton> <RadioButton IsChecked="{Binding Selector.IsYellow}">Yellow</RadioButton> <TextBlock Text="{Binding ColorName}"/> </StackPanel> </Grid> </Window> 

希望别人觉得这个有用,因为我认为这个在我的工具箱里。