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

增强的 CollectionEditor 框架

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2014年7月17日

CPOL

46分钟阅读

viewsIcon

32391

downloadIcon

712

一个易于使用、可自定义的集合编辑器;支持继承

下载 EnhancedCollectionEds-DLLSrc.zip

引言

当集合包含基本类型时,实现基本的 CollectionEditor 是非常容易的。但是,当您想要编辑一个类对象集合时,复杂性会增加一些,这需要多个特性(Attribute),并且很可能还需要一个类型转换器(TypeConverter)。

当您的类对象继承自一个抽象/MustInherit 基类时,复杂性会再次升级。基类无法实例化,正如标准的集合编辑器会告诉您的那样:

糟糕的程序员!

由于基类无法实例化,需要将其从集合编辑器中排除。通常的解决方案是编写自己的集合编辑器并指定允许的类型。

在多次这样做之后,我决定开发一个 CollectionEditor 基类来自动检测并移除那些抽象类型。为了使其更具通用性,我还添加了其他一些周期性需要或想要的功能。最终得到的 EnhancedCollectionEditor 的功能包括:

  • 自动排除抽象基类
  • 用于调整集合窗体的属性
  • 能够处理各种强类型集合
  • 支持嵌套/子集合
  • 用于为新项提供唯一名称的命名服务
  • 能够在运行时调用 UITypeEditor

请注意,虽然过滤抽象基类是这个新编辑器的一个重要方面,但它适用于任何类集合,无论是否涉及继承。最重要的是,它非常易于使用,所以您可以仅仅因为想更改对话框窗体的标题就用它来代替标准的 .NET CollectionEditor

背景

在不提及序列化和类型转换器(TypeConverter)的情况下,几乎不可能解释如何实现一个 CollectionEditor。在集合的所有者上正确实现的东西可能比在一个新的集合编辑器中实现的还要多。由于本文更多是面向初级和中级技能水平的开发者,我不想只写一句“确保使用正确的 TypeConverter”而不解释这意味着什么或涉及什么。因此,在介绍新的集合编辑器之前,本文将回顾类型(类)、集合和设计器序列化的要求。

关于这个主题的一篇优秀文章来自 Daniel Zaharia:“如何使用 CollectionEditor 编辑和持久化集合”,该文对集合和序列化进行了概述——这是一篇必读文章。本文更新了那篇文章中的几点,澄清了其他一些问题,并扩展了与继承类相关的方面,更多地采用了实际应用而非概述的形式。最重要的是,本文的背景是 Visual Basic,以便更广泛的读者能够理解。建议您阅读 Zaharia 先生的文章,如果您还没读过的话。

集合项类

这个演示包含了几个类集,每个类集实现不同类型的继承或使用不同的集合类型进行存储。它们分别存储在各自的源文件中:XItemsZItemsXooItems。下面显示了 ExtendedItems 的概要。

    ' the basic data type 
    Public Enum ItemTypes
        TextType
        ValueType
        DateType
        FooleanType
    End Enum
            
   Public MustInherit ExtendedItem
        Public Property ItemType as ItemTypes
    
        Public Property Name As String
    
        Public Property Index As Integer 
    
        ' all inherited classes must have a type:
        Public Sub New(st as ItemTypes)
             Itemtype = st
        End Class
    
   End Class
    
   Public Class Textitem 
        Inherits ExtendedItem
    
        Private myType As ItemTypes = ItemTypes.TextType
    
        Public Property Text as String
                    
        Public Sub New()
            MyBase.New(myType)
            Name = "TextItem"
            Text = "TextItem Text"
        End Class
    
   End Class
    
   Public Class Valueitem: Inherits ExtendedItem  
    
   Public Class FooBarItem: Inherits ExtendedItem 

ExtendedItem 类集可能是最复杂的。它将控制 Index 属性,要求其是连续的,因此在编辑器中是只读的;后面的一个类集将要求项名称是唯一的。继承的 FooBarItem 本身承载了3个子集合:FoosBars 和一个 ZItems 的集合(Zoey、Zacky 类继承自 Ziggy)。

注释:

  • 如果您的类没有实现一个 Name 属性,那么类型名称将显示在 CollectionForm 上的 ListBox 中。上面的 ValueItem 将显示为“Plutonix.Test.ValueItem”。当您不需要 Name 时,一个替代方法是重写 ToString 方法以返回一些能识别此项的内容。如果这些都缺失,则使用类型名称。
  • 所有的 CollectionEditor 都需要一个简单的构造函数(一个没有参数的 Sub New()),因为编辑器不知道要传递什么值或使用哪一个构造函数。您可以添加其他构造函数来加快或简化创建过程。

选择一个集合

