MVVM: Binding radio buttons to a view model?
Solution 1
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 ListBox
control so that it shows up as a RadioButton
list.
<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>
Solution 2
Looks like they fixed binding to the IsChecked
property in .NET 4. A project that was broken in VS2008 works in VS2010.
Solution 3
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.
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 ListButtons
in the view model to represent the buttons in the list box, and I use each button's Tag
property to pass a string value of the enum value to use for that button. The ListBox.SelectedValuePath
property allows me to specify the Tag
property as the source for the selected value, which I bind to the view model using the SelectedValue
property. 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.
Here is the complete markup for 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 PropertyChanged
event:
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 SelectedButton
setter will call a service class method that will take the action required when a button is selected.
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!
Solution 4
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;
});
}
}
Solution 5
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.
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.)
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.
Comments
-
David Veeneman over 4 years
EDIT: Problem was fixed in .NET 4.0.
I have been trying to bind a group of radio buttons to a view model using the
IsChecked
button. After reviewing other posts, it appears that theIsChecked
property simply doesn't work. I have put together a short demo that reproduces the problem, which I have included below.Here is my question: Is there a straightforward and reliable way to bind radio buttons using MVVM? Thanks.
Additional information: The
IsChecked
property doesn't work for two reasons:When a button is selected, the IsChecked properties of other buttons in the group don't get set to false.
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.
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:
<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:
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
: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
IsChecked
property has been set to true. Now select Button B. Another message box will appear, saying that Button B'sIsChecked
property has been set to true, but there is no message box indicating that Button A'sIsChecked
property has been set to false--the property hasn't been changed.Now click Button A again. The button will be selected in the window, but no message box will appear--the
IsChecked
property has not been changed. Finally, click on Button B again--same result. TheIsChecked
property is not updated at all for either button after the button is first clicked. -
David Veeneman over 14 yearsThanks, but that doesn't really address the question. Even with that refactoring, you still have the problem of failure on the second click of a button.
-
David Veeneman over 14 yearsList box is the best solution of those proposed. But see stackoverflow.com/questions/883246/mvvm-radiobuttons for another solution.
-
David Veeneman over 14 yearsJaime Rodriguez has a very simple solution to the problem--set each RadioButton's GroupName to a different value. Putting each button in a different group keeps the bindings from getting wiped out when a button is selected. See the thread I referred to in my previous comment. In my app, the list box solution works best, so it is the selected answer.
-
John Bowen over 14 yearsYeah, there are so many different approaches to this that can work. I also used to use a method that relied on a converter which would return Binding.DoNothing for unchecked buttons to avoid breaking the bindings but ended up using this more often because it's less code. Good conversation starter!
-
David Veeneman over 14 yearsI don't think this proposed solution addresses the issue of the bindings getting trashed when a button is selected.
-
Doug Ferguson over 14 yearsWhen I tested it with breakpoints on the setters, the setters would get hit each time I toggled back and forth between the two radio buttons. It would seem that the bindings do not get destroyed.
-
thepaulpage over 13 yearsWhen I first realized my problem I was concerned, but this post made it all better.
-
Slauma about 13 yearsConfirmed! The demo project in the question works in .NET 4 as expected.
-
dub stylee about 8 yearsThis works well for binding the text of a
TextBlock
, but doesn't help with binding theIsChecked
property of theRadioButton
. -
rajibdotnet about 8 yearsDisplayMember seems to have no effect, it shows object.
-
Joachim Mairböck over 7 years@rajibdotnet See my answer for that, which should address this problem.
-
paraJdox1 almost 3 yearsI can confirm this too!