WPF MVVM Radio buttons on ItemsControl
Solution 1
It turns out that it is much simpler to abandon using ItemsControl
and instead go with ListBox
.
It may be more heavy-weight, but that's mostly because it is doing the heavy lifting for you. It is really easy to do a two-way binding between RadioButton.IsChecked
and ListBoxItem.IsSelected
. With the proper control template for the ListBoxItem
, you can easily get rid of all the selection visual.
<ListBox ItemsSource="{Binding Properties}" SelectedItem="{Binding SelectedItem}">
<ListBox.ItemContainerStyle>
<!-- Style to get rid of the selection visual -->
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<ContentPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:SomeClass}">
<RadioButton Content="{Binding Name}" GroupName="Properties">
<!-- Binding IsChecked to IsSelected requires no support code -->
<RadioButton.IsChecked>
<Binding Path="IsSelected"
RelativeSource="{RelativeSource AncestorType=ListBoxItem}"
Mode="TwoWay" />
</RadioButton.IsChecked>
</RadioButton>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Solution 2
As far as I know, there's no good way to do this with a MultiBinding
, although you initially think there would be. Since you can't bind the ConverterParameter
, your ConvertBack
implementation doesn't have the information it needs.
What I have done is created a separate EnumModel
class solely for the purpose of binding an enum to radio buttons. Use a converter on the ItemsSource
property and then you're binding to an EnumModel
. The EnumModel
is just a forwarder object to make binding possible. It holds one possible value of the enum and a reference to the viewmodel so it can translate a property on the viewmodel to and from a boolean.
Here's an untested but generic version:
<ItemsControl ItemsSource="{Binding Converter={StaticResource theConverter} ConverterParameter="SomeEnumProperty"}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton IsChecked="{Binding IsChecked}">
<TextBlock Text="{Binding Name}" />
</RadioButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The converter:
public class ToEnumModelsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var viewmodel = value;
var prop = viewmodel.GetType().GetProperty(parameter as string);
List<EnumModel> enumModels = new List<EnumModel>();
foreach(var enumValue in Enum.GetValues(prop.PropertyType))
{
var enumModel = new EnumModel(enumValue, viewmodel, prop);
enumModels.Add(enumModel);
}
return enumModels;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The EnumModel:
public class EnumModel : INPC
{
object enumValue;
INotifyPropertyChanged viewmodel;
PropertyInfo property;
public EnumModel(object enumValue, object viewmodel, PropertyInfo property)
{
this.enumValue = enumValue;
this.viewmodel = viewmodel as INotifyPropertyChanged;
this.property = property;
this.viewmodel.PropertyChanged += new PropertyChangedEventHandler(viewmodel_PropertyChanged);
}
void viewmodel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == property.Name)
{
OnPropertyChanged("IsChecked");
}
}
public bool IsChecked
{
get
{
return property.GetValue(viewmodel, null).Equals(enumValue);
}
set
{
if (value)
{
property.SetValue(viewmodel, enumValue, null);
}
}
}
}
For a code sample that I know works (but it's still quite unpolished - WIP!), you can see http://code.google.com/p/pdx/source/browse/trunk/PDX/PDX/Toolkit/EnumControl.xaml.cs. This only works within the context of my library, but it demonstrates setting the Name of the EnumModel based on the DescriptionAttribute
, which might be useful to you.
Solution 3
You are so close. When you are need two bindings for one converter you need a MultiBinding
and a IMultiValueConverter
! The syntax is a little more verbose but no more difficult.
Edit:
Here's a little code to get you started.
The binding:
<RadioButton Content="{Binding Name}"
Grid.Column="0">
<RadioButton.IsChecked>
<MultiBinding Converter="{StaticResource EqualsConverter}">
<Binding Path="SelectedItem"/>
<Binding Path="Name"/>
</MultiBinding>
</RadioButton.IsChecked>
</RadioButton>
and the converter:
public class EqualsConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0].Equals(values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Second Edit:
The above approach is not useful to implement two-way binding using the technique linked in the question because the necessary information is not available when converting back.
The correct solution I believe is straight-up MVVM: code the view-model to match the needs of the view. The amount of code is quite small and obviates the need for any converters or funny bindings or tricks.
Here is the XAML;
<Grid>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton
GroupName="Value"
Content="{Binding Description}"
IsChecked="{Binding IsChecked, Mode=TwoWay}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
and code-behind to simulate the view-model:
DataContext = new CheckBoxValueCollection(new[] { "Foo", "Bar", "Baz" });
and some view-model infrastructure:
public class CheckBoxValue : INotifyPropertyChanged
{
private string description;
private bool isChecked;
public string Description
{
get { return description; }
set { description = value; OnPropertyChanged("Description"); }
}
public bool IsChecked
{
get { return isChecked; }
set { isChecked = value; OnPropertyChanged("IsChecked"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public class CheckBoxValueCollection : ObservableCollection<CheckBoxValue>
{
public CheckBoxValueCollection(IEnumerable<string> values)
{
foreach (var value in values)
this.Add(new CheckBoxValue { Description = value });
this[0].IsChecked = true;
}
public string SelectedItem
{
get { return this.First(item => item.IsChecked).Description; }
}
}
Solution 4
Now that I know about x:Shared (thanks to your other question), I renounce my previous answer and say that a MultiBinding
is the way to go after all.
The XAML:
<StackPanel>
<TextBlock Text="{Binding SelectedChoice}" />
<ItemsControl ItemsSource="{Binding Choices}">
<ItemsControl.Resources>
<local:MyConverter x:Key="myConverter" x:Shared="false" />
</ItemsControl.Resources>
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton>
<RadioButton.IsChecked>
<MultiBinding Converter="{StaticResource myConverter}" >
<Binding Path="DataContext.SelectedChoice" RelativeSource="{RelativeSource AncestorType=UserControl}" />
<Binding Path="DataContext" RelativeSource="{RelativeSource Mode=Self}" />
</MultiBinding>
</RadioButton.IsChecked>
<TextBlock Text="{Binding}" />
</RadioButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
The viewmodel:
class Viewmodel : INPC
{
public Viewmodel()
{
Choices = new List<string>() { "one", "two", "three" };
SelectedChoice = Choices[0];
}
public List<string> Choices { get; set; }
string selectedChoice;
public string SelectedChoice
{
get { return selectedChoice; }
set
{
if (selectedChoice != value)
{
selectedChoice = value;
OnPropertyChanged("SelectedChoice");
}
}
}
}
The converter:
public class MyConverter : IMultiValueConverter
{
object selectedValue;
object myValue;
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
selectedValue = values[0];
myValue = values[1];
return selectedValue == myValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
if ((bool)value)
{
return new object[] { myValue, Binding.DoNothing };
}
else
{
return new object[] { Binding.DoNothing, Binding.DoNothing };
}
}
}
Related videos on Youtube
Merlyn Morgan-Graham
Game Development / Game Engines • C • GLSL • C++ • C# Working at Shiny Shoe games as a developer (console ports of Monster Train. Xbox One/Win Store ports of Grim Fandango, Full Throttle, Day of the Tentacle) Past: Producer/dev manager/dev on Receiver 2 and Overgrowth at Wolfire Games Product manager and technical lead at a 120-headcount web startup Dev manager, web dev, test automator (at Launch Consulting and Microsoft)
Updated on March 28, 2020Comments
-
Merlyn Morgan-Graham over 4 years
I've bound enums to radio buttons before, and I generally understand how it works. I used the alternate implementation from this question: How to bind RadioButtons to an enum?
Instead of enumerations, I'd like to generate a runtime-enumerated set of a custom type and present those as a set of radio buttons. I have gotten a view working against a runtime-enumerated set with a
ListView
, binding to theItemsSource
andSelectedItem
properties, so myViewModel
is hooked up correctly. Now I am trying to switch from aListView
to aItemsControl
with radio buttons.Here's as far as I've gotten:
<Window.Resources> <vm:InstanceToBooleanConverter x:Key="InstanceToBooleanConverter" /> </Window.Resources> <!-- ... --> <ItemsControl ItemsSource="{Binding ItemSelections}"> <ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type vm:ISomeType}"> <RadioButton Content="{Binding Name}" IsChecked="{Binding Path=SelectedItem, Converter={StaticResource InstanceToBooleanConverter}, ConverterParameter={Binding}}" Grid.Column="0" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
InstanceToBooleanConverter
has the same implementation asEnumToBooleanConverter
from that other question. This seems right, since it seems like it just invokes theEquals
method:public class InstanceToBooleanConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return value.Equals(parameter); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return value.Equals(true) ? parameter : Binding.DoNothing; } }
The problem I am getting now is that I can't figure out how to send a runtime value as the
ConverterParameter
. When I try (with the code above), I get this error:A 'Binding' cannot be set on the 'ConverterParameter' property of type 'Binding'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
Is there a way to bind to the item instance, and pass it to the
IValueConverter
? -
Merlyn Morgan-Graham about 13 yearsActually been playing with that since I posted the question.
MultiValueConverter
seems to convert from one value to two, and vice-versa. If I convert from{ theSelectedInstance, thisInstance }
to a boolean, that's easy. The tricky part is how do I convert from a boolean to the instance. When I setIsChecked
to true or manually check it, how do I get the instance to setSelectedItem
with? I think I'd still end up having to bind to the converter parameter for that to work... -
Rick Sladkey about 13 yearsThis approach is doomed for two-way binding with a dynamic enumeration because a converter parameter cannot use databinding. You'll have to switch to a
SelectedIndex
approach and then the radio buttons can use integers. -
Merlyn Morgan-Graham about 13 yearsThis isn't a bad option. I opened another question before I fully understood this. If I get an answer to that question, it will allow you to essentially combine the converter and
EnumValue
classes, and will avoid doing manual reflections in your code - the binding would look up and set the appropriate properties. -
Merlyn Morgan-Graham about 13 yearsThe other problem is that I have two fields on my View Model:
ItemSelections
andSelectedItem
. I'm not actually using an enum, soEnum.GetValues
won't work. I know I could parse the parameter and pull two properties names out of it, but that feels kludgy. -
Merlyn Morgan-Graham about 13 yearsThe reason I originally went with
ItemsControl
is because I thought it would be more bug-prone to try to reuseListBox
, and I only knew theIValueConverter
approach to converting radio button items to a selection. It turns out it is much easier (and seems much cleaner) to adapt aListBox
. Please let me know if there are issues or potential bugs with this approach. -
Qwertie about 11 yearsBut how do you get rid of the white background that a
ListBox
normally has? -
Qwertie about 11 yearsI've got it! Just add these attributes to the
<ListBox>
:BorderBrush="Transparent" Background="Transparent"