首先要考虑的是您希望如何存储这些项。主要考虑的应该是您正在开发的应用程序的需求,但正如 Zaharia 先生指出的,您的集合必须实现 IList,必须实现 Add 方法和一个 Item 属性(在 C# 中是索引器)。

正如您将看到的,必须支持 Item,因为 .NET 需要它来确定您的集合包含的类型(事实证明,我们也需要)。需要 Add 是为了让设计器(VS)能够反序列化(读取/重新加载)您的集合项。最常见的集合类型是:

  • Collection(Of T) (来自 System.Collections.ObjectModel)
  • List(Of T) (来自 System.Collections.Generic)
  • CollectionBase (来自 System.Collections)

List(Of T) 很特别:因为它是一个容器,所以它允许您像使用一个合适的集合一样使用一个 List(Of T) 变量,因为它满足了所有要求。这让您可以非常快速地实现一个集合类。缺点是它也允许将集合变量设置为 Nothing,并允许访问您可能不希望的各种方法。(本演示公开了5个顶级集合和几个子集合,其中两个简单的子集合为了方便使用了 List(Of T) 变量。ExtendedItems 集合则严格遵循了上述建议。)

当您的集合类继承自 Collection(Of T) 时,所需的 AddItem 成员就已经存在了(还有其他有用的方法,如 ContainsIndexOf)。当继承自 CollectionBase 时,您将需要自己添加这些成员。请务必指定返回类型并将 Item 实现为一个 Property —— 在您的代码中,它作为函数可以工作,但也会迷惑集合编辑器。

 

MSDN集合指南中提供了更多注意事项和其他建议,通常推荐使用 Collection(Of T)

最后,您的集合需要通过主类(在演示中是 NuControl)上的一个属性来公开:

   Public Class NuControl
        Inherits Panel
        Implements ISupportInitialize
    
        ' this is the collection class of Extended items...  
        ' it **must** be instanced for the Collection Editor
        Friend XTDItems As New ExtendedItems
    
        <DesignerSerializationVisibility(DesignerSerializationVisibility.Content)> 
        <Editor(GetType(CollectionEditor), 
                GetType(System.Drawing.Design.UITypeEditor))>
        Public ReadOnly Property ItemExtenders As ExtendedItems
            Get
                ' MS pretty regularly does this in their code
                ' see ListView Groups collection:
                If XTDItems Is Nothing Then
                    XTDItems = New ExtendedItems
                End If
                             
                Return XTDItems
            End Get
        End Property
    
        Private Sub ResetItemExtenders()
            XTDItems.Clear()
        End Sub
    
        Private Function ShouldSerializeItemExtenders() As Boolean
            Return (XTDItems.Count > 0) 
        End Function

注释

  • 集合绝对必须被实例化,这样编辑器才有地方存储您创建的新项。因为我们将在设计时向集合添加项,所以我们需要一个“设计实例”。
  • Editor 特性指定了用于编辑此属性的 UITypeEditor。目前我们使用的是标准的 .NET CollectionEditor
  • 用于放入集合的项的类必须实现一个简单的构造函数(一个没有参数的 Sub New()),因为集合编辑器不知道如何使用带参数的构造函数。
  • 还有其他一些可用的类型化集合,例如 ObservableList(Of T),但 VisualBasic.Collection 在其列:它不是类型化的,并且返回只读的 Object,这使得它不适用。
  • 要使用 Collection(Of T),请添加对 System.Collections.ObjectModel 的引用并导入它。这将允许 Visual Studio 在自动完成中提供它,并且不会将其与 VB Collection 混淆。

 

许多论坛上的消息指出,您绝不能为集合属性包含 setter,否则会出现序列化问题。这是不正确的。MSDN 有几个实现 setter 的例子真正的事实是,CollectionEditor 不会使用 setter 将新的集合返回给您,设计器也不会。实现 setter 也是一个坏主意,因为它会允许您的集合被设置为 Nothing。为避免这种情况,您可以将属性设为 ReadOnly,或者使用一个空的 Setter。

有时人们对这一点说得有点过头了,因为一个 ReadOnly 的集合属性只是防止您的集合对象被设置为 Nothing。您公开的集合类很可能也有 ClearRemove 项的方法。但这并非小事,因为移除项是一回事,而您的代码在每次引用集合前都必须测试集合是否为 Is Nothing 则是另一回事。

ShouldSerializeXXX 函数(其中 XXX 是集合属性的名称)是设计器(VS)用来确定属性何时发生变化并应被序列化的方式。当集合中有项时,ShouldSerialize 的返回值提供了保存集合内容的指示。使用 PrivateFriend 都可以,VS 会找到它们(Private 更合理)。此过程还控制您的集合在包含项时是否在属性窗口中以粗体显示。随着项目的发展,如果集合属性名称发生变化,请确保更新过程名称。

毫不奇怪,那些警告属性 setter 的论坛消息中的代码,很少实现 ResetXXXShouldSerializeXXX

 

序列化

我本想将序列化作为一个单独的主题——甚至是单独的文章来讨论,但您必须边做边实现它,否则您会从 Visual Studio 收到各种错误消息,说您是个糟糕的程序员。序列化——更准确地说是设计器序列化——指的是将集合数据保存到窗体设计器。对于 VB 窗体,这就是 (formname).designer.vb 文件。这与 XML 或其他序列化不同。在他的文章中,Zaharia 先生将其称为持久化集合。仔细研究设计器序列化可能有助于您理解这个过程并诊断序列化问题。

在不同的时候,VS 会将集合的内容写入设计器文件。您的 ShouldSerializeXXX 函数的返回值是触发将集合项序列化到同一个文件的扳机。CollectionEditor 不会通过属性 setter(如果存在的话)将集合传回给您的类,因为您的闲置类代码无法对其做任何事情。请记住,这一切都发生在设计时。

当您在编辑器中点击 Add 时,一个新项会被添加到您的集合的一个副本中。同时,VS 将窗体设计器标记为脏(dirty),但它不会逐个添加项。如果您取消集合编辑会话,将使用原始集合的副本。

为了实现持久化或设计器序列化,当您退出集合编辑器时,VS 会将代表集合新状态的代码添加到设计器文件,然后运行它,基本上是重建窗体和您的集合。设计器代码位于您在窗体的 Sub New 中看到的那个“神秘的” InitializeComponent 过程中。您在那里看到的代码就是用来反序列化您的窗体、相关控件以及最终您的集合的代码。

    Dim TextItem1 As NuControl.TextItem = New NuControl.TextItem()
    ...
    TextItem1.Index = 0
    TextItem1.ItemType = ItemTypes.TextType
    TextItem1.Name = "TextItem"
    TextItem1.Text = "FooBar"
    ...
    Me.NuControl.XTDRItems.Add(TextItem1) 

这就是 VS 如何向您的集合中添加项:它使用您集合的 Add 方法。

设计器代码在不同时间运行(清理、重新生成——这就是它会闪烁的原因)。在重新生成窗体和控件时,它也会重新生成您的集合——也就是反序列化它。这个过程可以很快地揭示序列化问题:如果您曾遇到过可以向集合中添加对象,并且它们在集合中保持可用,但在您重新生成或重新加载项目时却消失了,那是因为它们没有被序列化。这些项只在集合被重建之前保留在集合中。

注意 Add 语句。XTDRItems 只是一个属性,所以在属性上调用 Add 方法看起来可能很疯狂(或者像魔法)。在这种情况下,XTDRItems 只是一个底层类的属性包装器,该类确实实现了那个方法。重要的一点是,如果没有一个可用的 Add 方法,您的集合就无法被重建或反序列化。如果您的集合类实现了 AddRange,VS 将使用它来反序列化您的集合。我倾向于稍后再添加这个,以便更容易检查各个属性的设计器序列化代码。

序列化要求

在为您的集合和集合编辑器开发基础结构时,必须考虑序列化。当您退出 CollectionEditor 的那一刻,VS 就需要序列化其内容。这是我们需要使我们的集合类可序列化的内容:

  1. 如前所述,集合属性(来自演示和上文的 XTDRItems,使用 Editor 特性)必须具有 DesignerSerializationVisibility 特性,这次使用 Contents 设置。集合是一个 Object,所以它不能被序列化,但我们可以序列化集合的内容
  2. 集合属性还必须如前所述包含 ShouldSerializeXXXResetXXX
  3. 用于集合的项类必须用 Serializable 特性标记。对于从抽象类继承的类,您可以在基类上标记为 Serializable,代表所有继承的类型。这样做是有效的,因为序列化作用于类型:一个类型为 TextItem 的对象,例如,其类型也将是 ExtendedItem。或者,您也可以标记每个类。
    • 这将消除“<TypeName> 未标记为可序列化”的错误。
  4. 由于集合本身无法序列化,这是通过项类来处理的。这几乎肯定需要一个 TypeConverter(稍后介绍)。
  5. 项类的属性必须用 DesignerSerializationVisibility 特性标记,使用 VisibleHidden,具体取决于它是否被 TypeConverter 使用。
示例
' alternatively, mark the ExtendedItem base class (and so, all inherited types)
<Serializable>
Public Class TextItem
    Inherits ExtendedItem

    Private myType As ItemTypes = ItemTypes.TextType

    <DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)>
    Public Property Text As String

    <DefaultValue("")>
    Public Property MoreText As String

    Public Sub New()
        MyBase.New(myType)
        Name = "TextItem"
        Text = "TextItem Text"
    End Sub

End Class

上面显示的 DefaultValue 特性所做的可能和您想的不一样。它不是提供一个默认值,而是像 ShouldSerializeXXX 一样工作,通知设计器在属性值与指定的默认值不同时序列化该属性。它还会将集合编辑器的 PropertyGrid 显示为粗体,当当前值与默认值不匹配时。如果不使用该特性,VS 设计器似乎会假定文本为 String.EmptyInteger 为 0 等。


对于除了非常简单的类之外的所有类,.NET 要求被序列化的项提供为设计器文件编写代码所需的信息。这是通过使用 TypeConverter 来完成的,但既然我们已经深入到设计器文件中,这里有一个您可能想知道的辅助工具。

值得注意的是,设计器代码会按照 CollectionEditor 中指定的顺序将项添加到您的集合中。您可能已经注意到,对于 DataGridView,当添加列时,每个索引最初都是零。只有在您保存并重新访问该 CollectionEditor 后,索引才会表示正确的位置。这是在集合的 Add 方法中完成的(请参阅 MS 引用源中的第155-161行):

    int index = this.items.Add(dataGridViewColumn);
    dataGridViewColumn.IndexInternal = index;  

我们注意到我们希望为 ExtendedItems 集合类中项的 Index 属性提供此功能。由于用户无法控制 Index,它使用了 ReadOnly 特性。好吧,控制 Index 使其连续并没有什么魔力,但我们如何执行更复杂的检查或操作呢?

ISupportInitialize - 方便的辅助工具

这是一个非常、非常有用的接口,用于设计或子类化控件。在您需要根据另一个属性设置来限定或验证一个属性设置的情况下,Sub New 并不是做这件事的地方,因为用户设计时的属性值尚未设置。您偶尔会看到的一个“技巧”是在 HandleCreated 事件中执行初始化。

