如何绑定带有自定义字符串格式的枚举值的ComboBox?

140

在帖子Enum ToString中,介绍了一种使用自定义属性DescriptionAttribute的方法:

Enum HowNice {
  [Description("Really Nice")]
  ReallyNice,
  [Description("Kinda Nice")]
  SortOfNice,
  [Description("Not Nice At All")]
  NotNice
}

然后,您调用一个名为GetDescription的函数,语法类似于:

GetDescription<HowNice>(NotNice); // Returns "Not Nice At All"

但是这并不能帮助我简单地使用枚举的值填充ComboBox,因为我无法强制ComboBox调用GetDescription

我想要实现以下要求:

  • 读取(HowNice)myComboBox.selectedItem将返回所选值作为枚举值。
  • 用户应该看到友好的显示字符串,而不仅仅是枚举值的名称。 因此,用户将看到“Not Nice At All”,而不是“NotNice”。
  • 希望解决方案对现有枚举代码的更改最小。

显然,我可以为每个枚举实现一个新类,并覆盖其ToString()方法,但对于每个枚举来说这都是很多工作,我宁愿避免这种情况。

有任何想法吗?

嘿,我甚至会给你一个拥抱作为赏金 :-)


1
jjnguy说得对,Java枚举类型非常好地解决了这个问题(http://javahowto.blogspot.com/2006/10/custom-string-values-for-enum.html),但这个问题的相关性值得怀疑。 - Matthew Flaschen
8
Java的枚举类型有些可笑。或许他们会在2020年添加属性:/ - Chad Grant
对于一个更轻量级(但可能不够强大)的解决方案,请参见我的帖子 - Gutblender
21个回答

2

跟进 @scraimer 的回答,这是一个支持标志位的枚举转字符串类型转换器的版本:

    /// <summary>
/// A drop-in converter that returns the strings from 
/// <see cref="System.ComponentModel.DescriptionAttribute"/>
/// of items in an enumaration when they are converted to a string,
/// like in ToString().
/// </summary>
public class EnumToStringUsingDescription : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return (sourceType.Equals(typeof(Enum)));
    }

    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
        return (destinationType.Equals(typeof(String)));
    }

    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
    {
        return base.ConvertFrom(context, culture, value);
    }

    public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType.Equals(typeof(String)))
        {
            string name = value.ToString();
            Type effectiveType = value.GetType();          

            if (name != null)
            {
                FieldInfo fi = effectiveType.GetField(name);
                if (fi != null)
                {
                    object[] attrs =
                    fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
                    return (attrs.Length > 0) ? ((DescriptionAttribute)attrs[0]).Description : name;
                }

            }
        }

        return base.ConvertTo(context, culture, value, destinationType);
    }

    /// <summary>
    /// Coverts an Enums to string by it's description. falls back to ToString.
    /// </summary>
    /// <param name="value">The value.</param>
    /// <returns></returns>
    public string EnumToString(Enum value)
    {
        //getting the actual values
        List<Enum> values = EnumToStringUsingDescription.GetFlaggedValues(value);
        //values.ToString();
        //Will hold results for each value
        List<string> results = new List<string>();
        //getting the representing strings
        foreach (Enum currValue in values)
        {
            string currresult = this.ConvertTo(null, null, currValue, typeof(String)).ToString();;
            results.Add(currresult);
        }

        return String.Join("\n",results);

    }

    /// <summary>
    /// All of the values of enumeration that are represented by specified value.
    /// If it is not a flag, the value will be the only value retured
    /// </summary>
    /// <param name="value">The value.</param>
    /// <returns></returns>
    private static List<Enum> GetFlaggedValues(Enum value)
    {
        //checking if this string is a flaged Enum
        Type enumType = value.GetType();
        object[] attributes = enumType.GetCustomAttributes(true);
        bool hasFlags = false;
        foreach (object currAttibute in attributes)
        {
            if (enumType.GetCustomAttributes(true)[0] is System.FlagsAttribute)
            {
                hasFlags = true;
                break;
            }
        }
        //If it is a flag, add all fllaged values
        List<Enum> values = new List<Enum>();
        if (hasFlags)
        {
            Array allValues = Enum.GetValues(enumType);
            foreach (Enum currValue in allValues)
            {
                if (value.HasFlag(currValue))
                {
                    values.Add(currValue);
                }
            }



        }
        else//if not just add current value
        {
            values.Add(value);
        }
        return values;
    }

}

