【问题标题】:Dynamically binding to an Enum动态绑定到枚举
【发布时间】:2016-03-09 11:46:58
【问题描述】:

我想在我的 ViewModel 上有一个枚举,假设它代表一个人的性别。代表该 ViewModel 的 View 应该能够提供一种提供该值的方式;无论是一组单选按钮还是一个组合框(如果有很多)。并且有很多示例,您可以在 XAML 中对单选按钮进行硬编码,每个按钮都说明它代表的值。更好的也会使用显示属性的名称来为单选按钮提供文本。

我希望更进一步。我希望它根据 Enum 的值以及 DisplayAttribute 的名称和描述等动态生成 RadioButtons。理想情况下,如果它超过 6 个项目(可能作为某种控件实现),我希望它选择创建一个 ComboBox(而不是 RadioButtons);但是在我们尝试跑步之前,让我们看看我们是否可以走路。 :)

我的谷歌搜索让我非常接近......这就是我所拥有的:

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>

EnumDescriptionProvider:

public static class EnumDescriptionProvider
{
    public static IList<EnumerationItem> GetValues(Type enumType)
    {
        string typeName = enumType.Name;
        var typeList = new List<EnumerationItem>();

        foreach (var value in Enum.GetValues(enumType))
        {
            FieldInfo fieldInfo = enumType.GetField(value.ToString());
            var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayAttribute));

            if (displayAttribute == null)
            {
                typeList.Add(new EnumerationItem
                {
                    GroupName = typeName,
                    Value = value,
                    Name = value.ToString(),
                    Description = value.ToString()
                });
            }
            else
            {
                typeList.Add(new EnumerationItem
                {
                    GroupName = typeName,
                    Value = value,
                    Name = displayAttribute.Name,
                    Description = displayAttribute.Description
                });
            }
        }

        return typeList;
    }
}

枚举项:

public class EnumerationItem
{
    public object GroupName { get; set; }
    public object Value { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

还有MultiConverter(因为IValueConverter不能为ConverterParameter做一个Binding):

public class EnumMultiConverter : 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 NotSupportedException();
    }
}

所以我遇到的唯一问题是我无法执行 ConvertBack。但也许有人有一个绝妙的解决方案。正如我所说,理想情况下,我只想要一些可以绑定到 ViewModel 上的枚举的神奇控件,并让它为该枚举的每个值动态创建 RadioButtons。但我会接受我能得到的任何建议。

【问题讨论】:

  • 当字典同样能很好地完成任务时,为什么要使用枚举,当我们有编译时间常数时使用枚举。在你的情况下Dictionary&lt;string,DisplayAttribute&gt; 也会这样做
  • 打上面我看错问题了
  • 为什么要转换回来?您已经有了索引 - 只需将选定的索引绑定到视图模型,您就可以将索引(一个 int)转换为枚举。
  • @code4life 这是双向绑定,所以我需要转换器双向工作。是的,我可以在我的 ViewModel 中添加其他内容,但由于我实现视图的方式,我不应该更改我的视图模型。另外,如果我有 5 个枚举,这意味着我需要添加 5 个额外的属性。公认的解决方案表明 XAML 应该 是多么简单,并且不需要更改视图模型。

标签: c# wpf mvvm data-binding


【解决方案1】:

我建议您使用自定义行为,这将使您能够将所有 Enum 到 ViewModel 逻辑放入单个可重用的代码中。这样你就不必纠结复杂的 ValueConverters

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

WPF – Enum ItemsSource With Custom Behavior - Article

GitHub repository for sample code

我希望这能给你你想要的东西

【讨论】:

  • 感谢您的解决方案;但是,它只是为每个值创建了一个包含一个项目的 ListBox。我正在寻找能够为每个值提供一个可用的 RadioButton 的东西。例如,如果我从上面发布的内容中删除“ItemsControl.ItemTemplate”标签及其内容,我将拥有与您的解决方案提供的类似的内容(如果我使用相同的“作弊”在我的枚举项)。
  • 你说你真的想要一个组合框。只需将列表框更改为组合框,它的工作原理完全相同
  • 抱歉造成误解,让我澄清一下……这是一个长期存在的用户界面设计原则,如果只有少数项目,您将使用 RadioButtons(我认为微软有 6 个作为限制) );但如果还有更多,您应该使用 ComboBox。我想要一些能给我 RadioButtons 或 ComboBox 的东西,具体取决于有多少项目。
  • 如果做不到这一点,我肯定想要一些东西来动态创建 RadioButtons;因为如果我知道会有很多值,我可以使用我的 EnumDescriptionProvider 作为 ComboBox 的 ItemsSource。我处于枚举值可能会发生变化的情况下,我不想仅仅因为 Enum 中的项目数发生变化就必须重新设计屏幕。
