C# 动态绑定到枚举

C# 动态绑定到枚举,c#,wpf,mvvm,data-binding,C#,Wpf,Mvvm,Data Binding,我想在我的ViewModel上有一个枚举,比如说代表一个人的性别。表示ViewModel的视图应该能够提供提供该值的方法;无论是一组单选按钮还是一个组合框(如果有很多)。还有很多例子,你可以在XAML中硬编码单选按钮,每一个按钮都表示它代表的值。更好的方法还将使用Display属性的名称为单选按钮提供文本 我希望能更进一步。我希望它能够根据枚举的值以及DisplayAttribute的名称和描述等动态生成单选按钮。理想情况下,我希望它选择创建一个组合框(而不是单选按钮),如果它有6个以上的项(可

我想在我的ViewModel上有一个枚举,比如说代表一个人的性别。表示ViewModel的视图应该能够提供提供该值的方法;无论是一组单选按钮还是一个组合框(如果有很多)。还有很多例子,你可以在XAML中硬编码单选按钮,每一个按钮都表示它代表的值。更好的方法还将使用Display属性的名称为单选按钮提供文本

我希望能更进一步。我希望它能够根据枚举的值以及DisplayAttribute的名称和描述等动态生成单选按钮。理想情况下,我希望它选择创建一个组合框(而不是单选按钮),如果它有6个以上的项(可能实现为某种控件);但在尝试跑步之前,让我们先看看我们是否能走路

我的谷歌搜索让我非常接近。。。以下是我得到的:

public enum Gender
{
    [Display(Name="Gentleman", Description = "Slugs and snails and puppy-dogs' tails")]
    Male,

    [Display(Name = "Lady", Description = "Sugar and spice and all things nice")]
    Female
}
窗口:

<Window x:Class="WpfApplication2.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApplication2"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <local:EnumMultiConverter x:Key="EnumMultiConverter"/>

    <ObjectDataProvider
        MethodName="GetValues"
        ObjectType="{x:Type local:EnumDescriptionProvider}"
        x:Key="AdvancedGenderTypeEnum">

        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="local:Gender"/>
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
</Window.Resources>
<StackPanel>
    <ItemsControl ItemsSource="{Binding Source={StaticResource AdvancedGenderTypeEnum}}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <RadioButton GroupName="{Binding GroupName}" Content="{Binding Name}" ToolTip="{Binding Description}">
                    <RadioButton.IsChecked>
                        <MultiBinding Converter="{StaticResource EnumMultiConverter}" Mode="TwoWay">
                            <Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="DataContext.Gender" Mode="TwoWay" />
                            <Binding Path="Value" Mode="OneWay"/>
                        </MultiBinding>
                    </RadioButton.IsChecked>
                </RadioButton>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</StackPanel>
</Window>
以及多转换器(因为IValueConverter不能为ConverterParameter进行绑定):


所以我唯一的问题是,我不能回去。但也许有人有一个绝妙的解决办法。正如我所说,理想情况下,我只需要一些神奇的控件,可以绑定到ViewModel上的枚举,并为该枚举的每个值动态创建单选按钮。但是我会采纳我能得到的任何建议。

我建议您使用自定义行为,这将使您能够将所有Enum-to-ViewModel逻辑放入一段可重用的代码中。这样,您就不必与复杂的值转换器争吵

有一篇很棒的文章和GitHub示例演示了这个问题的解决方案,请参见下面的链接


我希望这能让你找到你想要的东西,你就快到了,关键是要意识到,当用户点击RadioButton的
命令时,它的
事件总是会被触发,即使
属性已被选中。您只需将
设置为选中
多值绑定
单向
,并添加一个在用户选中单选按钮时调用的命令处理程序,例如:

