System.ComponentModel.DescriptionAttribute 这个 Attribute,经常被用来为属性或事件提供说明,这个说明是可以被本地化的。在一些用户界面中,就可以利用这个 Attribute 提供一些额外的信息,就像 Visual Studio 中所做的,如图 1 所示:
图 1 可以看到,对 AutoSizeMode 的说明,被显示在了下面的框中。
但是,界面中的枚举项就没这么好的待遇了,C# 类库中并没有内建对枚举项的 DescriptionAttribute 的支持,就像上面的图所显示的那样,枚举项仍然是英文的。要想提供自己想要的说明,就需要自己来完成。
这个功能实现起来其实也很简单,就是通过反射去读取 DescriptionAttribute 的 Description 属性的值,代码如下所示:
/// <summary> /// 返回枚举项的描述信息。 /// </summary> /// <param name="value">要获取描述信息的枚举项。</param> /// <returns>枚举想的描述信息。</returns> public static string GetDescription(Enum value) { Type enumType = value.GetType(); // 获取枚举常数名称。 string name = Enum.GetName(enumType, value); if (name != null) { // 获取枚举字段。 FieldInfo fieldInfo = enumType.GetField(name); if (fieldInfo != null) { // 获取描述的属性。 DescriptionAttribute attr = Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute), false) as DescriptionAttribute; if (attr != null) { return attr.Description; } } } return null; }
这段代码还是很容易看懂的,这里取得枚举常数的名称使用的是 Enum.GetName() 而不是 ToString(),因为前者更快,而且对于不是枚举常数的值会返回 null,不用进行额外的反射。
当然,这段代码仅是一个简单的示例,接下来会进行更详细的分析。
在给出更加完整的实现之前,先要说说这个 DescriptionAttribute 的问题。
我个人认为,对于枚举来说,这个说明更像是一个可以本地化的、更为友好的别名,而不是一个解释或说明。就拿开头图片里的 AutoSizeMode 这个枚举为例子,我们更希望看到的是“自动扩大或缩小”和“只能扩大”,而不是 MSDN 中的说明那样“控件根据它的内容增大或缩小。 不能手动调整该控件的大小。”和“控件可以根据其内容任意增大,但不会缩小至小于它的 Size 属性值。 窗体可以调整大小,但不能缩小到它所包含的任意控件被隐藏。”
所以,这里更适合的使用 DisplayNameAttribute,而不是 DescriptionAttribute。但可惜的是,DisplayNameAttribute 只能用于类、方法、属性或事件,字段被它无情的抛弃了,因此目前只能拿并不是很合适的 DescriptionAttribute 来凑和了。
吐槽完毕,开始说正事。首先来说,上面的那个函数还是很粗糙的,有很多情况都没有考虑,例如:如果给出的 value 并没有对应一个枚举常数,应该怎么办?
首先参考下 Microsoft 是怎么做的,下面是 Enum.ToString() 的做法:
所以我也将采用类似的做法,但是对于实例的值不能等于已命名常数的组合的情况(上面的第二点),会返回能够匹配的常数名称+未被匹配的数字值,而不仅仅只是数字值,这样我看来会更方便一些。
拿 BindingFlags 枚举来举例子的话,对于值 129,如果直接使用 Enum.ToString(),会直接返回 129,但我认为返回 IgnoreCase, 128 是一个更好的选择。
下面先上代码:
/// <summary> /// 返回指定枚举值的描述(通过 /// <see cref="System.ComponentModel.DescriptionAttribute"/> 指定)。 /// 如果没有指定描述,则返回枚举常数的名称,没有找到枚举常数则返回枚举值。 /// </summary> /// <param name="value">要获取描述的枚举值。</param> /// <returns>指定枚举值的描述。</returns> public static string GetDescription(this Enum value) { Type enumType = value.GetType(); // 寻找枚举值的组合。 EnumCache cache = GetEnumCache(enumType.TypeHandle); ulong valueUL = ToUInt64(value); int idx = Array.BinarySearch(cache.Values, valueUL); if (idx >= 0) { // 枚举值已定义,直接返回相应的描述。 return cache.Descriptions[idx]; } // 不是可组合的枚举,直接返回枚举值得字符串形式。 if (!cache.HasFlagsAttribute) { return GetStringValue(enumType, valueUL); } List<string> list = new List<string>(); // 从后向前寻找匹配的二进制。 for (int i = cache.Values.Length - 1; i >= 0 && valueUL != 0UL; i--) { ulong enumValue = cache.Values[i]; if (enumValue == 0UL) { continue; } if ((valueUL & enumValue) == enumValue) { valueUL -= enumValue; list.Add(cache.Descriptions[i]); } } list.Reverse(); // 添加最后剩余的未定义值。 if (list.Count == 0 || valueUL != 0UL) { list.Add(GetStringValue(enumType, valueUL)); } return string.Join(", ", list); }
代码中的 GetEnumCache 会返回特定枚举类型的值和对应说明的缓存,这样能够避免每次都进行反射,可以显著提高性能。
枚举值的所有比较都是使用 UInt64 来完成的,这样更容易写代码(比直接拿着 object 去写更方便),而且在进行二分查找时效率也更高。
对于应用了 Flags 标志的枚举,二进制的匹配时从后向前的(注意 Values 是从小到大排序的),在最后再进行反转,这样就可以得到与 Enum.ToString() 相同的顺序。
而 GetStringValue 方法,就是获取枚举值对应的数字。但这里不能直接 ToString(),因为枚举值可以是负数,为了保证输出的值与定义的相同,需要根据枚举的基础类型进行判断,是否转换为 Int64 再输出。
现在已经可以根据枚举得到相应的说明了,接下来要完成其逆过程——解析。解析过程大体说来就是下面的四步:
解析方法的代码如下所示:
public static object ParseEx(Type enumType, string value, bool ignoreCase) { ExceptionHelper.CheckArgumentNull(enumType, "enumType"); ExceptionHelper.CheckArgumentNull(value, "value"); if (!enumType.IsEnum) { throw ExceptionHelper.MustBeEnum(enumType); } value = value.Trim(); if (value.Length == 0) { throw ExceptionHelper.MustContainEnumInfo(); } // 尝试对数字进行解析,这样可避免之后的字符串比较。 char firstChar = value[0]; ulong tmpValue; if (ParseString(value, out tmpValue)) { return Enum.ToObject(enumType, tmpValue); } // 尝试对描述信息进行解析。 EnumCache cache = GetEnumCache(enumType.TypeHandle); StringComparison comparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; ulong valueUL = 0; int start = 0; do { // 去除前导空白。 while (char.IsWhiteSpace(value, start)) { start++; } int idx = value.IndexOf(‘,‘, start); if (idx < 0) { idx = value.Length; } int nIdx = idx - 1; // 去除后面的空白。 while (char.IsWhiteSpace(value, nIdx)) { nIdx--; } if (nIdx >= start) { string str = value.Substring(start, nIdx - start + 1); int j = 0; // 比较常数值的名称和描述信息,先比较名称,后比较描述信息。 for (; j < cache.Names.Length; j++) { if (string.Equals(str, cache.Names[j], comparison)) { // 与常数值匹配。 valueUL |= cache.Values[j]; break; } } if (j == cache.Names.Length && cache.HasDescription) { // 比较描述信息。 for (j = 0; j < cache.Descriptions.Length; j++) { if (string.Equals(str, cache.Descriptions[j], comparison)) { // 与描述信息匹配。 valueUL |= cache.Values[j]; break; } } } // 未识别的枚举值。 if (j == cache.Descriptions.Length) { // 尝试识别为数字。 if (ParseString(str, out tmpValue)) { valueUL |= tmpValue; } else { // 不能识别为数字。 throw ExceptionHelper.EnumValueNotFound(enumType, str); } } } start = idx + 1; } while (start < value.Length); return Enum.ToObject(enumType, valueUL); }
要在界面中显示对象的属性,经常用到的控件就是 PropertyGrid 了。如果希望枚举的说明可以在 PropertyGrid 中显示,可以利用 TypeConverterAttribute 来做到这一点。
首先需要定义一个支持读取枚举说明的 EnumDescConverter 类,它可以直接继承自 TypeConverter 类,也可以继承自 EnumConverter。它需要做的就是将枚举值转换为字符串(ConvertTo)时,使用 GetDescription() 而不是 ToString()。在 ConvertFrom 时,也要支持枚举说明的解析。
using System; using System.ComponentModel; using System.Globalization; namespace Cyjb.ComponentModel { /// <summary> /// 提供将 <see cref="System.Enum"/> 对象与其他各种表示形式相互转换的类型转换器。 /// 支持枚举值的描述信息。 /// </summary> public class EnumDescConverter : EnumConverter { /// <summary> /// 使用指定类型初始化 <see cref="EnumDescConverter"/> 类的新实例。 /// </summary> /// <param name="type">表示与此转换器关联的枚举类型。</param> public EnumDescConverter(Type type) : base(type) { } /// <summary> /// 将指定的值对象转换为枚举对象。 /// </summary> /// <param name="context"><see cref="System.ComponentModel.ITypeDescriptorContext"/>, /// 提供格式上下文。</param> /// <param name="culture">一个可选的 <see cref="System.Globalization.CultureInfo"/>。 /// 如果未提供区域性设置,则使用当前区域性。</param> /// <param name="value">要转换的 <see cref="System.Object"/>。</param> /// <returns>表示转换的 <paramref name="value"/> 的 <see cref="System.Object"/>。</returns> public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { string strValue = value as string; if (strValue != null) { try { return EnumExt.ParseEx(this.EnumType, strValue, true); } catch (Exception ex) { throw ExceptionHelper.ConvertInvalidValue(value, this.EnumType, ex); } } return base.ConvertFrom(context, culture, value); } /// <summary> /// 将给定的值对象转换为指定的目标类型。 /// </summary> /// <param name="context"><see cref="System.ComponentModel.ITypeDescriptorContext"/>, /// 提供格式上下文。</param> /// <param name="culture">一个可选的 <see cref="System.Globalization.CultureInfo"/>。 /// 如果未提供区域性设置,则使用当前区域性。</param> /// <param name="value">要转换的 <see cref="System.Object"/>。</param> /// <param name="destinationType">要将值转换成的 <see cref="System.Type"/>。</param> /// <returns>表示转换的 <paramref name="value"/> 的 <see cref="System.Object"/>。</returns> public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { ExceptionHelper.CheckArgumentNull(destinationType, "destinationType"); if (value != null && destinationType.TypeHandle.Equals(typeof(string).TypeHandle)) { return EnumExt.GetDescription((Enum)value); } return base.ConvertTo(context, culture, value, destinationType); } } }
然后利用 [TypeConverter(EnumDescConverter)] 在需要的属性上标识出自己的转换器类,这样 PropertyGrid 上显示的就是想要的说明了。
public class TestClass { [TypeConverter(typeof(EnumDescConverter))] public Tristate Value { get; set; } // 这里的 Tristate 就是一个应用了 DescriptionAttribute 的枚举。 }
图 2 界面中显示的枚举值已经被正确的显示为中文。
最后是相关代码的链接:
包含枚举的相关方法的类 EnumExt 的完整代码可见 https://github.com/CYJB/Cyjb/blob/master/Cyjb/EnumExt.cs
上面的 EnumDescConverter 可见 https://github.com/CYJB/Cyjb/blob/master/Cyjb/ComponentModel/EnumDescConverter.cs
C# 获取与解析枚举类型的 DescriptionAttribute,布布扣,bubuko.com
C# 获取与解析枚举类型的 DescriptionAttribute
原文:http://www.cnblogs.com/lonelyxmas/p/3815762.html