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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2284752/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-07 01:04:31  来源:igfitidea点击:

MVVM: Binding radio buttons to a view model?

c#wpfmvvmbindingradio-button

提问by David Veeneman

EDIT:Problem was fixed in .NET 4.0.

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

I have been trying to bind a group of radio buttons to a view model using the IsCheckedbutton. After reviewing other posts, it appears that the IsCheckedproperty simply doesn't work. I have put together a short demo that reproduces the problem, which I have included below.

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

Here is my question: Is there a straightforward and reliable way to bind radio buttons using MVVM? Thanks.

这是我的问题:是否有一种简单可靠的方法来使用 MVVM 绑定单选按钮?谢谢。

Additional information:The IsCheckedproperty doesn't work for two reasons:

附加信息:IsChecked属性不起作用有两个原因:

  1. When a button is selected, the IsChecked properties of other buttons in the group don't get set to false.

  2. When a button is selected, its own IsChecked property does not get set after the first time the button is selected. I am guessing that the binding is getting trashed by WPF on the first click.

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

  2. 当一个按钮被选中时,它自己的 IsChecked 属性在第一次被选中后不会被设置。我猜测绑定在第一次点击时被 WPF 破坏了。

Demo project:Here is the code and markup for a simple demo that reproduces the problem. Create a WPF project and replace the markup in Window1.xaml with the following:

演示项目:这是重现问题的简单演示的代码和标记。创建一个 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>

Replace the code in Window1.xaml.cs with the following code (a hack), which sets the view model:

将 Window1.xaml.cs 中的代码替换为以下设置视图模型的代码(一个 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();
        }
    }
}

Now add the following code to the project as Window1ViewModel.cs:

现在将以下代码添加到项目中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));
            }
        }

    }
}

To reproduce the problem, run the app and click Button A. A message box will appear, saying that Button A's IsCheckedproperty has been set to true. Now select Button B. Another message box will appear, saying that Button B's IsCheckedproperty has been set to true, but there is no message box indicating that Button A's IsCheckedproperty has been set to false--the property hasn't been changed.

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

Now click Button A again. The button will be selected in the window, but no message box will appear--the IsCheckedproperty has not been changed. Finally, click on Button B again--same result. The IsCheckedproperty is not updated at all for either button after the button is first clicked.

现在再次单击按钮 A。该按钮将在窗口中被选中,但不会出现消息框——该IsChecked属性没有被更改。最后,再次单击按钮 B——结果相同。IsChecked在第一次单击按钮后,任何一个按钮的属性都不会更新。

采纳答案by John Bowen

If you start with Jason's suggestion then the problem becomes a single bound selection from a list which translates very nicely to a ListBox. At that point it's trivial to apply styling to a ListBoxcontrol so that it shows up as a RadioButtonlist.

如果您从 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>

回答by Jason Roberts

Not sure about any IsChecked bugs, one possible refactor you could make to your viewmodel:the view has a number of mutually exclusive states represented by a series of RadioButtons, only one of which at any given time can be selected. In the view model, just have 1 property (e.g. an enum) which represents the possible states: stateA, stateB, etc That way you wouldn't need all the individual ButtonAIsChecked, etc

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

回答by Doug Ferguson

One solution is to update the ViewModel for the radio buttons in the setter of the properties. When Button A is set to True, set Button B to false.

一种解决方案是更新属性设置器中单选按钮的 ViewModel。当按钮 A 设置为 True 时,将按钮 B 设置为 false。

Another important factor when binding to an object in the DataContext is that the object should implement INotifyPropertyChanged. When any bound property changes, the event should be fired and include the name of the changed property. (Null check omitted in the sample for brevity.)

绑定到 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;
        }
    }
}

Edit:

编辑:

The issue is that when first clicking on Button B the IsChecked value changes and the binding feeds through, but Button A does not feed through its unchecked state to the ButtonAChecked property. By manually updating in code the ButtonAChecked property setter will get called the next time Button A is clicked.