还有一个用于使用它的扩展方法:

    /// <summary>
    /// Converts an Enum to string by it's description. falls back to ToString
    /// </summary>
    /// <param name="enumVal">The enum val.</param>
    /// <returns></returns>
    public static string ToStringByDescription(this Enum enumVal)
    {
        EnumToStringUsingDescription inter = new EnumToStringUsingDescription();
        string str = inter.EnumToString(enumVal);
        return str;
    }

1

抱歉让这个旧帖子浮出水面。

我会采用以下方式本地化枚举,因为它可以通过下拉列表文本字段向用户显示有意义和本地化的值,而不仅仅是描述。在此示例中,我创建了一个名为OwToStringByCulture的简单方法,以从全局资源文件获取本地化字符串,例如App_GlobalResources文件夹中的BiBongNet.resx。在此资源文件中,请确保所有字符串与枚举的值(ReallyNice、SortOfNice、NotNice)相同。在此方法中,我传递参数:resourceClassName,通常是资源文件的名称。

接下来,我创建了一个静态方法,使用枚举作为其数据源填充下拉列表,称为OwFillDataWithEnum。稍后可以将此方法与任何枚举一起使用。

然后,在具有名为DropDownList1的下拉列表的页面中,我在Page_Load中设置以下仅一行简单的代码,以将枚举填充到下拉列表中。

 BiBongNet.OwFillDataWithEnum<HowNice>(DropDownList1, "BiBongNet");

就是这样。我认为通过一些简单的方法,您可以使用不仅具有描述性值而且具有本地化文本以显示的任何枚举填充任何列表控件。您可以将所有这些方法作为扩展方法以获得更好的使用。

希望这有所帮助。 分享以获得分享!

以下是这些方法:

public class BiBongNet
{

        enum HowNice
        {
            ReallyNice,
            SortOfNice,
            NotNice
        }

        /// <summary>
        /// This method is for filling a listcontrol,
        /// such as dropdownlist, listbox... 
        /// with an enum as the datasource.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="ctrl"></param>
        /// <param name="resourceClassName"></param>
        public static void OwFillDataWithEnum<T>(ListControl ctrl, string resourceClassName)
        {
            var owType = typeof(T);
            var values = Enum.GetValues(owType);
            for (var i = 0; i < values.Length; i++)
            {
                //Localize this for displaying listcontrol's text field.
                var text = OwToStringByCulture(resourceClassName, Enum.Parse(owType, values.GetValue(i).ToString()).ToString());
                //This is for listcontrol's value field
                var key = (Enum.Parse(owType, values.GetValue(i).ToString()));
                //add values of enum to listcontrol.
                ctrl.Items.Add(new ListItem(text, key.ToString()));
            }
        }

        /// <summary>
        /// Get localized strings.
        /// </summary>
        /// <param name="resourceClassName"></param>
        /// <param name="resourceKey"></param>
        /// <returns></returns>
        public static string OwToStringByCulture(string resourceClassName, string resourceKey)
        {
                return (string)HttpContext.GetGlobalResourceObject(resourceClassName, resourceKey);
        }
}

1
Enum HowNice {
  [Description("Really Nice")]
  ReallyNice,
  [Description("Kinda Nice")]
  SortOfNice,
  [Description("Not Nice At All")]
  NotNice
}

为了解决这个问题,您应该使用扩展方法和字符串数组,如下所示:
Enum HowNice {
  ReallyNice  = 0,
  SortOfNice  = 1,
  NotNice     = 2
}

internal static class HowNiceIsThis
{
 const String[] strings = { "Really Nice", "Kinda Nice", "Not Nice At All" }

 public static String DecodeToString(this HowNice howNice)
 {
   return strings[(int)howNice];
 }
}

简单的代码和快速的解码。


字符串变量应该是静态的,并且声明如下:静态字符串[] strings = new [] {...}。 - Sérgio
唯一的问题是,您需要为每个枚举编写一个函数,并且描述将成为枚举本身的一部分... - Avi Turner

1
创建一个集合,其中包含所需的内容(如包含一个 Value 属性的简单对象,该属性包含 HowNice 枚举值,以及一个包含 GetDescription<HowNice>(Value)Description 属性),然后将组合框与该集合进行数据绑定。
类似于这样:
Combo.DataSource = new EnumeratedValueCollection<HowNice>();
Combo.ValueMember = "Value";
Combo.DisplayMember = "Description";