ISupportInitialize 提供了一个更好的解决方案。实现后,设计器将在设置任何属性之前调用您类上的新 BeginInit 方法,然后在设置所有属性之后调用一个新的 EndInit 方法。因此,在您必须根据另一个属性来限定一个属性的情况下,EndInit 是执行此操作的绝佳位置——Visual Studio 已经完成了对所有内容的属性设置,所以您不必担心其中一个会是 Nothing。设计器代码展示了这是如何工作的:

    CType(Me.NuControl1, System.ComponentModel.ISupportInitialize).BeginInit()
    Me.SuspendLayout()
    ...
    Me.NuControl1.Name = "NuControl1"
    ...
    Me.NuControl.ItemExtenders.Add(TextItem1)
    ...
    CType(Me.NuControl1, System.ComponentModel.ISupportInitialize).EndInit() 

EndInit 在运行时和设计时都会被调用。这是我们可以设置集合中项的 Index 的另一个地方(但似乎在它甚至集合中之前,在 Add 方法中设置更好)。然而,其他要求可以从 EndInit 中大大受益。考虑一个具有相互依赖引用的集合,其中 ItemA 通过索引引用指示与 ItemB 的关系。EndInit 是一个很好的地方,例如通过名称查找 ItemB,然后检查或设置一个索引值,因为当该方法被调用时,完整的集合是可用的。

要实现 ISupportInitialize

    Partial Public Class NuControl
        Inherits Panel
        Implements ISupportInitialize 

在较新的 VS 版本中,在 ISupportInitialize 之后按回车键会添加两个必需的方法:BeginInitEndInit

您可以通过多种方式将此功能扩展到其他类。您可以在其他类上添加类似的方法,这些方法从 NuControl.EndInit 调用以执行类似的操作。该接口的扩展版本 ISupportInitializeNotification 允许通过一个新事件通知其他组件。

这次对 ISupportInitialize 的旁征博引的重点是,许多您可能觉得需要在集合编辑器中做的事情——比如强制一个 Index 属性是连续的——一旦集合——以及其他所有东西——都初始化完毕,处理起来会更好、更容易。


至此,Visual Studio 知道我们序列化什么。接下来,我们需要告诉它我们如何想要序列化它。如果我们不这样做,VS 会尽力而为,并且很可能会失败。序列化您的类项的最简单方法,可能是让每个集合项都继承自 Component,或者实现 IComponent

    Public Class Foobar
        Implments IComponent  

这非常简单,但它也带来了开销:VS 将需要支持 IDispose 的代码(您可能不需要),并且它还会拖累像 ISiteGenerateMember 这样的东西。您的类项默认也会显示在窗体托盘中。另一方面,它将强制一个唯一的名称并为您处理设计器序列化。如果您想暂时避免学习 TypeConverter,您可以这样做。请参考 Zaharia 先生的文章和演示,其中他的一个集合就是这样做的。

我们其他人将在这里学习,一个最小的 TypeConverter 毕竟不是什么高深的魔法。

 

TypeConverter

TypeConverter无处不在。它们可以将属性值“Red”转换为Color.Red;或者将 X 和 Y 值转换为您窗体的 Location。有些是自动的:XItems 中的 ValueItem 包含一个 Enum 属性,VS 会将其转换为在属性窗口的下拉列表中使用 Enum名称。该演示实现了一个 EnumConverter 来展示如何为下拉列表提供更友好的文本(请参阅 XItems 中 ValueItem 的 ValueEnumConverter)。

MSDN 的示例 TypeConverter 项目非常全面,但让它们看起来比实际更复杂和令人生畏(当然也比我们需要的更复杂)。即使是前面提到的文章也比我们需要的稍微雄心勃勃了一些。

在这里,我们将以循序渐进的方式介绍实现 TypeConverter 所需的最低限度。这是一个需要掌握的重要概念,因为在本文结束时,您将拥有一个易于使用的 EnhancedCollectionEditor,但为了在项目中使用它,您需要知道如何为集合类项添加一个 TypeConverter

回想一下,我们用 DesignerSerialization.Contents 来修饰集合属性。基本上,我们告诉 VS 不要去管集合对象本身,但关心它里面的东西。做了这件事之后,我们现在必须通过 TypeConverter 来提供序列化内部对象的方法。您的 TypeConverter 将帮助 Visual Studio 为设计器文件生成以下内容(前两行):

     Dim Ziggy1 As NuControl.Ziggy = New NuControl.Ziggy("NewZiggy", -1, "ZiggyName")
     Dim Zoey1 As NuControl.Zoey = New NuControl.Zoey("NewZoey", 7)
     ...
     ' Typeconverters dont do this part, but we control it:
     Ziggy1.PropVal = 0
     Ziggy1.ZFoo = "Zig's Foo"
     Zoey1.PropVal = 7
     Zoey1.ZBar = "Zoey Bar"
     Me.NuControl1.ZItemExtenders.Add(Ziggy1)
     Me.NuControl1.ZItemExtenders.Add(Zoey1) 

这其中有一个相当巧妙的地方。当需要序列化代码时,每个集合项的实例都会被轮询,以便为 VS 提供重新创建该项所需的信息。也就是说,VS 基本上是在问:“我该如何按照你现在的样子重新创建你?” 你编写的 TypeConverter 会用构造函数信息和任何构造参数的实际来回应。

这些是通过 TypeConverter 特性与类(类型)关联的。

    <Serializable, TypeConverter(GetType(ZoeConverter))>
    Public Class Zoey
        Inherits ZItem  

在上面的设计器代码中,请注意 ZiggyZoey 各自使用了不同的构造函数。演示中的每个 ZItem 都使用不同的构造函数来提供不同的 TypeConverter 示例。Attribute 通常是不会被继承的,但与 Serializable 特性一样,TypeConverter 适用于一个类型。因此,与基类关联的任何 TypeConverter 都将应用于继承的类(同样,因为一个类型为 Zoey 的项同时也是类型为 ZItem)。ZItem 类没有这样做,因为它们每个(人为地)都使用不同的构造函数,所以它们需要不同的 TypeConverter

请注意,我们可以通过名称指定要使用的 TypeConverter<TypeConverter("ZoeyConverter")>,但在这样做时,如果名称拼写错误,VS 无法像指定了错误的类型时那样通知我们。

一个最小的 TypeConverter 只需要实现两个方法:CanConvertToConvertTo。第一个方法只是在 VS 查询对象看它是否能将当前项转换为特定类型(如 String 等)时返回 True——在这种情况下,我们需要提供一个 InstanceDescriptorConvertTo 看起来可能很复杂,因为它通过反射使用不寻常的术语,但关键只在于几行代码:

 Friend Class ZoeConverter
    Inherits TypeConverter
    
    ' CanConvertTo omitted for brevity
    
    Public Overrides Function ConvertTo(context As ITypeDescriptorContext, 
                    info As CultureInfo, Value As Object, 
                    destType As Type) As Object
    
            If destType = GetType(InstanceDescriptor) Then
                ' convert value (Object) to correct type
                Dim z As Zoey = CType(value, Zoey)

                ' declare a ctor info variable
                Dim ctor As Reflection.ConstructorInfo

                ' get the ctor info matching the sig desired
                ctor = GetType(Zoey).GetConstructor(New Type() 
                                    {GetType(String), GetType(Integer)})
                
                ' create inst descriptor using the ctorInfo and instance values
                Return New InstanceDescriptor(ctor,
                            New Object() {z.Name, z.ZCount}, False))
           End If
           Return MyBase.ConvertTo(context, info, value, destType)
     End Function
 End Class 

分步指南