【解决方案2】:

你快到了,关键是意识到 RadioButton 的Command 事件总是在用户点击它时被触发,即使IsChecked 属性被绑定。您需要做的就是使您的IsChecked 多值绑定OneWay 并添加一个命令处理程序,该处理程序在用户选中单选按钮时被调用,例如:

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

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

    public ICommand CheckedCommand { get { return new RelayCommand<Gender>(value => this.Gender = value); } }

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

【讨论】:

  • 谢谢。很好的解决方案。对于这种方法,关于 GroupName 的好提示。 (顺便说一句,CommandParameter 需要是“{Binding Value}”,因为 Binding 是 EnumerationItem)。迄今为止最好的答案。我只是觉得我的视图应该能够双向绑定到视图模型上的枚举。 (即,我不需要更改视图模型以支持视图的实现方式;并且必须为每个枚举添加一个命令)。这就是为什么我认为我可能需要编写自己的控件(我不知道该怎么做)。
  • 我希望您可以使用附加的行为来做同样的事情,然后您可以使用全局样式自动将其应用于所有单选按钮。下周我回来工作时,甚至可以自己尝试一下。
【解决方案3】:

我最终找到了这个帖子:How to bind RadioButtons to an enum? 如果您对artiom 的答案有很长的路要走,他会提出一个解决方案,并提供一个链接(现在已损坏),然后因提供可能损坏的链接而受到惩罚:) 我联系了他,他立即给我发了信息。例如,它允许我在 XAML 中使用它:

    <local:EnumRadioButton
        SelectedItem="{Binding Path=Gender, Mode=TwoWay}"
        EnumType="{x:Type local:Gender}"
        RadioButtonStyle="{StaticResource MyStyle}"/>

因此,您需要的不是我在原帖中提到的 MultiConverter:

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

这是神奇的一点:

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

【讨论】:

    【解决方案4】:

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

    我绝对有正确的想法,希望有一个控件来表示 RadioButtons 的集合(例如,这样您就可以在一组单选按钮或 @987654322 之间轻松地来回切换@。但是,在我的另一个答案中,将项目的生成塞进该控件是一个错误。允许控件的用户将他们喜欢的任何内容绑定到您的控件中,这更像是 WPF-y。(它也导致线程当我想修改在特定时间显示哪些值时出现问题。)

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

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

    ViewModel 在哪里...

        public ObservableCollection<IEnumerationItem> Genders { get; }
    
        public Gender? Gender
        {
            get => _gender;
            set => SetProperty(ref _gender, value); // common implementation of INotifyPropertyChanged, as seen on ViewModels.
        }
    

    所以安顿下来,我会引导你完成它......如果我教你吸鸡蛋,我会道歉。

    控件本身基本上是ItemsControl 的扩展,这使其能够包含其他控件的集合。它允许您以与使用 ItemsControl 相同的方式(通过 ItemsPanel)控制单个项目的整体布局(例如,如果您希望它们横向而不是垂直)。

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

    我们需要设置它的默认样式;但我稍后再谈。我们来看看单独的EnumRadioButton 控件。这里最大的问题与我最初的问题中提出的问题相同......转换器不能通过Binding 获取ConverterParameter。这意味着我不能把它留给调用者,所以我需要知道项目集合是什么类型。所以我定义了这个接口来代表每个项目...

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

    我们的想法是,这将为您提供起点,您可以修改项目及其属性(如果需要)将它们放入 ObservableCollection 并绑定它之前到EnumRadioButtons.ItemsSource。在那之后,您可以在集合中添加/删除项目;但是不会反映更改属性(因为我还没有让它实现INotifyPropertyChanged,因为我不希望在那之后需要更改它们)。我认为这是合理的;但如果您不同意,您可以更改实现。

    所以,回到个人EnumRadioButton。基本上它只是一个RadioButton,它会在设置DataContext 时设置Binding。正如我之前提到的,我们必须这样做,因为ConverterParameter 不能是Binding,而MultiConverter 将不能ConvertBack 成为其中之一它的来源。

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

    剩下的唯一事情就是为这些控件设置默认样式。 (请注意,如果您已经为RadioButtonItemsControl 定义了默认样式,那么您将需要添加BasedOn 子句。)

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

    希望这会有所帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-21
      • 2015-10-17
      • 2011-07-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多