当你有一个像这样的集合类:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace Whatever.Tickles.Your.Fancy
{
    public class EnumeratedValueCollection<T> : ReadOnlyCollection<EnumeratedValue<T>>
    {
        public EnumeratedValueCollection()
            : base(ListConstructor()) { }
        public EnumeratedValueCollection(Func<T, bool> selection)
            : base(ListConstructor(selection)) { }
        public EnumeratedValueCollection(Func<T, string> format)
            : base(ListConstructor(format)) { }
        public EnumeratedValueCollection(Func<T, bool> selection, Func<T, string> format)
            : base(ListConstructor(selection, format)) { }
        internal EnumeratedValueCollection(IList<EnumeratedValue<T>> data)
            : base(data) { }

        internal static List<EnumeratedValue<T>> ListConstructor()
        {
            return ListConstructor(null, null);
        }

        internal static List<EnumeratedValue<T>> ListConstructor(Func<T, string> format)
        {
            return ListConstructor(null, format);
        }

        internal static List<EnumeratedValue<T>> ListConstructor(Func<T, bool> selection)
        {
            return ListConstructor(selection, null);
        }

        internal static List<EnumeratedValue<T>> ListConstructor(Func<T, bool> selection, Func<T, string> format)
        {
            if (null == selection) selection = (x => true);
            if (null == format) format = (x => GetDescription<T>(x));
            var result = new List<EnumeratedValue<T>>();
            foreach (T value in System.Enum.GetValues(typeof(T)))
            {
                if (selection(value))
                {
                    string description = format(value);
                    result.Add(new EnumeratedValue<T>(value, description));
                }
            }
            return result;
        }

        public bool Contains(T value)
        {
            return (Items.FirstOrDefault(item => item.Value.Equals(value)) != null);
        }

        public EnumeratedValue<T> this[T value]
        {
            get
            {
                return Items.First(item => item.Value.Equals(value));
            }
        }

        public string Describe(T value)
        {
            return this[value].Description;
        }
    }

    [System.Diagnostics.DebuggerDisplay("{Value} ({Description})")]
    public class EnumeratedValue<T>
    {
        private T value;
        private string description;
        internal EnumeratedValue(T value, string description) {
            this.value = value;
            this.description = description;
        }
        public T Value { get { return this.value; } }
        public string Description { get { return this.description; } }
    }

}

正如你所见,这个集合非常易于定制,可以使用lambda表达式来选择枚举器的子集,并且可以实现自定义的字符串格式化而不是使用你提到的 GetDescription<T>(x) 函数。


很好,但我正在寻找需要更少代码工作的东西。 - Shalom Craimer
你是否可以为所有枚举器使用相同的通用集合来处理这种情况呢?当然,我并不是建议为每个枚举器编写自定义集合。 - peSHIr

1

我将编写一个通用类型的类,以便与任何类型一起使用。 我曾经在过去使用过类似以下代码:

public class ComboBoxItem<T>
{
    /// The text to display.
    private string text = "";
    /// The associated tag.
    private T tag = default(T);

    public string Text
    {
        get
        {
            return text;
        }
    }

    public T Tag
    {
        get
        {
            return tag;
        }
    }

    public override string ToString()
    {
        return text;
    }

    // Add various constructors here to fit your needs
}