假设这是您的第一个 TypeConverter 之一,让我们一步一步来:正在为序列化而转换的是您集合中的一个项,其 Name 为 "NewZoey",Index 值为 7(请参见上面的 VS 设计器代码)。由于它是一个 Zoey 对象,因此使用了上面的 ZoeConverter。当被问及是否可以为 Zoey 对象提供一个 InstanceDescriptor 时,转换器已经返回了 True,现在它必须实际这样做——希望能是正确的。

  • 传入 ConvertToValue 参数是正在被转换/序列化的对象(Zoey 的实例),所以使用 CType 将其转换为正确的 Type。这将在稍后用于提供构造函数的值。
  • GetConstructor 创建一个 ConstructorInfo 对象,该对象与我们正在处理的类型(Zoey)所需的签名相匹配。
    • 签名指的是构造函数中数据类型参数顺序
    • 代码通过传递一个包含所需数据类型顺序的 Type 数组来指定使用哪个构造函数。
    • 任何 CollectionEditor 都需要一个简单的(无参数)构造函数来创建新项,您可能在代码中使用一个,并且通常会专门为您的 TypeConverter 添加一个构造函数。因此,您通常会有多个构造函数可供选择,请确保指定正确的类型和参数类型顺序。
    • 在这种情况下,Type 数组包含 StringInteger 类型,所以用 VB 的术语来说,代码请求的是这个构造函数:
       Sub New(Name As String, Index As Integer)  
  • 转换器准备使用我们刚刚创建的构造函数描述符返回一个 InstanceDescriptor
  • 接下来,它使用一个对象数组,填充了实际的参数值,这些值我们从传入的实例对象(并转换为正确类型)中获取。在这种情况下,是 z.Name ("NewZoey") 和 z.Index (7)。这些值是最近通过 CollectionEditor 输入的。
  • 最后一个 Boolean 参数表示这个对象(集合项)是否是完整的。接下来将介绍这一点。

VS 设计器会将您给它的内容转换为文本,以得到您在设计器文件中看到的结果(如果没有您的 TypeConverter,这是不可能正确的):

   Dim Zoey1 As NuControl.Zoey = New NuControl.Zoey("NewZoey", 7) 

当您熟悉这个过程后,可以简化代码。

   If destType = GetType(InstanceDescriptor) Then
        Dim z As Zoey = CType(value, Zoey)

        Return New InstanceDescriptor(GetType(Zoey). _
                            GetConstructor(New Type() {GetType(String),
                                           GetType(Integer)}),
                                           New Object() {z.Name, z.ZCount}, False)
   End If 

在 .NET Framework 中有许多类型的 TypeConverter(例如提到的 EnumConverter),它们非常有用。值得一提的是 ExpandableObjectConverter。这些用于具有多个值的属性——例如 Point(x 和 y 值)或 Size(宽度和高度值)。您可以使用一个继承自 ExpandableObjectConverter 的转换器,在属性窗口中展开您自己的对象。

另一个学习 TypeConverter 的有趣来源是 MS .NET 参考源。这个链接是 ListViewColumnsHeaderCollection TypeConverter

处理其他属性

但是,如果一个类有 8 或 10 个属性怎么办?您在第一次研究 Zaharia 先生的文章时可能会忽略这一点,但您不需要创建一个复杂的构造函数并通过您的 TypeConverter 处理所有的类属性。TypeConverter 应该使用尽可能简单的构造函数,提供类在创建或实例化时必须知道的参数。如果只有 Name 属性是必需的,那么就使用一个参数的构造函数,让其余的属性在设计器中作为普通属性设置。

但是,“普通”方式是什么,我们该如何做到呢?首先,如果除了构造函数中处理的属性外还有其他属性要设置,请将上面 InstanceDescriptor 的最后一个参数设置为 False。这会告诉 VS 该对象完整,因此,VS 会去寻找其他标记为 DesignerSerializationVisibility.Visible 的属性。将构造函数中使用的任何属性设置为 Hidden,因为那些已经处理过了('Hidden' 实际上意味着什么都不做)。

     ' handled by the TypeConverter in the ctor
     <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
     Public Property Name As String

     <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
     Public Property Index As Integer

     ' Serialize "normally":
     <DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)>
     Public Property ZBar As String

     <DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)>
     Public Property ZFoo As String 

TypeConverter是类特定的,不具备很强的可重用性。但是,对于使用无参构造函数的类,转换器可以是相同的。所有的 XItem 类都使用一个只需要名称的构造函数。由于它们都继承自 ExtendedItem,它们可以共享同一个 TypeConverter,只需做一些简单的修改:

  Return New InstanceDescriptor(value.GetType. _
                      GetConstructor(New Type() {GetType(String)}),
                      New Object() {CType(value, ExtendedItem).Name}, False) 

使用 value.GetType 来代替 GetType(Zoey) 作为 GetConstructor 的参数,可以解析回正在创建的实际类型。同样地,将 Value 转换为 ExtendedItem(在 Option Strict 下是必需的)以获取 Name 属性值是可行的,因为它是在基类中定义的,因此所有继承的类都存在该属性。但对于继承类特有的属性,这种方法就不起作用了。

 

如果您搞砸了 TypeConverter,VS 会尽力而为。有时它会将集合项转换为 Base64 字符串并存储在资源文件中,然后在尝试将其反序列化为正确的类对象时失败。如果您深入研究错误,它会指向 resx 文件(或属性 | 资源)中的一个 <Data> 条目。其他时候,它可能会将一个普通字符串发布到资源文件,但仍然无法反序列化。您的设计器文件将包含如下行:

    Dim resources As System.ComponentModel.ComponentResourceManager = New 
        System.ComponentModel.ComponentResourceManager(GetType(Form1))
    ' ...
    ' and later:
    Me.NuControl1.ZCollectionBase.Add(CType(resources._
        GetObject("NuControl1.ZCollectionBase"), NuControl.ZItem)) 

这种情况发生的频率刚好让人感觉好像你不需要为项目类提供 TypeConverter。但最终它会失败,通常只有在你将项目重新加载到 VS 之后才会发生。

如果您开始遇到奇怪的 CodeDom 序列化异常:停下来。现在就找出您的序列化过程中出了什么问题(如果任其发展,情况会变得非常糟糕,以至于您可能被迫重新创建项目和解决方案文件)。这种情况可能在您的 CollectionEditor 开始做一些有用的事情时就发生,所以看起来问题可能出在编辑器上,并且很想去修改——呃,我是说调试——CollectionEditor 的代码。但真正的问题更有可能出在您的序列化方法上。

这旨在概述 TypeConverter 在序列化您的集合项中的作用,以及如何通过检查设计器文件来检测问题——而不是详细介绍设计器序列化。例如,CollectionEditor 每次您添加一个项时也会使用您的 TypeConverter——在 ConvertTo 过程中添加 Console.Beep,看看它被调用了多少次。您也可以在 EndInit 中添加一个蜂鸣声,看看窗体和您的集合被反序列化和重建的频率和时间。

以上关键要素已在附录中以清单形式呈现。

 

测试您已有的成果

我们似乎已经拥有了所需的一切:集合类 - ExtendedItems - 已经实例化并为序列化进行了修饰;它将包含的类已经编写并为序列化进行了修饰;集合类也为 Editor 进行了修饰,并且我们有了 TypeConverter。让我们看看 VS/.NET/VB 到目前为止是否认可。

回想一下,我将属性设置为使用标准的 .NET 集合编辑器:

   <DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
   <Editor(GetType(CollectionEditor), 
           GetType(System.Drawing.Design.UITypeEditor))>
   Public Property XTDRItems As ExtendedItems 

这样做有两个原因:主要是,如果标准的 .NET CollectionEditor 工作不正常,那么毫无疑问是集合相关的代码出了问题,而不是任何自定义集合编辑器的问题。反之,如果我们在开始开发 UITypeEditor 后事情开始失败,我们就会知道问题出在那里。

编译您的项目,如果需要,在您的窗体上拖放一个 NuControl 的实例,交叉手指,然后打开属性窗口。XTDRItems 属性应该看起来像这样:

 

(Collection) 部分表示 VS/NET 识别 XTDRItems 为一个有效的集合,而 Ellipses (...) 表示 VS/NET 知道它可以被编辑。如果不是这样,很可能是 IDE 无法访问集合的实例,或者集合类的实现不正确。

