65.9K
CodeProject 正在变化。 阅读更多。
Home

用于 PropertyGrid 和 Visual Studio 的位枚举编辑器

2014 年 8 月 20 日

CPOL

12分钟阅读

viewsIcon

51653

downloadIcon

907

应用于枚举类型的 Editor 属性使其可以在 PropertyGrid 中进行编辑。这足以让 Visual Studio Designer 使用该编辑器,而无需编写 Visual Studio 插件。

Visual Studio look

目录

1. 引言

本文是我为 CodeProject 成员呈现的一系列关于枚举类型的文章中的第四篇。

  1. 枚举类型不枚举!.NET 和语言限制的变通方法;
  2. 人类可读的枚举元数据;
  3. 基于枚举的命令行实用程序;
  4. 本文。

我将使用相同的代码库,并更新了新功能。如有需要,我也会引用本文以正确理解相关内容。概念上,本文源自文章 #2 的代码,但 `Enumerations` 库的代码本身源自文章 #3 的代码,其中引入了附加项,即枚举成员名称的缩写。因此,我将该库的版本号定为 4.0。新库是 `Enumerations.UI` v.1.0。

1.1. 超越文章标题

类 `EnumerationEditor` 并非完全冗余。它不仅在风格上与 `BitwiseEnumerationEditor` 相匹配;这两个类都显示了枚举成员的可读名称和描述,显示在枚举成员值树节点上的工具提示中。

1.2. 演示应用程序

请看本文顶部的图片。它演示了如何在 Visual Studio 中为组件编辑枚举值。要查看 Visual Studio 中的演示,请生成解决方案,然后定位解决方案资源管理器中的主窗体节点,单击它以显示设计器,选择左侧的亮矩形控件(它不执行任何操作,只是承载两个演示枚举属性),然后激活属性窗口,找到图片中由树莓色箭头指示的“Demo”类别。它将演示两个编辑器;“Feature Set”属性使用了按位枚举编辑器。

要查看运行时相同的效果,只需运行应用程序。尽管演示应用程序是用 `System.Windows.Forms` 编写的,但提供的库可以用于所有其他类型的程序集:WPF、ASP.NET、Silverlight 或任何其他类型,因为无论如何,枚举类型将在 Visual Studio 和其他 .NET IDE 中以相同的方式显示和编辑。

2. 示例用法

现在,让我们回顾一下用于实现上述 PropertyGrid 行为的代码示例。

namespace EnumerationEditorDemo {
    using FlagsAttribute = System.FlagsAttribute;
    using EditorAttribute = System.ComponentModel.EditorAttribute;
    using DisplayNameAttribute = SA.Universal.Enumerations.DisplayNameAttribute;
    using DescriptionAttribute = SA.Universal.Enumerations.DescriptionAttribute;
    using AbbreviationAttribute = SA.Universal.Enumerations.AbbreviationAttribute;
    using EnumerationEditor = SA.Universal.Enumerations.UI.EnumerationEditor;
    using BitwiseEnumerationEditor = SA.Universal.Enumerations.UI.BitwiseEnumerationEditor;
    using UITypeEditor = System.Drawing.Design.UITypeEditor;
    using NonEnumerableAttribute = SA.Universal.Enumerations.NonEnumerableAttribute;
    using SA.Universal.Enumerations.UI;

    [Editor(typeof(BitwiseEnumerationEditor), typeof(UITypeEditor))]
    [DisplayName(typeof(Data.DisplayNames)), Description(typeof(Data.Descriptions))]
    [Flags]
    public enum FeatureSet {
        [NonEnumerable]
        None = 0,
        AutoCenterX = 1 << 0,
        AutoCenterY = 1 << 1,
        Border = 1 << 2,
        [Abbreviation(6)]
        TransparentBackground = 1 << 3,
        [Abbreviation(11)]
        BackgroundImage = 1 << 4,
        [NonEnumerable]
        Center = AutoCenterX | AutoCenterY,
        [NonEnumerable]
        All = AutoCenterX | AutoCenterY | Border | TransparentBackground | BackgroundImage,
    } //enum FeatureSet

    [Editor(typeof(EnumerationEditor), typeof(UITypeEditor))]
    [DisplayName(typeof(Data.DisplayNames)), Description(typeof(Data.Descriptions))]
    public enum Position {
        SoftwareArchitect,
        PrincipalSoftwareEngineer,
        TeamLeader,
        TechnicalLead,
        SeniorSoftwareEngineer,
        SoftwareEngineer,
        JuniorSoftwareEngineer,
    } //enum Position

} //namespace EnumerationEditorDemo

