ItemsControl 上的 WPF MVVM 单选按钮

发布于 2024-11-05 04:17:42 字数 2025 浏览 0 评论 0原文

我之前已经将枚举绑定到单选按钮,并且我通常了解它是如何工作的。我使用了这个问题的替代实现:如何绑定RadioButtons 到枚举?

我想生成自定义类型的运行时枚举集,并将其呈现为一组单选按钮,而不是枚举。我已经获得了一个针对带有 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

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 the ItemsSource and SelectedItem properties, so my ViewModel is hooked up correctly. Now I am trying to switch from a ListView to a ItemsControl 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 as EnumToBooleanConverter from that other question. This seems right, since it seems like it just invokes the Equals 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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

何时共饮酒 2024-11-12 04:17:42

事实证明,放弃使用 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>

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>
北恋 2024-11-12 04:17:42

据我所知,没有什么好的方法可以使用 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();
    }
}

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

对于我知道有效的代码示例(但它仍然很未经修饰 - WIP!),您可以看到 http://code.google.com/p/pdx/source/browse/trunk/PDX/ PDX/Toolkit/EnumControl.xaml.cs。这仅在我的库的上下文中有效,但它演示了基于 DescriptionAttribute 设置 EnumModel 的名称,这可能对您有用。

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.

空‖城人不在 2024-11-12 04:17:42

现在我知道了 x:Shared (感谢您的其他问题 ),我放弃之前的答案,并说 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 };
        }

    }
}

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

    }
}
痴意少年 2024-11-12 04:17:42

你是如此接近。当一个转换器需要两个绑定时,您需要一个 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; }
        }
    }

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; }
        }
    }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文