WPF绑定ComboBox到枚举(有一个小技巧)

14

问题在于我有一个枚举类型,但我不想让下拉框显示枚举值。这是该枚举类型:

public enum Mode
    {
        [Description("Display active only")]
        Active,
        [Description("Display selected only")]
        Selected,
        [Description("Display active and selected")]
        ActiveAndSelected
    }

所以在ComboBox中,我想要显示枚举每个值对应的DescriptionProperty,而不是显示Active、Selected或ActiveAndSelected。对于枚举,我已经有一个名为GetDescription()的扩展方法:

public static string GetDescription(this Enum enumObj)
        {
            FieldInfo fieldInfo =
                enumObj.GetType().GetField(enumObj.ToString());

            object[] attribArray = fieldInfo.GetCustomAttributes(false);

            if (attribArray.Length == 0)
            {
                return enumObj.ToString();
            }
            else
            {
                DescriptionAttribute attrib =
                    attribArray[0] as DescriptionAttribute;
                return attrib.Description;
            }
        }

那么我可以将枚举绑定到ComboBox并使用 GetDescription 扩展方法显示其内容吗?

谢谢!

6个回答

20

我建议使用DataTemplate和ValueConverter。这将允许您自定义其显示方式,但仍然可以读取组合框的SelectedItem属性并获取实际的枚举值。

ValueConverter需要大量样板代码,但这里没有太复杂的东西。首先,您需要创建ValueConverter类:

public class ModeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter,
        CultureInfo culture)
    {
        return ((Mode) value).GetDescription();
    }
    public object ConvertBack(object value, Type targetType, object parameter,
        CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

由于您只是将枚举值转换为字符串(用于显示),因此您不需要 ConvertBack -- 这仅适用于双向绑定场景。

然后,您可以将 ValueConverter 的一个实例放入资源中,类似于以下内容:

<Window ... xmlns:WpfApplication1="clr-namespace:WpfApplication1">
    <Window.Resources>
        <WpfApplication1:ModeConverter x:Key="modeConverter"/>
    </Window.Resources>
    ....
</Window>

那么您就可以为ComboBox提供一个DisplayTemplate,使用ModeConverter格式化其项:

<ComboBox Name="comboBox" ...>
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Converter={StaticResource modeConverter}}"/>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

为了测试这个,我也添加了一个 Label,它将显示实际的 SelectedItem 值,而它确实显示出 SelectedItem 是枚举而不是显示文本,而我想要的是后者:

<Label Content="{Binding ElementName=comboBox, Path=SelectedItem}"/>

兄弟,你的回答终于在我花费了几个小时在互联网上搜寻后解决了我的问题。谢谢! - mcy

6

以下是我用 MVVM 实现的方法。在我的模型中,我会定义枚举:

    public enum VelocityUnitOfMeasure
    {
        [Description("Miles per Hour")]
        MilesPerHour,
        [Description("Kilometers per Hour")]
        KilometersPerHour
    }

在我的ViewModel中,我公开了一个属性,提供可能的选择作为字符串,以及一个用于获取/设置模型值的属性。如果我们不想使用类型中的每个枚举值,则这非常有用:

    //UI Helper
    public IEnumerable<string> VelocityUnitOfMeasureSelections
    {
        get
        {
            var units = new []
                            {
                               VelocityUnitOfMeasure.MilesPerHour.Description(),
                               VelocityUnitOfMeasure.KilometersPerHour.Description()
                            };
            return units;
        }
    }

    //VM property
    public VelocityUnitOfMeasure UnitOfMeasure
    {
        get { return model.UnitOfMeasure; }
        set { model.UnitOfMeasure = value; }
    }

此外,我使用通用的EnumDescriptionConverter:
public class EnumDescriptionConverter : IValueConverter
{
    //From Binding Source
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is Enum)) throw new ArgumentException("Value is not an Enum");
        return (value as Enum).Description();
    }

    //From Binding Target
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is string)) throw new ArgumentException("Value is not a string");
        foreach(var item in Enum.GetValues(targetType))
        {
            var asString = (item as Enum).Description();
            if (asString == (string) value)
            {
                return item;
            }
        }
        throw new ArgumentException("Unable to match string to Enum description");
    }
}