我为所有使用的属性提供了单独的 *别名* `using` 指令,以显示它们各自的来源。`DisplayName` 和 `Description` 属性的用法已在我 *第二篇系列文章* 中详细描述;缩写的用法已在我*第三篇系列文章*中描述。 `Editor` 属性定义了 `PropertyGrid` 中应使用的编辑器。`Description` 属性定义的值将显示在表示枚举成员值的树节点工具提示中。

请注意 `NonEnumerable` 属性的用法。在使用按位编辑器时,避免显示不代表单个位的枚举成员非常重要。

请注意 `Flags` 属性的使用。通常,此属性不影响应用程序运行时,但如果对枚举类型使用 `System.Object.ToString()`,则使用 `Flags` 属性很重要;它可能会影响设计时,对调试有用,等等……

3 工作原理?

3.1 主要思想

用于以独立于平台的方式编辑枚举值(如 `System.ComponentModel.EditorAttrubute` 所需,并且能够被 `ProperyGrid` 的任何实例自动调用的通用编辑器)的整个技术的唯一 *隐蔽* 方法是:`System.Type.MakeGenericType`。

甚至这个重要方法的名称也具有误导性:它实际上期望被调用的 `Type` 实例是一个泛型类型,并且它会用实际类型替换泛型参数,并将泛型类型实例化为一个新类型,该新类型可以成为一个完全定义的类型,即非泛型类型。我的代码中就是这样做的:调用创建了一个 *完全定义的类型*,通常是一个枚举类型。

为什么这样做?当然,从头开始基于反射来开发编辑器和所有必需的枚举实用程序将非常容易。但是我想重用我在*我的第一个系列文章*中介绍的机制。这项工作介绍了通过枚举类型成员进行枚举以及其他重要功能,这些功能比本文的主题更为基础;代码基于泛型,这对于具体枚举类型的用途非常重要。以独立于类型的方式进行枚举编程非常棘手,正如下一节关于“最棘手的问题”的讨论所说明的那样。

因此,想法是使用为具体枚举类型开发的反射代码,它对泛型进行反射。为此,我将抽象类 `EnumerationItemBase` 与派生的泛型类 `Enumeration<ENUM>` 分离开来,它们都代表一个具有所有属性的单个枚举成员。对基类的唯一添加是泛型类型属性和字段,它们代表具体类型的枚举成员值,以及反射机制本身。自然,独立于类型的反射应该与编辑器传入的对象的 *运行时类型*(当然,是 *编译时类型* `System.Object`)一起工作,并返回 `EnumerationItemBase` 抽象基类型的对象。这就是如何……

internal static EnumerationItemBase[] ReflectItems(object value) {
    Type type = typeof(Enumeration<>);
    Type enumerationType = type.MakeGenericType(new Type[] { value.GetType() });
    ConstructorInfo constructor = enumerationType.GetConstructor(Type.EmptyTypes);
    object enumeration = constructor.Invoke(null);
    IEnumerable enumerable = (IEnumerable)enumeration;
    EnumerationItemList list = new EnumerationItemList();
    foreach (object @object in enumerable)
        list.Add((EnumerationItemBase)@object);
    return list.ToArray();
} //ReflectItems

这段代码相当复杂但又很简单。使用反射的核心实现是在通用类 `Enumeration<enum>` 的构造函数中实现的。我*第一个系列文章*中对此进行了详细解释。

对于值对象类型为非枚举类型的情况,请参阅*我的第一个系列文章*。

这也会起作用,但接下来会发生什么:在两个枚举编辑器中,我检查了要编辑的值的类型是否为枚举类型。显然,编辑非枚举类型没有任何意义,因此编辑器根本不会被调用。由于在编译时无法检查类型级别的属性是否应用于正确的类型,因此这是一个合理的解决方案,可以帮助开发人员尽早发现问题。

顺便说一句,这项工作中所有的关键或棘手技术都放在了一个文件“EnumerationUtility.cs”中。我们刚才讨论了一种关键技术;现在让我们来讨论我面临的最棘手的问题。

3.2 最棘手的问题:底层整数类型

真正的问题是枚举类型特有的:它们具有不同的底层整数类型。而且泛型的机制不能应用于原始类型。但是,使用单个位需要使用数值整数类型。是的,这也适用于枚举类型(如*我的第一个系列文章*所示),但是……这需要具体的枚举类型,不能直接为抽象类型 `System.Enum` 完成。