如果您遇到问题,缩小问题范围的一种方法是更改属性以公开集合的内部列表(例如 Items)。在进行所有此类更改后重新编译,然后检查属性窗口中的条目。如果现在显示了,您就知道您的集合类中存在问题。请务必将其改回,以保护您的集合不被代码之外的地方替换或清除。

同时检查属性的“As Type”,但如果那错了,说明您没有使用 Option Strict,活该去浪费一个小时找茬。

 

ControlDesigner / DesignerActionList

如果您正在通过 DesignerActionList(控件本身右上角的那些下拉面板)实现对集合的访问,还有一步要做。面板上呈现的属性和操作本质上是穿透(pass-through)的属性和方法:

Friend Class SomeControlActionList    
    Private thisCtl As SomeControl

    Public Property Items() As StartItems
        Get
            Return thisCtl.StartItems
        End Get
        Set(ByVal value As StartItems)

        End Set
    End Property
     ...

End Class

.NET 会调用您的 ActionList 类上的属性和方法,而该类又会使用您的类对象的实例来获取值。如果您的类使用自定义集合编辑器,并且您想——或者需要——通过智能标记来使用它,您也必须像在您的类上那样修饰这个属性声明:

    <Editor(GetType(StartItemEditor), GetType(System.Drawing.Design.UITypeEditor))>
    Public Property Items() As StartItems
     ...

DesignerSerializationVisibility 是不需要的,因为我们不是为这个对象保存数据,但对于任何自定义的 CollectionEditor,您将需要 Editor 特性。您可以引用您实际类属性上使用的同一个特性。

另一个选择是创建一个 DesignerActionListMethod,这似乎是微软为各种“编辑项...”条目所做的方式。

 

 

第二部分:集合编辑器

新的 EnhancedCollectionEditor 继承自 .NET 的 CollectionEditor,然后通过属性选项添加了附加功能。这里不会深入探讨新编辑器的 DNA,但我们将研究的有趣或主要方面是:

  1. 确定集合类型
  2. 确定它可以包含的类型
  3. 过滤掉任何抽象基类
  4. 重写一些标准函数
  5. 调整 CollectionForm

获取包含的类型

第一步是获取在构造函数中传入的类集合 Type,然后是它包含的类型:

  Public Sub New(t As Type)
        MyBase.New(t)

        ' somebody, sometime might have need to
        ' do something somewhere with the original Type
        myType = t

        ' ought not throw exceptions 
        If MyBase.CollectionType Is GetType(Microsoft.VisualBasic.Collection) Then
            DisplayError("VB Collection is not supported.")                     
            Exit Sub
        End If

        ' get actual type in the collection
        mycolBaseType = MyBase.CollectionItemType

        ' check if it is Object, it may be poorly typed or constructed
        If mycolBaseType Is GetType(Object) Then
            mycolBaseType = GetCollectionItemType(t)
        End If

        ' this works only for List(of T) and not even for Collection(Of T)
        ' mycolBaseType = t.GetGenericArguments(0)

        If (mycolBaseType Is Nothing) Then
            DisplayError("Underlying Type must implement 'Item' as a Property!")
            DisplayError(
                String.Format("Underlying Type [{0}] must implement 'Item' as a PROPERTY." _
                              & "{1}A NullReferenceException will result trying to use [{2}]",
                                 myType.ToString, Environment.NewLine,
                                 Me.GetType.Name))
            ' you have been warned
            Exit Sub
        End If
        If (mycolBaseType Is GetType(Object)) Then
            DisplayError("Type [System.Object] is not supported.")
            Exit Sub
        End If

    End Sub 
  • 传入的类型 T 是您属性 getter 返回的任何类型,在本例中是 ExtendedItems 类。请注意,这是集合不是项的集合。
  • 这里特别查找并拒绝了 VB Collection 类型,因为它会以 Object 的形式传入,并且无法确定其包含的类型(它们也是 Object)。
  • NuControl 在演示中包含了5个顶级集合。如果其中任何一个在构造函数中抛出异常,它们可能都会停止工作,所以使用了基于 MessageBox 的错误显示。
  • 有几种方法可以获取集合中包含的 Type。最简单的是使用 CollectionItemType 属性。这将返回基类类型,例如本例中的 ExtendedItemCreateCollectionItemType 函数类似:它在属性中搜索索引器(Item 属性)以返回类型;而 CollectionItemType 返回一个缓存的值(请参阅 MS 参考源第 213 - 220 行 vs 2328 - 2338 行)。
  • VB 通常对您自己的利益过于宽容,尤其是在关闭 Option Strict 的情况下。如果您从属性中省略了 As <Type>,它将是一个 Object 类型,这会让集合编辑器很不爽。所以也有针对这种情况的陷阱。

回想一下,我说过可以将您的 Item 访问器定义为一个函数,并在您的代码中正常工作。当使用 Collection(Of T) 时,基类实现了一个 Item 属性,因此可以识别正确的类型。但是,对于使用 CollectionBase 并将 Item 实现为方法的集合类,两种 .NET 获取包含类型的方法都会失败,因为没有索引器(Item)属性。因此,如果所有其他方法都失败了,提供了 GetCollectionItemType 过程来检查 Item 属性方法,在您将其用作方法时提醒您。

优化类型列表

接下来,我们想要创建一个包含所有合法类型的列表——TextItemValueItem 等——这些类型是集合可以包含的,但我们必须过滤掉任何抽象基类。这是通过重写 CreateNewItemTypes 函数来完成的。

“常规”的做法是简单地告诉 CollectionEditor 合法的类型,通过返回一个此编辑器可以处理的类型(类)数组。但这丝毫不可重用,因为每个集合的类型都会不同。此外,俗话说,“当你可以花两个小时编写一个系统来为你做事时,为什么只花 15 分钟编写代码来做这件事呢?” 核心代码是:

 Protected Overrides Function CreateNewItemTypes() As Type()
     Dim ValidTypes As New List(Of Type)

     If mycolBaseType.IsAbstract Then

         ' start with all Types contained in the assembly mycolBaseType came from
         ' this may not be the same as Executing Assembly if the UIEditor is in a DLL:
         Dim allTypes As Type() = Assembly.GetAssembly(mycolBaseType).GetTypes()

         ' go thru all the types returned
          For Each t As Type In allTypes

              ' if this Type derives from mycolBaseType and itself is 
              ' not Abstract/MustInherit, it can go in the list
              If t.IsSubclassOf(mycolBaseType) And (t.IsAbstract = False) Then
                  ' test Nothing is in case someone thinks it is a good
                  ' idea to set it to null it out
                  If (ExcludedTypes IsNot Nothing) AndAlso
                             (ExcludedTypes.Contains(t) = False) Then
                      ValidTypes.Add(t)
                  End If

               End If
          Next
          ' return array to NET
          Return ValidTypes.ToArray()

      Else
          ' do nothing special - the baseType is not an abstract class
          Return MyBase.CreateNewItemTypes
      End If

 End Function 

代码注释应该能清楚地说明我们是如何过滤掉基类的:所有继承自 mycolBaseType 并且本身不是抽象类的都是可以的。它还允许所有具体类型通过,因此最终的编辑器可以在没有使用抽象类的情况下使用。System.Type 类有一套丰富的过程,这使得操作变得容易。返回的数组只包含基类 CollectionEditor 将使用并附加到 Add 按钮的类型:

 

搞定!——没有 ExtendedItem 基类,也没有责备。

单项选择

如果您想增加一些花样,建议您使用 VS 对象浏览器 来探索 .NET CollectionEditor 类暴露的各种方法和属性。例如,您可以非常简单地防止用户在 ListBox 中选择多个项:

    Protected Overrides Function CanSelectMultipleInstances() As Boolean
        Return False
    End Function  

修改 CollectionForm