最后,通过这个视图,我可以做到以下几点:

<Window.Resources>
    <ValueConverters:EnumDescriptionConverter x:Key="enumDescriptionConverter" />
</Window.Resources>
...
<ComboBox SelectedItem="{Binding UnitOfMeasure, Converter={StaticResource enumDescriptionConverter}}"
          ItemsSource="{Binding VelocityUnitOfMeasureSelections, Mode=OneWay}" />

Enum.Description()是一个扩展方法吗?我在System.Enum类型中找不到这个方法。 - mtijn
.Description() 是一个扩展方法,用于获取描述属性。回过头来看,使用 DisplayName 属性可能更合适。 - Mike Rowley
我在问题描述中忽略了扩展方法,这可能是你所指的,而且没有使用DisplayName,因为它不适用于枚举字段目标(除非你扩展属性使用)。 - mtijn
我喜欢你的方法,但我想升级你的“ConvertBack”方法为:public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return Enum.GetValues(targetType) .OfType() .Single(element => element.Description() == (string)value); } - memory of a dream

6

18
哥们儿,反射并不是那么慢,特别是与显示 GUI 所需的时间相比。我不认为这会成为问题。 - Joe White
好吧,不要只听我的话。上面提到的帖子说这是一个问题。 - Robert Harvey
3
但是并没有引用任何个人资料的结果。作者对此有所担忧,但这并不意味着实际上存在问题。 - Joe White
这是我唯一能够使其工作的代码。在初始化字典时,我使用了GetDescription扩展方法,使其变得更短了一些。谢谢! - Carlo

3

除了使用反射和属性之外,有几种方法可以实现此功能,但我认为最好的方法是创建一个小视图模型类来包装枚举值:

public class ModeViewModel : ViewModel
{
    private readonly Mode _mode;

    public ModeViewModel(Mode mode)
    {
        ...
    }

    public Mode Mode
    {
        get { ... }
    }

    public string Description
    {
        get { return _mode.GetDescription(); }
    }
}

另外,您可以考虑使用ObjectDataProvider


3

我建议您使用我已经发布的一个标记扩展,链接在这里,只需进行一些小修改:

[MarkupExtensionReturnType(typeof(IEnumerable))]
public class EnumValuesExtension : MarkupExtension
{
    public EnumValuesExtension()
    {
    }

    public EnumValuesExtension(Type enumType)
    {
        this.EnumType = enumType;
    }

    [ConstructorArgument("enumType")]
    public Type EnumType { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (this.EnumType == null)
            throw new ArgumentException("The enum type is not set");
        return Enum.GetValues(this.EnumType).Select(o => GetDescription(o));
    }
}

您可以像这样使用它:
<ComboBox ItemsSource="{local:EnumValues local:Mode}"/>

编辑:我建议的方法将绑定到字符串列表,这不是理想的,因为我们希望SelectedItem是Mode类型。最好删除.Select(...)部分,并在ItemTemplate中使用具有自定义转换器的绑定。


这样做会使组合框的SelectedItem变为“仅显示活动”,而不是Mode.Active吗?对我来说,这似乎是一个不良的副作用。 - Joe White
那么这意味着使用这种方法,我不能将选定的项目设置为枚举对象当前选择的内容? - Carlo
@Joe:是的,你说得对...那确实是个问题。我会更新我的答案。 - Thomas Levesque

0
我是这样做的:
<ComboBox x:Name="CurrencyCodeComboBox" Grid.Column="4" DisplayMemberPath="." HorizontalAlignment="Left" Height="22"   Margin="11,6.2,0,10.2" VerticalAlignment="Center" Width="81" Grid.Row="1" SelectedValue="{Binding currencyCode}" >
            <ComboBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel/>
                </ItemsPanelTemplate>
            </ComboBox.ItemsPanel>
        </ComboBox>

在代码中我设置了 itemSource:

CurrencyCodeComboBox.ItemsSource = [Enum].GetValues(GetType(currencyCode))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接