如果所有这些类型都是无符号的(甚至是有符号的),那就不会有问题。在这种情况下,所有枚举值都可以转换为“最宽”的类型,例如 `System.UInt64`。不行,这是不可能的:在所有有符号和无符号类型集合中没有“最宽”的类型。这可以很容易地从两个“最宽”的候选者中看出:一个是有符号类型中的,另一个是无符号类型中的。

-9223372036854775808   0                     9223372036854775807
[======================|======================] long (signed)      18446744073709551615
                      [=============================================] ulong

显然,值的域是重叠的。如果出于某种奇怪的原因,库的用户使用了 `ulong` 作为基类型,并且实际上为某个成员使用了显式定义的高值,那么转换为 `long` 或任何其他有符号类型将抛出 `System.InvalidCast` 异常。如果用户使用 `long` 并且实际上使用了任何负成员值,那么尝试转换为 `ulong` 将抛出相同的异常。怎么办?我识别了几种可能性。

  1. 使用所有可以是任何枚举类型的底层类型的类型,可以使用函数 `System.Enum.GetUnderlyingType` 来检查(参见 http://msdn.microsoft.com/en-us/library/system.enum.getunderlyingtype(v=vs.110).aspx)。这是论坛上最常见的建议。我能说什么?太糟糕了,太糟糕了!
  2. 使用 `System.Runtime.InteropServices.Marshal` 类将任何未知类型序列化为字节数组。这种方法和下一个解决方案的问题在于,互操作服务总是存在破坏代码平台兼容性的风险。此外,代码很复杂,几乎不可能调试,容易出错。
  3. 与上述类似,使用 `Marshal` 来创建类似于 C++ `reinterpret_cast` 的东西。
  4. 使用程序集的 unsafe 模式将允许处理枚举值实例的指针。在没有特殊需要的情况下使用 unsafe,嗯……是不安全的。这样的代码更简单,但也可能容易出错,而且问题很难发现;涉及内存固定。
  5. 值可以序列化到内存流中,然后从流中解析。嗯,除了明显的性能影响(谁关心性能,当所有这些都在 UI 的一次单独点击中发生时?),这个解决方案非常不自然。我称之为“用左腿挠右耳”。

因此,考虑到这一切,我得出了可能是最简单的解决方案:使用 `long` 和 `ulong` 类型,并可能处理上述异常。

internal static bool IsBitSet(object bit, object value) {
    try {
        return
            ((ulong)Convert.ChangeType(bit, typeof(ulong)) &     
             (ulong)Convert.ChangeType(value, typeof(ulong))) > 0;
    } catch (System.InvalidCastException) {
        return
            ((long)Convert.ChangeType(bit, typeof(long)) &
             (long)Convert.ChangeType(value, typeof(long))) > 0;
    } // exception
} //IsBitSet

internal static object SetBit(object bit, object value) {
    Type underlyingType = Enum.GetUnderlyingType(bit.GetType());
    try {
        ulong numBit = (ulong)Convert.ChangeType(bit, typeof(ulong));
        ulong numValue = (ulong)Convert.ChangeType(value, typeof(ulong));
        ulong numResult = numValue | numBit;
        return Convert.ChangeType(numResult, underlyingType);
    } catch (System.InvalidCastException) {
        long numBit = (long)Convert.ChangeType(bit, typeof(long));
        long numValue = (long)Convert.ChangeType(value, typeof(long));
        long numResult = numValue | numBit;
        return Convert.ChangeType(numResult, underlyingType);
    } // exception
} //IsBitSet

显然,使用非负枚举值是最可能的情况,因此代码从尝试使用 `ulong` 开始。在出现任何负值的情况下,`ulong` 转换将失败。这种情况由使用 `long` 类型来处理。

我认为这个解决方案是最简单且相当可靠的。如果有人发现更好的解决方案,我将非常兴奋。如果有人发现一个缺陷,我将印象深刻,并将尽力修复它。

除了上述之外,还需要将抽象类型枚举值的初始值设置为某种“通用”二进制零,这显然是更新按位编辑器中的值所必需的。这也花了一些思考,但要容易得多。

internal static object MakeZeroObject(Enum value) {
    return Convert.ChangeType(0, Enum.GetUnderlyingType(value.GetType()));
} //MakeZeroObject

让我们看看这段代码能坚持多久。此功能实际上可能最终会失效。要使其失效,1)新的 CLR 标准应引入 128 位整数类型(或其他比 `long` 和 ulong 更宽的类型);2)它应允许这些类型作为枚举类型的底层整数类型;3)某些用户应实际使用这些类型,并使用负值或“大”值,以进入 `long` 和 `ulong` 域之外的域。如果发生这种情况,则需要修复上面的代码:`long` 和 `ulong` 应替换为这些新类型。