<DataTemplate>
    <RadioButton Content="{Binding Name}" ToolTip="{Binding Description}"
                 Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}, Path=DataContext.CheckedCommand}"
                 CommandParameter="{Binding}">
        <RadioButton.IsChecked>
            <MultiBinding Converter="{StaticResource EnumMultiConverter}" Mode="OneWay">
                <Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="DataContext.Gender" />
                <Binding Path="Value" />
            </MultiBinding>
        </RadioButton.IsChecked>
    </RadioButton>
</DataTemplate>

然后回到视图模型中,您为命令提供了一个处理程序,该处理程序手动设置性别值,而不是依靠单选按钮将值传播回自己:

    public ICommand CheckedCommand { get { return new RelayCommand<Gender>(value => this.Gender = value); } }
public ICommand checked命令{get{返回新的RelayCommand(value=>this.Gender=value);}

请注意,您甚至不需要组名,它都是根据您在视图模型中绑定到的属性和命令自动处理的(这对于测试目的来说更好)。

我最终找到了这篇文章: 如果你仔细看答案,他提出了一个解决方案,并给出了一个链接(现在已断开),然后因为给出了一个可能断开的链接而受到惩罚:)我联系了他,他立即给我发送了信息。例如,它允许我在XAML中使用以下内容:

    <local:EnumRadioButton
        SelectedItem="{Binding Path=Gender, Mode=TwoWay}"
        EnumType="{x:Type local:Gender}"
        RadioButtonStyle="{StaticResource MyStyle}"/>
这里是神奇的一点:

public class EnumRadioButton : ItemsControl
{
    public static readonly DependencyProperty EnumTypeProperty =
        DependencyProperty.Register(nameof(EnumType), typeof(Type), typeof(EnumRadioButton), new PropertyMetadata(null, EnumTypeChanged));

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register(nameof(SelectedItem), typeof(object), typeof(EnumRadioButton));

    public static readonly DependencyProperty RadioButtonStyleProperty =
        DependencyProperty.Register(nameof(RadioButtonStyle), typeof(Style), typeof(EnumRadioButton));


    public Type EnumType
    {
        get { return (Type)GetValue(EnumTypeProperty); }
        set { SetValue(EnumTypeProperty, value); }
    }

    public object SelectedItem
    {
        get { return GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public Style RadioButtonStyle
    {
        get { return (Style)GetValue(RadioButtonStyleProperty); }
        set { SetValue(RadioButtonStyleProperty, value); }
    }

    private static void EnumTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        EnumRadioButton enumRadioButton = (EnumRadioButton)d;
        enumRadioButton.UpdateItems(e.NewValue as Type);
    }

    private void UpdateItems(Type newValue)
    {
        Items.Clear();
        if (!newValue.IsEnum)
        {
            throw new ArgumentOutOfRangeException(nameof(newValue), $"Only enum types are supported in {GetType().Name} control");
        }

        var enumerationItems = EnumerationItemProvider.GetValues(newValue);
        foreach (var enumerationItem in enumerationItems)
        {
            var radioButton = new RadioButton { Content = enumerationItem.Name, ToolTip = enumerationItem.Description };
            SetCheckedBinding(enumerationItem, radioButton);
            SetStyleBinding(radioButton);
            Items.Add(radioButton);
        }
    }

    private void SetStyleBinding(RadioButton radioButton)
    {
        var binding = new Binding
        {
            Source = this,
            Mode = BindingMode.OneWay,
            Path = new PropertyPath(nameof(RadioButtonStyle))
        };
        radioButton.SetBinding(StyleProperty, binding);
    }