在一个项目中,我花时间为属性添加了 Description 特性,以便在 PropertyGrid 的帮助面板中显示——然后发现该面板默认是关闭的。EnhancedCollectionEditor 将允许您通过一个属性来切换它。这是通过获取对 propertyBrowser 控件的引用并切换该属性来实现的。

 Protected Overrides Function CreateCollectionForm() As CollectionEditor.CollectionForm
     Dim EditorForm As CollectionForm = MyBase.CreateCollectionForm

     _propG = CType(EditorForm.Controls("overArchingTableLayoutPanel").Controls("propertyBrowser"), 
                            PropertyGrid)

     If _propG IsNot Nothing Then
         ' ShowPropGrid is a property we expose for this purpose
         _propG.HelpVisible = ShowPropGridHelp
     End If

     ' not needed, form is sizable...just proving you can change it
     EditorForm.Height += 40

     ' set the form title
     EditorForm.Text = FormCaption

     ' return form ref to work with
     Return EditorForm

 End Function 

一旦您了解了窗体的布局,这就非常简单了:请参阅 MS 源代码参考第 1233 行左右

TableLayoutPanel 是添加到窗体的第一个控件,所以您可以使用像 EditorForm.Controls(0).Controls(5) 这样的引用来获取 propertyBrowser 的引用,但是魔法数字让我感到不安。EnhancedCollectionEditor 将允许您获取窗体上任何主要控件的引用(按名称)。演示中包含一个示例;这些名称作为常量公开,并在 IntelliSense 中可见。

请注意,当您的集合项被添加到 ListBox 中时,它们被包装在一个内部的 ListItem 类中(参见第 902-904 行),因此尝试通过引用它来“帮助”编辑值可能比简单地挂接到 PropertyGrid 的值更改事件更困难。EnhancedCollectionEditor 也为您提供了订阅 propertyBrowser.PropertyValueChanged 事件的方法。

 

使用 EnhancedCollectionEditor

最终的结果是一个抽象/MustInherit 基类,它具有多个特性和选项,使其易于使用。而且它将同时处理抽象类和具体类。在我们看更高级的功能之前,这里是如何使用它:

  • 创建一个继承自新基类的类。
  • 如果您正在从 DLL 中使用基础编辑器,请确保添加引用并导入。
  • 在构造函数中设置属性以激活任何期望的行为:
     Public Class ZItemCollectionEditor
        Inherits EnhancedCollectionEditor

        Public Sub New(t As Type)
            MyBase.New(t)

            MyBase.FormCaption = "General ZItem Collection Editor"
            MyBase.ShowPropGridHelp = True
            MyBase.AllowMultipleSelect = True
            
        End Sub

    End Class 

唯一要做的另一件事就是将其指定为您的集合的编辑器。

    <Editor(GetType(ZItemCollectionEditor), GetType(System.Drawing.Design.UITypeEditor))>
    Public ReadOnly Property ZItemCollection As ZItems

 

这确实是创建一个自定义编辑器所需要的全部。演示中使用的所有集合编辑器都包含在 CollectionEds.vb 中。但它还能做更多……

 

属性

FormCaption - 显示在 CollectionEditor 窗体标题栏上的文本。

ShowPropGridHelp - 一个布尔值,决定是否显示属性网格的帮助面板。

AllowMultipleSelect - 是否可以在编辑器的 ListBox 中选择多个项。

UsePropGridChangeEvent - 当设置为 true 时,当属性值在编辑器的 PropertyGrid 中改变时,会触发一个 PropertyValueChanged 事件(详见下文)。

GetControlByName - 提供对 CollectionForm 上某个控件的引用,允许您附加事件处理程序以执行扩展操作。控件名称在基础编辑器类中定义,这使得 Visual Studio 和 IntelliSense 可以提供帮助。这些名称是:listboxdownButtonupButtonokButtoncancelButtonaddButtonremoveButtonpropertyBrowser。要获取您的编辑器类,需要订阅 EditorFormCreated 事件 - 请参阅 XTDItemCollectionEditor 中的示例。

NameService - 决定您希望实现的项命名服务,以确保名称唯一(详见下文)。

ExcludedTypes - 您希望从 EnhancedCollectionEditor 中排除的 Type(见下文)。

DisplayError - 一个简单的 MessageBox 包装器,您可以用它来调试和测试您的集合编辑器类。

BaseCollectionType (ReadOnly) - 返回传递给编辑器的类型(即集合 - ExtendedItemsZItems 等)。您的编辑器包装器应该知道它正在处理什么类型,但由于一个编辑器可以处理多种类型,这可能有助于确定此实例的类型。

BaseItemType (ReadOnly) - 返回集合项的类型:ExtendedItemZItemXoobar 等。这可以是一个抽象类型。

注意:BaseCollectionTypeBaseItemType 的价值很小。如果您试图为两种类型定义一个编辑器,并尝试使用它们来实现条件逻辑,它可能不会按预期工作。更好的做法是为每种类型定义一个单独的编辑器。包含它们是为了完整性,也因为某人某地可能会有半合法的需求来了解这些信息。

UsePropGridChangeEvent

当设置为 true 时,EnhancedCollectionEditor 将转发编辑器窗体上 propertyBrowser 控件的 PropertyValueChanged 事件。这可以防止您必须找到它并直接挂接它,以便在属性被编辑时访问您的项。

  Public Sub New(t As Type)
      MyBase.New(t)

      MyBase.FormCaption = "Extended Item Collection Editor"
      MyBase.ShowPropGridHelp = True
      MyBase.AllowMultipleSelect = False

      MyBase.UsePropGridChangeEvent = True
      AddHandler MyBase.PropertyValueChanged, AddressOf mypropG_PropertyValueChanged

  End Sub

  Private Sub mypropG_PropertyValueChanged(sender As Object,
                                           e As PropertyValueChangedEventArgs)
       ' your code here

  End Sub 

请注意,您可能希望在这里做的许多事情,在 ISupportInitializeEndInit 过程中可能会更容易完成。您不仅对集合信息的访问非常有限,而且您可能更改/设置的许多东西,用户都可以重新编辑。

ExcludedTypes

这允许您进一步细化可以添加到集合中的类型。假设在使用 {Ziggy, Zoey, Zacky} 类集的情况下,Ziggy 是一个合法的成员,但可能只在运行时某些所需信息可用时才是。或者,也许 Ziggy 类型由于某种原因不能被定义为抽象类,但它充当了一个抽象类,因此不应被实例化。

由于 Ziggy 是集合的合法 Type,只是尚未或在这种情况下不合法,当使用继承时,该类型在编辑器中是可用的。在这种边缘情况下,任何类型都可以从编辑器中排除:

  Public Sub New(t As Type)
      MyBase.New(t)

      MyBase.FormCaption = "Ziggy-less ZItem Collection Editor"
      MyBase.ShowPropGridHelp = True
      MyBase.AllowMultipleSelect = False
      ' pretend Ziggy is a special class which cannot
      ' be a part of the nested version
      MyBase.ExcludedTypes.Add(GetType(Ziggy))
 End Sub 

结果是一个没有 Ziggy 的 ZItem 编辑器。

ExcludedTypes 是一个 List(of Type),您可以向其中添加要忽略的类型,但这可能是一个相当边缘/小众的情况(除非您真的需要它)。

 

NameService

新的 EnhancedCollectionEditor 类还为项的唯一命名提供了支持。这对于组件通常比集合更重要,但由于 Name 属性在集合项上很常见,它们通常确实具有一定的重要性。这应该很明显,但使用此功能要求存在一个非只读的 Name 属性;如果不存在,编辑器会提醒您。

虽然 NameService 的部分内容基于与前面提到的文章中介绍的 ISupportUniqueName 相同的前提,但这种实现方式更为经济,因为它不需要每个新项都拥有集合的整个副本。

EnhancedCollectionEditor 基类提供了几种创建新名称的方法,使用 NameService 属性:

None - 不执行任何与名称相关的操作。(默认)

Automatic - EnhancedCollectionEditor 会根据 TypeName 和当前设计器宿主集合的计数自动创建一个新名称。例如,对于 Plutonix.Test.XooBar,如果已经创建了 3 个项,下一个将是“XooBar4”。这是最简单的,因为您只需要在您的集合编辑器中设置该属性。

