动态绑定到枚举

时间:2016-03-09 11:46:59

标签: c# wpf mvvm data-binding

我希望在我的ViewModel上有一个Enum,让我们说代表一个人的性别。表示ViewModel的View应该能够提供一种提供该值的方法;这是一组无线电按钮还是一个组合框(如果有很多)。并且有很多例子可以在XAML中硬编码单选按钮,每个单元都说明它代表的值。而较好的也将使用显示属性的名称来提供单选按钮的文本。

我希望更进一步。我希望根据Enum的值以及DisplayAttribute的名称和描述等动态生成 RadioButtons。理想情况下,我喜欢选择创建一个ComboBox(而不是RadioButtons),如果它超过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>

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

EnumerationItem:

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进行绑定):

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

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

4 个答案:

答案 0 :(得分:1)

我建议您使用自定义行为,这样您就可以将所有Enum to ViewModel逻辑放入一个可重复使用的代码段中。这样你就不必纠缠复杂的ValueConverters

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

WPF – Enum ItemsSource With Custom Behavior - Article

GitHub repository for sample code

我希望能为您提供所需的内容

答案 1 :(得分:0)

你几乎就在那里,关键是意识到当用户点击它时,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,它们都会根据您在视图模型中绑定的属性和命令自动处理(无论如何都更适合测试目的)。

答案 2 :(得分:0)

我最终发现了这篇文章: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);
    }
}

答案 3 :(得分:0)

距离我发布其他答案已经有好几年了,所以我认为我应该把我从采用这种方法的经验中受益,以及一个更新更好的解决方案。

我绝对有一个正确的想法,就是希望有一个控件来表示RadioButton的集合(例如,以便您可以在具有一组单选按钮或一个单选按钮之间来回切换。 ComboBox。但是,在我的另一个答案中,将项目的生成填充到该控件中是一个错误。允许控件的用户将所需的任何内容绑定到控件中,要用的是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控件。这里最大的问题是我最初提出的问题……转换器无法通过ConverterParameter来获取Binding。这意味着我不能将其留给调用者,因此我需要知道项目集合的类型。所以我已经定义了此接口来表示每个项目...

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时设置绑定。正如我之前提到的,我们必须以这种方式进行操作,因为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);
        }
    }
}

正如您在上面看到的,我们仍然需要一个Converter,并且您可能已经有一个类似的转换器了;但是为了完整性,这里是...

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>

希望这会有所帮助。