ItemsControl 上的 WPF MVVM 单选按钮
Posted
技术标签:
【中文标题】ItemsControl 上的 WPF MVVM 单选按钮【英文标题】:WPF MVVM Radio buttons on ItemsControl 【发布时间】:2011-08-19 00:20:20 【问题描述】:我之前已经将枚举绑定到单选按钮,并且我大致了解它是如何工作的。我使用了这个问题的替代实现:How to bind RadioButtons to an enum?
我想生成一个自定义类型的运行时枚举集,而不是枚举,并将它们呈现为一组单选按钮。我已经获得了一个针对具有ListView
的运行时枚举集的视图,绑定到ItemsSource
和SelectedItem
属性,因此我的ViewModel
连接正确。现在我正在尝试使用单选按钮从ListView
切换到ItemsControl
。
这是我所得到的:
<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
与其他问题中的EnumToBooleanConverter
具有相同的实现。这似乎是对的,因为它似乎只是调用了Equals
方法:
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;
我现在遇到的问题是我不知道如何将运行时值作为ConverterParameter
发送。当我尝试(使用上面的代码)时,出现此错误:
不能在“Binding”类型的“ConverterParameter”属性上设置“Binding”。 “绑定”只能在 DependencyObject 的 DependencyProperty 上设置。
有没有办法绑定到项目实例,并将其传递给IValueConverter
?
【问题讨论】:
【参考方案1】:事实证明,放弃使用ItemsControl
转而使用ListBox
要简单得多。
它可能更重,但这主要是因为它正在为你做繁重的工作。在RadioButton.IsChecked
和ListBoxItem.IsSelected
之间进行双向绑定非常容易。使用ListBoxItem
的适当控制模板,您可以轻松摆脱所有选择视觉效果。
<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>
【讨论】:
我最初选择ItemsControl
的原因是因为我认为尝试重用ListBox
会更容易出错,而我只知道IValueConverter
转换单选按钮项的方法到一个选择。事实证明,适应ListBox
要容易得多(而且看起来更干净)。如果这种方法存在问题或潜在错误,请告诉我。
但是您如何摆脱ListBox
通常具有的白色背景?
我明白了!只需将这些属性添加到<ListBox>
:BorderBrush="Transparent" Background="Transparent"
【参考方案2】:
据我所知,使用MultiBinding
没有好方法,尽管您最初认为会有。由于您无法绑定ConverterParameter
,因此您的ConvertBack
实现没有它需要的信息。
我所做的是创建一个单独的EnumModel
类,仅用于将枚举绑定到单选按钮。在ItemsSource
属性上使用转换器,然后您将绑定到EnumModel
。 EnumModel
只是一个转发器对象,使绑定成为可能。它保存枚举的一个可能值和对视图模型的引用,因此它可以将视图模型上的属性转换为布尔值。
这是一个未经测试但通用的版本:
<ItemsControl ItemsSource="Binding Converter=StaticResource theConverter ConverterParameter="SomeEnumProperty"">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton IsChecked="Binding IsChecked">
<TextBlock Text="Binding Name" />
</RadioButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
转换器:
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();
枚举模型:
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);
对于我知道有效的代码示例(但它仍然很粗糙 - WIP!),您可以查看http://code.google.com/p/pdx/source/browse/trunk/PDX/PDX/Toolkit/EnumControl.xaml.cs。这仅适用于我的库的上下文,但它演示了基于 DescriptionAttribute
设置 EnumModel 的名称,这可能对您有用。
【讨论】:
这不是一个糟糕的选择。在我完全理解这一点之前,我打开了另一个问题。如果我得到这个问题的答案,它将允许您从本质上组合转换器和EnumValue
类,并且将避免在您的代码中进行手动反射 - 绑定将查找并设置适当的属性。
另一个问题是我的视图模型上有两个字段:ItemSelections
和SelectedItem
。我实际上并没有使用枚举,所以Enum.GetValues
不起作用。我知道我可以解析参数并从中提取两个属性名称,但这感觉很笨拙。【参考方案3】:
你离得很近。当您需要一个转换器的两个绑定时,您需要一个MultiBinding
和一个IMultiValueConverter
!语法有点冗长,但并不难。
编辑:
这里有一个小代码可以帮助您入门。
绑定:
<RadioButton Content="Binding Name"
Grid.Column="0">
<RadioButton.IsChecked>
<MultiBinding Converter="StaticResource EqualsConverter">
<Binding Path="SelectedItem"/>
<Binding Path="Name"/>
</MultiBinding>
</RadioButton.IsChecked>
</RadioButton>
和转换器:
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();
第二次编辑:
上述方法对于使用问题中链接的技术实现双向绑定没有用处,因为在转换回来时没有必要的信息。
我认为正确的解决方案是直接 MVVM:对视图模型进行编码以匹配视图的需求。代码量非常小,无需任何转换器或有趣的绑定或技巧。
这里是 XAML;
<Grid>
<ItemsControl ItemsSource="Binding">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton
GroupName="Value"
Content="Binding Description"
IsChecked="Binding IsChecked, Mode=TwoWay"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
和代码隐藏来模拟视图模型:
DataContext = new CheckBoxValueCollection(new[] "Foo", "Bar", "Baz" );
以及一些视图模型基础设施:
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;
【讨论】:
自从我发布问题以来,实际上一直在玩这个。MultiValueConverter
似乎从一个值转换为两个值,反之亦然。如果我从 theSelectedInstance, thisInstance
转换为布尔值,那很容易。棘手的部分是如何将布尔值转换为实例。当我将IsChecked
设置为true 或手动检查它时,如何让实例设置SelectedItem
?我想我最终还是不得不绑定到转换器参数才能工作......
这种方法注定要使用动态枚举进行双向绑定,因为转换器参数不能使用数据绑定。您必须切换到SelectedIndex
方法,然后单选按钮才能使用整数。【参考方案4】:
现在我知道了 x:Shared(感谢您的 other question),我放弃了我之前的回答,并说 MultiBinding
毕竟是要走的路。
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>
视图模型:
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");
转换器:
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 ;
【讨论】:
以上是关于ItemsControl 上的 WPF MVVM 单选按钮的主要内容,如果未能解决你的问题,请参考以下文章