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 的运行时枚举集的视图,绑定到ItemsSourceSelectedItem 属性,因此我的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.IsCheckedListBoxItem.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 通常具有的白色背景? 我明白了!只需将这些属性添加到&lt;ListBox&gt;BorderBrush="Transparent" Background="Transparent"【参考方案2】:

据我所知,使用MultiBinding 没有好方法,尽管您最初认为会有。由于您无法绑定ConverterParameter,因此您的ConvertBack 实现没有它需要的信息。

我所做的是创建一个单独的EnumModel 类,仅用于将枚举绑定到单选按钮。在ItemsSource 属性上使用转换器,然后您将绑定到EnumModelEnumModel 只是一个转发器对象,使绑定成为可能。它保存枚举的一个可能值和对视图模型的引用,因此它可以将视图模型上的属性转换为布尔值。

这是一个未经测试但通用的版本:

<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 类,并且将避免在您的代码中进行手动反射 - 绑定将查找并设置适当的属性。 另一个问题是我的视图模型上有两个字段:ItemSelectionsSelectedItem。我实际上并没有使用枚举,所以Enum.GetValues 不起作用。我知道我可以解析参数并从中提取两个属性名称,但这感觉很笨拙。【参考方案3】:

你离得很近。当您需要一个转换器的两个绑定时,您需要一个MultiBinding 和一个IMultiValueConverter!语法有点冗长,但并不难。

MultiBinding Class IMultiValueConverter Interface

编辑:

这里有一个小代码可以帮助您入门。

绑定:

<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 单选按钮的主要内容,如果未能解决你的问题,请参考以下文章

C#:WPF MVVM 中的按钮绑定

我去年码了个表(WPF MvvM)

WPF里ItemsControl的分组实现

WPF下Itemscontrol分组 样式

是否可以在 mvvm 模式中获取 wpf 数据网格上的动态列?

WPF - ItemsControl - 如何找到ItemTemplate中的“CheckBox”项?