尽管 128 位架构实际上正在兴起,但有些东西告诉我,这种修复可能不会很快需要。

3.3 其余都很简单

在 MSDN 中描述了用于 `System.ComponentModel.EditorAttribute` 的编辑器的创建,也许描述不够详细,但并不难理解。类型转换器也是如此。请参阅
`System.ComponentModel.EditorAttribute`,http://msdn.microsoft.com/en-us/library/system.componentmodel.editorattribute%28v=vs.110%29.aspx
`System.ComponentModel.TypeConverterAttribute`,http://msdn.microsoft.com/en-us/library/system.componentmodel.typeconverterattribute%28v=vs.110%29.aspx
`System.Drawing.Design.UITypeEditor`,http://msdn.microsoft.com/en-us/library/system.drawing.design.uitypeeditor%28v=vs.110%29.aspx
`System.ComponentModel.TypeConverter`,http://msdn.microsoft.com/en-us/library/system.componentmodel.typeconverter%28v=vs.110%29.aspx

两个枚举编辑器都基于一个实现通用功能的抽象基类。这是主要的编辑方法。

public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) {
    if (value == null) return null;
    Type type = value.GetType();
    if (!type.IsEnum) return null;
    treeView.Nodes.Clear();
    Populate(value);
    edSvc = (IWindowsFormsEditorService)
        provider.GetService(typeof(IWindowsFormsEditorService));
    edSvc.DropDownControl(treeView);
    return UpdateData(value, treeView);
} //EditValue

唯一不那么明显的难题是获取显示在下拉列表中的类型的实例;它的类型是 `IWindowsFormsEditorService`。

其余代码使用了上面描述的枚举实用程序以及我在该系列的第一篇和第二篇文章中解释的工具。

4. 结论

本文很好地总结了我关于枚举的系列文章。该系列文章中涵盖的所有代码的最新版本可以在本文的源代码中找到。上一篇文章添加了一个更具应用性质的库:命令行实用程序。

然而,我在上一篇关于该主题的文章中已经犯了一个小错误:我称其为该系列的最后一篇。此刻,我也觉得本文是最后一篇,但这次我不那么确定了。如果我碰巧又有了关于这个主题的想法,我会尝试写出来。如果有些读者问我一个有趣的问题,或者以任何方式给我另一个关于这个主题的想法,我会非常兴奋,并会尝试去实现它。这就引出了最后一节……

5 致谢

我想提及两位 CodeProject 会员与这项工作相关。

Grant Frisken 是这篇 CodeProject 文章的作者:本地化 .NET 枚举。他和我争论了我第一部作品中描述的库的功能。他坚持类型转换器和 `System.ComponentModel.TypeConverterAttribute` 的重要性。我一直批评 `System.ComponentModel` 的可维护性和总体架构缺陷,并坚持我的方法具有更基础和清晰的特性。我希望我们在这个非常有趣和友好的讨论中最终互相理解了。为了最终调和我们的观点,我现在可以添加,我的第一部作品是正确的,而且那不是类型转换器适用的地方,因为它与 UI 无关。然而,通过 PropertyGrid 为枚举类型提供“可编辑性”确实需要使用类型转换器。现在正是这样做的恰当时机。:-)

尽管 Grant 的信息没有给我提供本文的灵感(其灵感源于我自己的非主题相关作品以及我在文章开头引用的与枚举相关的工作),但我希望归功于他有趣且有用的文章,以及他本人,感谢他对这个问题的深刻理解,以及他是一位知识渊博、乐于助人、思想开放且善于协作的开发者和社区成员。

至于本文的灵感……Bigbro_1985 问了我几个有趣的问题,这些问题激发了我写这篇文章。此外,他还提醒了我使用工具提示的想法;我在枚举成员的描述文本中使用了树节点工具提示。谢谢你,Marco!

[更新]

2017 年 4 月 4 日,BillWoodruff 报告了一个我当天修复的关键问题。
© . All rights reserved.