NameProvider - 当您希望您的代码提供名称时,请使用此值;您需要在提供集合属性的类上实现 INameProvider(在 UIDesign.DLL 中公开的接口)。当创建新项时,集合编辑器将调用所需的 GetNewName 方法来获取新名称。

 

NameProvider 方法可以通过两种方式实现。请考虑演示中的这个类结构:

  • NuControl 提供一个名为 XooBars 的集合属性。
  • XooBars 包含具有唯一名称的 XooBar 项。
  • 每个 XooBar 项可以包含一个 ZItems 的(子)集合属性,也具有唯一的名称。

当您在编辑器中添加一个新的 XooItem 时,会首先查询 XooBars 集合类,看它是否实现了 INameProvider。如果实现了,就会调用所需的 GetNewName 方法来获取名称。由于集合将是新项的“所有者”,它最了解集合的当前状态,所以会首先轮询它来提供新名称。

如果 XooBars 没有实现 INameProvider,那么会查询 NuControl是否实现了。这为多种原因提供了第二次机会。首先,并非所有集合都能实现接口(例如 List(Of T) 变量,它们非常方便)。此外,由于 NuControl 托管了5个(到目前为止)集合;演示可以在 NuControl 上实现 INameProvider,而不是在每个类上都实现,然后简单地将调用分派给其他类的方法来获取新名称。

对于 ZItem 子集合的情况,首先会查询集合类,然后查询集合编辑器中活动的 XooBar 项。ZItem 实际上使用自动命名,但代码中也包含了 NameProvider 的实现:只需在集合编辑器中更改属性设置即可。

注释

  • NameService 在您的编辑器类中指定,因此它可以因集合而异。在演示中,XooBars 被设置为对新的 XooBar 项使用 INameProvider,而 ZItem 子集合则使用自动命名。
  • XooBar 的构造也模仿了 IComponent 的风格,将 Name 放在括号中,并在属性编辑器中设置为只读,以作说明。(在 ZItems 上,Name 属性不是只读的,因为正如您所见,该类在演示中经常用于子集合。)
  • 自动模式下,名称被设计为唯一的,而不是连续的。对于 INameProvider,您的代码提供规则和逻辑。
  • INameProviderUIDesign Namespace 的一部分。
  • .NET 中有一个 INameCreationSerice 接口,但这似乎更适用于 Components

 

事件

EnhancedCollectionEditor 包含几个有用的事件

EditorFormCreated

当创建 CollectionForm 时会引发此事件。如果需要向窗体上的控件添加事件处理程序,可以在这里进行。在此事件之前,窗体及其控件都还不存在。你还可以对窗体进行其他更改,例如设置 BackColor。你不应该存储对该窗体的引用。

   Event EditorFormCreated(sender As Object, e As EditorCreatedEventArgs)

编辑器窗体引用可通过 e.EditorForm 获取

 

NewItemCreated

    Event NewItemCreated(sender As Object, e As NewItemCreatedEventArgs)

这个可怜的事件曾以各种形式多次进出项目,直到我真正需要它——现在它将一直保留下来。当用户在编辑器中按下添加按钮后,但在调用自动 NameService 方法创建名称之前,会调用此事件。

该事件必须在你编写的编辑器中处理,并且你无法从那里做太多事情,比如访问集合数据。你能做的是更改用于自动命名的基本类型名称,或者只是柔化或微调要在编辑器中使用的类型名称。

在我的案例中,我试图不让用户感到困惑。他们可以在设计时定义集合项,但这些项只是运行时实际使用的类型的子集,并且有很大不同。所以,我将从类型派生的基本名称从 StartUpItem 改为 CheckItem(是的,真实类型名称会显示在设计器代码中)。这种用法的有效场景很有限,在演示中并未使用。

 

PropertyValueChanged

该事件来自编辑器窗体的 PropertyGrid。由于这可能是你最想访问的控件和事件,集合编辑器将该事件传递给你的代码,使你能够轻松订阅。只有当 UsePropGridChangeEventTrue 时,此事件才会触发。

    Protected Friend Event PropertyValueChanged(ByVal sender As Object,
                                                ByVal e As PropertyValueChangedEventArgs) 

 

嵌套/子集合

嵌套集合的处理方式没有区别。在集合属性上使用 Editor 特性来指定要使用的集合编辑器,确保你有一个 TypeConverter,并且所有内容都标记为 Serializable

最大的区别在于为这些集合使用 NameProvider 命名服务时:对 NameProvider 方法的“第二次机会”调用将转到“属性提供者”,即托管子集合属性的类。请参阅演示中的 XooBars 集合,其中一个 XooBar 项可以包含一个 ZItems 的集合(这些在演示中以自动命名开始)。如果将编辑器更改为使用 NameProvider,第一次调用将转到集合,第二次调用将转到提供集合属性的类——即 XooBar item

 

随时在运行时使用

如果你还需要为用户提供在运行时编辑集合项的方法,可以使用一个单独的实用工具类,在运行时显示指定的集合编辑器,而无需单独的运行时窗体。这是我看到 Mark Gravell 在 StackOverflow 上的一些回答后茅塞顿开的结果,他似乎是所有与类型相关事务的奇才。

该实用工具类使用 Reflection 找出你传入的属性名称的 UITypeEditor 特性,然后创建该编辑器的一个实例并显示它,接着再次使用 Reflection 来设置值。结果是,你只需一行代码即可在运行时调用你的集合编辑器。

   Shared Sub ShowEditor(owner As IWin32Window, component As Object, propertyName As String)
     
   ' runtime usage:
   RunTimeTypeEdit.ShowEditor(Me, NuControl1, "ZItemCollection") 

Owner 是将作为对话框父窗体或所有者的窗体。

Component 是包含你希望编辑的属性的实例类型(类或控件)。

propertyName 是要编辑的属性的确切名称。这必须是一个包含 Editor 特性的集合属性。

  • 为给定属性指定的 UITypeEditor 将会运行。这被限制为标准的 .NET CollectionEditor 或继承自 EnhancedCollectionEditor 的编辑器。
  • 这是在运行时,所以输入的数据不会被持久化或序列化,但集合中当前存在的任何数据都将在运行时集合中。

在演示中,按钮运行 XItems 类的自定义集合编辑器。FooBarItem 上的子集合关联的编辑器也将运行。

RunTimeUIEdTools 类还提供了 2 个共享方法来解析集合类项的类型/名称。

'  return the base type name parsed from the Type
Public Shared Function BaseNameFromType(ItemType As Type) As String


'  return the base type name parsed from the Type Name
Public Shared Function BaseNameFromType(ItemType As Type) As String

在演示中,XooItems.GetNewName 使用 BaseNameFromTypeName 来提供一个唯一的名称,具体取决于刚刚创建的实际类型(Ziggy1, Zoey1, Ziggy2, Zacky1, Zoey2...),而不是一个通用的 ZItem9 名称。

 

一个集合编辑器统领一切

由于所有智能和功能都已内置于基类中,你编写的“小”编辑器类所剩无几,除了创建它的一个实例并设置所需的选项外。并不妨碍你的集合编辑器做更多事情,比如通过处理 PropertyValueChange 事件或附加到其他控件来参与编辑过程。

在许多情况下,你可能会发现可以定义一个编辑器来处理项目中的所有集合。在演示中几乎就是这样。演示中定义了 6 个编辑器,其中 1 个处理多个 ZItem 集合,FooBarCollectionEditor 也同时处理 FoosBars。这本可以进一步减少,但作为一个演示,它的设置是为了让每个子集合编辑器都展示不同的功能。

 

演示

该演示本身没有太多功能,只是作为一个平台来展示 EnhancedCollectionEditor 的各种集合。为了真实起见,它由 3 个项目组成。

UIDesigner - 包含本文中描述的 EnhancedCollectionEditor 和其他 UI 编辑器及工具。

NuControl - 这代表某个控件或组件,其中包含你项目的集合。该项目是一个类库,正如在真实项目中很可能出现的情况。它包含与集合、特性、类型转换器等相关的所有代码。