此外,您可以添加一个静态的“工厂方法”,根据枚举类型创建一个组合框项目列表(与您现在的GetDescriptions方法几乎相同)。这将使您无需为每个枚举类型实现一个实体,并提供一个良好/逻辑的位置来使用“GetDescriptions”辅助方法(我个人会将其命名为FromEnum(T obj)...

1
你需要做的是将枚举转换为只读集合,并将集合绑定到组合框(或任何支持键值对的控件)。
首先,你需要一个类来包含列表项。由于你只需要 int/string 对,建议使用接口和基类组合,以便你可以在任何想要的对象中实现功能:
public interface IValueDescritionItem
{
    int Value { get; set;}
    string Description { get; set;}
}

public class MyItem : IValueDescritionItem
{
    HowNice _howNice;
    string _description;

    public MyItem()
    {

    }

    public MyItem(HowNice howNice, string howNice_descr)
    {
        _howNice = howNice;
        _description = howNice_descr;
    }

    public HowNice Niceness { get { return _howNice; } }
    public String NicenessDescription { get { return _description; } }


    #region IValueDescritionItem Members

    int IValueDescritionItem.Value
    {
        get { return (int)_howNice; }
        set { _howNice = (HowNice)value; }
    }

    string IValueDescritionItem.Description
    {
        get { return _description; }
        set { _description = value; }
    }

    #endregion
}

这里是接口和一个实现它的示例类。请注意,该类的键是强类型枚举,并且 IValueDescritionItem 属性是显式实现的(因此该类可以具有任何属性,您可以选择实现键/值对的属性)。

现在是 EnumToReadOnlyCollection 类:

public class EnumToReadOnlyCollection<T,TEnum> : ReadOnlyCollection<T> where T: IValueDescritionItem,new() where TEnum : struct
{
    Type _type;

    public EnumToReadOnlyCollection() : base(new List<T>())
    {
        _type = typeof(TEnum);
        if (_type.IsEnum)
        {
            FieldInfo[] fields = _type.GetFields();

            foreach (FieldInfo enum_item in fields)
            {
                if (!enum_item.IsSpecialName)
                {
                    T item = new T();
                    item.Value = (int)enum_item.GetValue(null);
                    item.Description = ((ItemDescription)enum_item.GetCustomAttributes(false)[0]).Description;
                    //above line should be replaced with proper code that gets the description attribute
                    Items.Add(item);
                }
            }
        }
        else
            throw new Exception("Only enum types are supported.");
    }

    public T this[TEnum key]
    {
        get 
        {
            return Items[Convert.ToInt32(key)];
        }
    }

}

所以你在代码中需要的只有:

private EnumToReadOnlyCollection<MyItem, HowNice> enumcol;
enumcol = new EnumToReadOnlyCollection<MyItem, HowNice>();
comboBox1.ValueMember = "Niceness";
comboBox1.DisplayMember = "NicenessDescription";
comboBox1.DataSource = enumcol;

请记住,您的集合是使用MyItem类型进行类型化的,因此如果绑定到适当的属性,则组合框值应返回枚举值。

我添加了T this [Enum t]属性,使该集合比简单的可消耗组合框更有用,例如textBox1.Text = enumcol[HowNice.ReallyNice].NicenessDescription;

当然,您可以选择将MyItem转换为仅用于此目的的键/值类,从而完全跳过EnumToReadnlyCollection的类型参数中的MyItem,但这样您将被迫使用int作为键(这意味着获取combobox1.SelectedValue将返回int而不是枚举类型)。如果您创建一个KeyValueItem类来替换MyItem等等,您可以解决这个问题...


1
你可以使用PostSharp来针对Enum.ToString并添加你想要的额外代码。这不需要任何代码更改。

1
我尝试了这种方法,对我很有效。
我创建了一个枚举的包装类,并重载了隐式操作符,以便我可以将其分配给枚举变量(在我的情况下,我必须将对象绑定到 ComboBox 值)。
您可以使用反射来按您想要的方式格式化枚举值,在我的情况下,我从枚举值中检索 DisplayAttribute(如果存在)。
希望这有所帮助。
public sealed class EnumItem<T>
{
    T value;

    public override string ToString()
    {
        return Display;
    }

    public string Display { get; private set; }
    public T Value { get; set; }

    public EnumItem(T val)
    {
        value = val;
        Type en = val.GetType();
        MemberInfo res = en.GetMember(val.ToString())?.FirstOrDefault();
        DisplayAttribute display = res.GetCustomAttribute<DisplayAttribute>();
        Display = display != null ? String.Format(display.Name, val) : val.ToString();
    }

    public static implicit operator T(EnumItem<T> val)
    {
        return val.Value;
    }

    public static implicit operator EnumItem<T>(T val)
    {
        return new EnumItem<T>(val);
    }
}

编辑:

以防万一,我使用以下函数获取用于ComboBoxDataSourceenum值。

public static class Utils
{
    public static IEnumerable<EnumItem<T>> GetEnumValues<T>()
    {
        List<EnumItem<T>> result = new List<EnumItem<T>>();
        foreach (T item in Enum.GetValues(typeof(T)))
        {
            result.Add(item);
        }
        return result;
    }
}

0

一旦您拥有了GetDescription方法(它需要是全局静态的),您可以通过扩展方法使用它:

public static string ToString(this HowNice self)
{
    return GetDescription<HowNice>(self);
}

-1

你可以定义枚举类型为

Enum HowNice {   
[StringValue("Really Nice")]   
ReallyNice,   
[StringValue("Kinda Nice")]   
SortOfNice,   
[StringValue("Not Nice At All")]   
NotNice 
} 

然后使用 HowNice.GetStringValue()


2
这段代码无法编译(我使用的是.NET 3.5)。´StringValue´在哪里声明的? - awe
1
@scraimer的答案是一样的,只是他在使用框架之外的属性,而你使用了某种自定义属性。 - Oliver

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