    private void SetCheckedBinding(EnumerationItem enumerationItem, RadioButton radioButton)
    {
        var binding = new Binding
        {
            Source = this,
            Mode = BindingMode.TwoWay,
            Path = new PropertyPath(nameof(SelectedItem)),
            Converter = new EnumToBooleanConverter(), // would be more efficient as a singleton
            ConverterParameter = enumerationItem.Value
        };
        radioButton.SetBinding(ToggleButton.IsCheckedProperty, binding);
    }
}

我发布另一个答案已经有几年了,所以我想我应该发布我采用这种方法的经验的好处,以及一个更新、更好的解决方案

我想用一个控件来表示
RadioButton
s的集合,这个想法绝对正确(例如,您可以轻松地在拥有一组单选按钮或一个组合框之间来回切换。但是,在我的另一个回答中,将项的生成塞进该控件是一个错误。允许控件用户将他们喜欢的任何内容绑定到您的控件中要比WPF-y多得多。(当我想要修补在特定时间显示的值时,这也导致了线程问题。)

这个新的解决方案看起来更干净,尽管它(必要时)由相当多的部分组成;但它确实实现了使用单个控件来表示单选按钮集合的目标。例如,您将能够:

<local:EnumRadioButtons SelectedValue="{Binding Gender, Mode=TwoWay}" ItemsSource="{Binding Genders}"/>

我们需要设置默认的样式,但是稍后我会回到这里。让我们看看个人<代码> EnumRadioButton <代码>控件。这里最大的问题是在我原来的问题中提出的一个问题……转换器不能通过<代码>绑定<代码> > <代码> >转换参数< /代码>。这意味着我不能LEA。我需要知道项集合的类型,所以我定义了这个接口来表示每个项

public interface IEnumerationItem
{
    string Name { get; set; }

    object Value { get; set; }

    string Description { get; set; }

    bool IsEnabled { get; set; }
}
下面是一个示例实现

using System.Diagnostics;

// I'm making the assumption that although the values can be set at any time, they will not be changed after these items are bound,
// so there is no need for this class to implement INotifyPropertyChanged.
[DebuggerDisplay("Name={Name}")]
public class EnumerationItem : IEnumerationItem
{
    public object Value { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public bool IsEnabled { get; set; }
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

internal class EnumerationItemProvider : IEnumerationItemProvider
{
    public IList<IEnumerationItem> GetValues(Type enumType)
    {
        var result = new List<IEnumerationItem>();

        foreach (var value in Enum.GetValues(enumType))
        {
            var item = new EnumerationItem { Value = value };

            FieldInfo fieldInfo = enumType.GetField(value.ToString());

            var obsoleteAttribute = (ObsoleteAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(ObsoleteAttribute));
            item.IsEnabled = obsoleteAttribute == null;

            var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayAttribute));
            item.Name = displayAttribute?.Name ?? value.ToString();
            item.Description = displayAttribute?.Description ?? value.ToString();

            result.Add(item);
        }

        return result;
    }
}
显然,有一些东西可以帮助您创建这些东西是很有用的,所以这里是界面

using System;
using System.Collections.Generic;

public interface IEnumerationItemProvider
{
    IList<IEnumerationItem> GetValues(Type enumType);
}
正如您在上面看到的,我们仍然需要一个转换器,您可能已经有了这样一个转换器;但是为了完整性,这里是

using System;
using System.Globalization;
using System.Windows.Data;

public class EnumToBooleanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value?.Equals(parameter);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value.Equals(true) ? parameter : Binding.DoNothing;
    }
}
剩下的唯一一件事就是为这些控件设置默认样式。(请注意,如果您已经为
RadioButton
ItemsControl
定义了默认样式,那么您需要添加
BasedOn
子句。)



希望这能有所帮助。

当字典可以同样很好地完成任务时,为什么要使用枚举?当我们有编译时常量时,就使用枚举。在您的情况下,
字典
也可以。删除上面的问题我误读了问题,为什么要转换回?您已经有了索引-只需将所选索引绑定到查看模型,您可以将索引(int)强制转换为枚举。@code4life这是一个双向绑定,因此我需要转换器双向工作。是的,我可以向我的Vie添加其他内容
using System.Windows;
using System.Windows.Controls;

public class EnumRadioButtons : ItemsControl
{
    public static readonly DependencyProperty SelectedValueProperty =
        DependencyProperty.Register(nameof(SelectedValue), typeof(object), typeof(EnumRadioButtons));

