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

时间:2010-02-17 22:05:54

标签: c# wpf mvvm binding radio-button

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

我一直在尝试使用IsChecked按钮将一组单选按钮绑定到视图模型。在查看其他帖子后,似乎IsChecked属性根本不起作用。我已经整理了一个简短的演示,可以重现这个问题,我在下面提到了这个问题。

以下是我的问题:使用MVVM绑定单选按钮是否有直接可靠的方法?感谢。

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

  1. 选择按钮后,组中其他按钮的IsChecked属性不会设置为 false

  2. 选择按钮后,第一次选择按钮后,其自身的IsChecked属性不会被设置。我猜测第一次点击时WPF会破坏绑定。

  3. 演示项目:以下是重现问题的简单演示的代码和标记。创建一个WPF项目并使用以下内容替换Window1.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>
    

    使用以下代码(hack)替换Window1.xaml.cs中的代码,该代码设置视图模型:

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

    要重现此问题,请运行该应用并单击按钮A.将出现一个消息框,指出按钮A的IsChecked属性已设置为 true 。现在选择Button B.将出现另一个消息框,表示Button B的IsChecked属性已设置为 true ,但没有消息框指示Button A的IsChecked属性已设置为 false - 该属性尚未更改。

    现在再次点击按钮A.该按钮将在窗口中选中,但不会显示任何消息框 - IsChecked属性尚未更改。最后,再次点击Button B - 结果相同。首次单击按钮后,任何按钮都不会更新IsChecked属性。

10 个答案:

答案 0 :(得分:52)

如果你从Jason的建议开始,那么问题就会成为一个列表中的单个绑定选择,该列表可以很好地转换为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>

答案 1 :(得分:17)

看起来他们修复了.NET 4中IsChecked属性的绑定。在VS2008中破解的项目在VS2010中有效。

答案 2 :(得分:9)

为了任何研究这个问题的人的利益,这是我最终实施的解决方案。它建立在John Bowen的答案之上,我选择这个答案作为问题的最佳解决方案。

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

我在视图模型中使用一个名为ListButtons的枚举来表示列表框中的按钮,我使用每个按钮的Tag属性来传递枚举值的字符串值以用于该值按钮。 ListBox.SelectedValuePath属性允许我指定Tag属性作为所选值的源,我使用SelectedValue属性绑定到视图模型。我以为我需要一个值转换器来在字符串及其枚举值之间进行转换,但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枚举来显示选择了哪个按钮。该属性调用我用于视图模型的基类中的事件,这会引发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 setter将调用一个服务类方法,该方法将在选择按钮时执行所需的操作。

要完成,这是基类:

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

希望有所帮助!

答案 3 :(得分:3)

一种解决方案是更新属性setter中单选按钮的ViewModel。当按钮A设置为True时,将Button B设置为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;
        }
    }
}

编辑:

问题在于,当第一次单击按钮B时,IsChecked值会更改并且绑定会通过,但按钮A不会通过其未经检查的状态进入ButtonAChecked属性。通过手动更新代码,ButtonAChecked属性setter将在下次单击Button A时被调用。

答案 4 :(得分:2)

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

答案 5 :(得分:2)

这是你可以采取的另一种方式

查看:

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

答案 6 :(得分:1)

您必须为单选按钮添加组名

.pdf

答案 7 :(得分:1)

我知道这是一个老问题,原始问题已在.NET 4中得到解决。而且说实话,这有点偏离主题。

在我想在MVVM中使用RadioButtons的大多数情况下,要在枚举的元素之间进行选择,这需要绑定 bool 在每个按钮的VM空间中的属性,并使用它们来设置反映实际选择的整体 enum 属性,这非常繁琐非常繁琐。所以我想出了一个可重用且易于实现的解决方案,并且不需要ValueConverters。

视图几乎相同,但是一旦你的枚举到位,就可以用一个属性来完成VM端。

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

执行所有工作的类继承自DynamicObject。从外部看,它为 enum 中的每个元素创建一个 bool 属性,前缀为&#39; Is&#39;,&#39; IsRed&#39;,& #39; IsBlue&#39;可以从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>

希望其他人觉得这很有用,因为我认为这些会在我的工具箱中出现。

答案 8 :(得分:0)

我在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">

...

正如您在此代码中看到的,我有一个列表视图,模板中的项目是属于组名的单选按钮。

如果我将新项目添加到集合中,并将属性Selected设置为True,则会显示已选中状态,其余按钮仍保持选中状态。

我通过首先获取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

`

答案 9 :(得分:0)

<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;
    }
}
这对我有用。根据单选按钮单击,甚至在视图和视图模型上都绑定了值。真 - &GT;男性和假 - &gt;女性