问题在于,当第一次单击按钮 B 时,IsChecked 值会发生变化并且绑定会传递,但按钮 A 不会将其未选中状态传递给 ButtonAChecked 属性。通过手动更新代码,ButtonAChecked 属性设置器将在下次单击按钮 A 时被调用。

回答by David Veeneman

For the benefit of anyone researching this question down the road, here is the solution I ultimately implemented. It builds on John Bowen's answer, which I selected as the best solution to the problem.

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

First, I created a style for a transparent list box containing radio buttons as items. Then, I created the buttons to go in the list box--my buttons are fixed, rather than read into the app as data, so I hard-coded them into the markup.

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

I use an enum called ListButtonsin the view model to represent the buttons in the list box, and I use each button's Tagproperty to pass a string value of the enum value to use for that button. The ListBox.SelectedValuePathproperty allows me to specify the Tagproperty as the source for the selected value, which I bind to the view model using the SelectedValueproperty. I thought I would need a value converter to convert between the string and its enum value, but WPF's built-in converters handled the conversion without problem.

我使用ListButtons在视图模型中调用的枚举来表示列表框中的按钮,并且我使用每个按钮的Tag属性来传递用于该按钮的枚举值的字符串值。该ListBox.SelectedValuePath属性允许我将该属性指定Tag为所选值的源,我使用该SelectedValue属性将其绑定到视图模型。我以为我需要一个值转换器来在字符串和它的枚举值之间进行转换,但是 WPF 的内置转换器可以毫无问题地处理转换。

Here is the complete markup for Window1.xaml:

这是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>

The view model has a single property, SelectedButton, which uses a ListButtons enum to show which button is selected. The property calls an event in the base class I use for view models, which raises the PropertyChangedevent:

视图模型有一个属性 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");
            }
        }

    }
} 

In my production app, the SelectedButtonsetter will call a service class method that will take the action required when a button is selected.

在我的生产应用程序中,SelectedButtonsetter 将调用一个服务类方法,该方法将在选择按钮时采取所需的操作。

And to be complete, here is the base class:

为了完整起见,这里是基类:

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

Hope that helps!

希望有帮助!

回答by RandomEngy

Looks like they fixed binding to the IsCheckedproperty in .NET 4. A project that was broken in VS2008 works in VS2010.

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

回答by Enkosi

Here is another way you can do it

这是您可以执行的另一种方法

VIEW:

看法:

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

ViewModel:

视图模型:

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

回答by Joachim Mairb?ck

A small extension to John Bowen's answer: It doesn't work when the values don't implement ToString(). What you need instead of setting the Contentof the RadioButton to a TemplateBinding, just put a ContentPresenterin it, like this:

John Bowen答案的一个小扩展:当值没有实现时它不起作用ToString()。您需要什么而不是Content将 RadioButton 设置为 TemplateBinding,只需将 aContentPresenter放入其中,如下所示:

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

This way you can additionally use DisplayMemberPathor an ItemTemplateas appropriate. The RadioButton just "wraps" the items, providing the selection.

这样,您可以额外使用DisplayMemberPathItemTemplate。RadioButton 只是“包装”项目,提供选择。

回答by ptsivakumar

You have to add the Group Name for the Radio 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>

回答by Jose

I have a very similar problem in VS2015 and .NET 4.5.1

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

XAML:

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

....

....

As you can see in this code i have a listview, and items in template are radiobuttons that belongs to a groupname.

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

If I add a new item to the collection with the property Selected set to True it appears checked and the rest of buttons remain checked.

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

I solve it by getting the checkedbutton first and set it to false manually but this is not the way it's supposed to be done.

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

code behind:

后面的代码:

`....
  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

`

`

回答by jeevan

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

Below is the code for IValueConverter

下面是 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;
    }
}

this worked for me. Even value got binded on both view and viewmodel according to the radio button click. True--> Male and False-->Female

这对我有用。根据单选按钮单击,甚至值都绑定在视图和视图模型上。真--> 男和假--> 女