    public object SelectedValue
    {
        get { return GetValue(SelectedValueProperty); }
        set { SetValue(SelectedValueProperty, value); }
    }
}
public interface IEnumerationItem
{
    string Name { get; set; }

    object Value { get; set; }

    string Description { get; set; }

    bool IsEnabled { get; set; }
}
using System.Diagnostics;

// I'm making the assumption that although the values can be set at any time, they will not be changed after these items are bound,
// so there is no need for this class to implement INotifyPropertyChanged.
[DebuggerDisplay("Name={Name}")]
public class EnumerationItem : IEnumerationItem
{
    public object Value { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public bool IsEnabled { get; set; }
}
using System;
using System.Collections.Generic;

public interface IEnumerationItemProvider
{
    IList<IEnumerationItem> GetValues(Type enumType);
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

internal class EnumerationItemProvider : IEnumerationItemProvider
{
    public IList<IEnumerationItem> GetValues(Type enumType)
    {
        var result = new List<IEnumerationItem>();

        foreach (var value in Enum.GetValues(enumType))
        {
            var item = new EnumerationItem { Value = value };

            FieldInfo fieldInfo = enumType.GetField(value.ToString());

            var obsoleteAttribute = (ObsoleteAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(ObsoleteAttribute));
            item.IsEnabled = obsoleteAttribute == null;

            var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayAttribute));
            item.Name = displayAttribute?.Name ?? value.ToString();
            item.Description = displayAttribute?.Description ?? value.ToString();

            result.Add(item);
        }

        return result;
    }
}
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

public class EnumRadioButton : RadioButton
{
    private static readonly Lazy<IValueConverter> ConverterFactory = new Lazy<IValueConverter>(() => new EnumToBooleanConverter());

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property == DataContextProperty)
        {
            SetupBindings();
        }
    }

    /// <summary>
    /// This entire method would not be necessary if I could have used a Binding for "ConverterParameter" - I could have done it all in XAML.
    /// </summary>
    private void SetupBindings()
    {
        var enumerationItem = DataContext as IEnumerationItem;
        if (enumerationItem != null)
        {
            // I'm making the assumption that the properties of an IEnumerationItem won't change after this point
            Content = enumerationItem.Name;
            IsEnabled = enumerationItem.IsEnabled;
            ToolTip = enumerationItem.Description;
            //// Note to self, I used to expose GroupName on IEnumerationItem, so that I could set that property here; but there is actually no need...
            //// You can have two EnumRadioButtons controls next to each other, bound to the same collection of values, each with SelectedItem bound
            //// to different properties, and they work independently without setting GroupName.

            var binding = new Binding
            {
                Mode = BindingMode.TwoWay,
                RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(EnumRadioButtons), 1),
                Path = new PropertyPath(nameof(EnumRadioButtons.SelectedValue)),
                Converter = ConverterFactory.Value, // because we can reuse the same instance for everything rather than having one for each individual value
                ConverterParameter = enumerationItem.Value,
            };

            SetBinding(IsCheckedProperty, binding);
        }
    }
}
using System;
using System.Globalization;
using System.Windows.Data;

public class EnumToBooleanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value?.Equals(parameter);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value.Equals(true) ? parameter : Binding.DoNothing;
    }
}
        <DataTemplate x:Key="EnumRadioButtonItem" DataType="{x:Type local:EnumerationItem}">
            <local:EnumRadioButton/>
        </DataTemplate>

        <Style TargetType="local:EnumRadioButton">
            <!-- Put your preferred stylings in here -->
        </Style>

        <Style TargetType="local:EnumRadioButtons">
            <Setter Property="IsTabStop" Value="False"/>
            <Setter Property="ItemTemplate" Value="{StaticResource EnumRadioButtonItem}"/>
            <!-- Put your preferred stylings in here -->
        </Style>