UIDesigner Test - 这里几乎没有任何东西,只是一个托管 NuControl 实例的方式。

由于关键元素是 UIEditor,你必须先编译它才能使用。你可能会发现代码本身比演示更有说明性,除非在比较实现方式时。

演示代码中有相当多的实现说明和提示。每组类都位于其自己的项目源文件中,NuControl 广泛使用 Regions 来组织公开的属性。

XTDRItems

这是最复杂的版本

  • 它使用一个 Collection(Of ExtendedItems) 作为集合,XTDItemCollectionEditor 是其编辑器。
  • 每个项都继承自一个抽象基类。
  • 这些项都使用同一个带单参数的TypeConverter
  • 编辑器订阅了 PropertyValueChanged 事件
  • NuControl 实现了 ISupportInitialize 接口,并调用 XTDItems 类上的相关过程。
  • 集合编辑器包含代码,用于演示如何获取对 CollectionForm 上控件的引用。
  • ValueItem 类还包含一个示例 EnumConverter
  • FooBarItem 类型包含两个子集合
    • 一个由 FoosBars 组成的集合,其中 BarItem 使用简单继承方式继承自 FooItem
    • ZItems 的子集合中,使用 ExcludedTypes 来从此子集合中排除 Ziggy 类型
ZItemCollection

这是一个 ZItems (Ziggy, Zoey, Zacky) 的集合,使用 Collection(Of T),集合编辑器为 ZItemCollectionEditor。每个项都使用其自己的 TypeConverter

ZCollectionBase

ZItems 的另一种用法,这次使用 CollectionBase,同样以 ZItemCollectionEditor 作为编辑器。

ZObserveList

又一个 ZItems 集合,但在集合类中使用了 ObservableList(Of T)。这也重用了 ZItemCollectionEditor。两种不同的集合类型,但使用同一个编辑器。

XooBar

这个也相当复杂,主要用于演示 NameService。两种命名约定都已实现。

NameProvider

  • XooBar 项由 NuControl 命名,因为它公开了 XooBars 集合属性。
  • NuControl 实现了 INameProvider
  • NuControl 上的 GetNewName 函数为所有 Xoobar 项提供新名称(但也可以像代码中显示的那样为其他项命名)。

自动

  • 每个 XooBar 项都可以包含一个 ZItems 的子集合。
  • 对于这些,作为属性提供者,XooBar 项 可以 提供名称,但实际上没有。
  • XooBar 也实现了 INameProvider
  • ZItems 子集合编辑器指定了 NameServices.Automatic
    • ZSubCollectionEditorINP 编辑器类(位于 CollectionEds.vb 文件中)中,将其更改为 NameServices.NameProvider
    • 这将导致调用 XooItem 上的 GetName 函数(该函数已存在),并按你指定的任何格式为这些 ZItems 提供名称。

如前所述,你实际上只需要在需要实现特殊行为(如 NameService 或排除特殊类型)时才需要继承一个新的编辑器类。也就是说,同一个编辑器可以用来编辑 XooBar 项和 ZItems 项。所有实际的 Type 处理细节都隐藏在 EnhancedCollectionEditor 基类中。

 

已编译的 DLL

对于那些不想在各个项目中反复跟踪和包含源文件的人,EnhancedCollectionEditor 以 DLL 形式提供。编译器设置如下:

  • Option Strict On
  • Any CPU
  • NET Framework 4
  • 已勾选代码分析

该 DLL 的名称与另一个 UIEditor 的名称相同。两篇文章的源代码都已包含并编译到一个组合的 DLL 中。Flag 类型的 EnumEditor 也包含在内。

 

 

使用 UITypeEditors

使用 UITypeEditors 的奇怪之处在于,你正在处理的代码处于设计模式,但你的 UITypeEditor 代码正在执行。因此,Visual Studio 需要一个编译后的版本来工作。所以,如果你要修改代码,请确保经常重新编译——并且始终在测试更改之前编译——这样 Visual Studio 才能使用正确的版本。由于 VS 使用了过时的代码,可能会浪费大量时间来追踪不存在的错误。

通常,为 AnyCPU 编译的代码似乎适用于任何项目。但有时,某个地方的缓存没有被清除/更新,导致运行的是默认的 .NET CollectionEditor,而不是你的编辑器或基类编辑器。如果清理/重新生成不起作用,请重新启动 Visual Studio。

 

附录 - 集合编辑器实现清单

首先,如果你在让 VS 保存(序列化)你的集合项时遇到问题,请尽早并经常地清理和重新生成。有时,在进行了大量基础性更改(例如重构你的 TypeConverter 和优化项的构造函数)后,Visual Studio 可能会变得混乱,某些东西没有按预期被重置/清除。在这种情况下,退出并重新启动 VS。

使用本文中关键概念的这份清单,帮助你找出正在尝试实现的集合可能存在的问题。该列表也涵盖了设计器序列化和类型转换器的方面。如果你的自定义 Collection Editor 无法启动,那么它能否使用默认的 .NET 编辑器启动?如果不能,那么可能与你的类相关的某些东西存在根本性问题。

那些 Option Strict 会捕获的愚蠢错误在此省略。

集合类

  • 集合变量是否已实例化?
  • 集合类是否使用了类型化集合,例如 Collection(Of T) 或实现了 IList 的集合?
  • 集合属性是否使用了 DesignerSerializationVisibility 特性并设置为 Content
  • 是否使用 Editor 特性指定了正确且有效的集合编辑器
  • 你是否实现了 ShouldSerializeXXXResetXXX 并正确命名了它们?
  • 如果你的集合类继承自 CollectionBase,你是否有正确的 Item 属性?
Default Public Property Item(ndx As Integer) As <your_Type)

集合项类

  • 用于集合中事物的 Items 类是否标记为 Serializable
  • 每个要保存的属性是否都使用了 DesignerSerializationVisibility 进行修饰?
    • 常规属性应为 Visible
    • 由 TypeConverter 通过构造函数处理的属性应为 Hidden
  • 它是否有一个供集合编辑器使用的简单构造函数?(无参数,但可以初始化属性为默认值)

增强型集合编辑器

  • 如果你不确定运行的是默认的 .NET 编辑器还是你的编辑器,可以使用自定义标题来确认。(Console.Beep 也能很好地工作——如果构造函数触发(发出哔声),那么你的编辑器正在运行)。

  • 确保要使用的编辑器类名是集合属性上 Editor 特性指定的名称。

类型转换器

  • 类是否有 TypeConverter 特性?如果通过名称指定,名称是否正确?
  • TypeConverter 是否将传递给它的 value 参数转换为正确的 Type(如果你是从类似类复制过来的,请仔细检查)?
  • TypeConverter 是否为 InstanceDescriptor 返回 True?
  • Item 类是否确实有一个与你调用 GetConstructor 所请求的构造函数相匹配的构造函数(Item 类是否真的有一个与指定的顺序类型相匹配的构造函数)?请再检查一遍。
  • 传递给创建 InstanceDescriptorObject 数组是否与 GetConstructor 的顺序和类型相匹配(并且显然,Item 类上是否存在这样的构造函数)?

 

每次更改后都要清理并重新生成

 

参考资料和资源

《Developing .NET Custom Controls & Designers using C#》 作者:James Henry

《Windows Forms Programming in C#》 作者:Chris Sells

《如何使用 CollectionEditor 编辑和持久化集合》 作者:Daniel Zaharia

MSDN

Microsoft .NET 源代码参考

关于 TypeConverter 的更多信息:《创建一个自定义 TypeConverter...》 作者:Richard Moss

Mark Gravell 在 StackOverflow 上的各种回答(甚至不是我提的问题)

设计器序列化概述 来自 MSDN(不如想象中实用)

《玩转自定义集合》 作者:Sander Rossel,内容密集但展示了几种非常有趣的技术

 

历史

2014.07 - 文章及初始版本 v. 1.03

 

 

 

增强型 CollectionEditor 框架 - CodeProject - 代码之家
© . All rights reserved.