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

使用 CollectionEditor 编辑和持久化集合

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (96投票s)

2003年11月4日

CPOL

16分钟阅读

viewsIcon

431706

downloadIcon

13642

本文演示了如何使用 CollectionEditor 来编辑和持久化集合。

摘要

本文演示了如何使用 CollectionEditor 来编辑和持久化集合。此外,它还介绍了一个新的、非常通用且可自定义的集合编辑器,名为 CustomCollectionEditor

目录

引言

.NET 框架中,集合无处不在:ComboBoxListBoxListViewDataGridMenu,所有这些对象以及其他许多对象都使用集合。不仅理解这些对象如何使用集合很重要,而且了解如何充分利用它们对您自己来说也极其有用。

CollectionEditor 是一个非常强大的工具,用于在设计时编辑和持久化集合,但它对于运行时编辑来说是一个糟糕的选择,因为在运行时,您通常需要高度的自定义和通用性来处理主题实现、多语言支持以及其他一些实际需求。需要此类工具的人可以在 CustomControls.CollectionEditor.CustomCollectionEditor 中找到。

为了使用 CollectionEditor 持久化集合,您需要做三件事:实现一个集合项,实现一个用于保存项的集合,并且很多时候,根据项和集合的实现方式,需要继承 CollectionEditor 以获得正确的行为。

本文附带的源代码结构如下:

CollectionEditingTest 解决方案

  • CustomControls 项目

    一个小型控件库,包含以下控件:

    CTextBoxDropDownCalendarDropDownColorPickerDropDownBoolDropDownListPushButtonToggleButtonDropDownListBoxButton,以及最重要的 CustomCollectionEditorForm

  • TestProject 项目

    一个测试项目,您可以在其中找到用于讨论的问题的支持代码。

    • CollectionEditorTest.cs 包含用于演示使用 System.ComponentModel.Design.CollectionEditor 进行集合编辑和持久化技术的类。
    • CustomCollectionEditorTest.cs 包含与 CustomControls.CollectionEditor.CustomCollectionEditor 功能相关的类。
    • TestForm.cs。在这里,您可以测试由测试控件实现的集合的设计时序列化和编辑。
    • CustomControlsForm.cs 作为 CustomControls 库中控件的展示厅。

首次打开解决方案时,请先进行编译,然后再进行其他操作。

所有这些类都已组织在具有描述性的命名空间中,因此最好使用“类视图”窗口来探索解决方案。

如何实现集合项

(您可以在 TestProject 的文件 CollectionEditorTest.cs 中找到配套的源代码。)

任何项的第一个要求由 CollectionEditor 引入,它需要一个无参数构造函数,因为每次按下 CollectionEditor 的“添加”按钮时,CollectionEditor 必须仅根据类型创建一个您的项类型的对象,而不可能自动提供参数。这与持久化略有不同,持久化时您需要从现有对象创建一个对象,并且可以自动提供正确的参数。

您可以在 CustomCollectionEditorFormCreateInstance 函数中看到远程创建对象的示例,该函数负责从类型创建新项。

protected virtual object CreateInstance(Type itemType)
{ 

/* ***this is just another way***

//Get a parameterless constructor of that type
ConstructorInfo ci = itemType.GetConstructor(new Type[0]);

//Create an instance descriptor
InstanceDescriptor id = new InstanceDescriptor(ci,null,false);
return id.Invoke();
*/

// with the help of Activator class it is straightforward
object instance=Activator.CreateInstance(itemType,true);
OnInstanceCreated(instance);
return instance ;
}

创建集合项类的过程在很大程度上取决于您想对集合做什么。如果您只对编辑集合感兴趣,那么您必须做的是确保您的项与集合的 Item 类型相同(或反之)。但如果您想在设计时持久化(即自动生成源代码来描述您的组件状态)集合,事情会稍微复杂一些。

要持久化一个对象,代码生成引擎必须首先知道如何创建该对象的实例,而这正是大多数集合项实现失败的地方。有两种主要选择:实现 IComponent 或为您的类创建自定义 TypeConverter

实现 IComponent

(参见 SimpleItem_Component 类作为示例。)

这对于代码生成引擎来说已足够,因为任何实现 IComponent 的类都必须提供一个不接受参数的基本构造函数,或者一个接受 IContainer 类型单个参数的构造函数。了解这一点后,就可以创建该类的实例。

实现 IComponent 有其优点和缺点。首先,您应该决定实现 IComponent 是否对您有帮助,还是仅仅是您需要携带的另一个负担。创建后,CollectionEditor 会立即将新创建的项添加到窗体的容器中。这可能很有用,因为它会出现在组件托盘中,您可以使用 PropertyGrid 编辑它,但对于通常很多菜单项的情况,这可能会带来很多混乱。为避免这种情况,您可以为您的项类添加 DesignTimeVisible(false) 属性。

继承自 Component 的对象的序列化是一个标准的序列化过程,就像普通控件一样:首先声明一个变量,然后 InitializeComponent 过程的第一部分会创建一个对象,稍后在 InitializeComponent 的主体中,会设置对象的公共属性。这种序列化方式存在一个主要弊端,因为您无法知道在调用集合的 AddAddRange 方法时,对象的所有(或至少部分)属性是否都已设置。(在下一个示例中……您无法知道代码生成引擎会先序列化哪个对象:compItem 还是 tc。)

private void InitializeComponent()
{
this.compItem = new Test.Items.SimpleItem_Component();
.
.
.
//
//tc
//
this.tc.SimpleItems.AddRange (new Test.Items.SimpleItem[]{this.compItem});
//it would be much easier to maintain your collection integrity if you 
//could validate each item before adding it to the collection.
//let’s say, to check if there is already an item with the id 45 
.
.
.
//
//compItem
//
this.compItem.Id = 45;
this.compItem.Name = "SimpleItem Comp";
}

在这种情况下,解决方案是在 Id 属性的 Set{} 访问器中验证值。但必须针对集合进行验证,而且正如您可能知道的,集合项本身没有对其集合的引用。您可以在项的构造函数中通过传递集合作为参数来设置对集合的引用,并确保您始终拥有对父集合的引用。但是,如果您想在不同集合之间移动项,这将无济于事。

请注意,当组件添加到窗体的容器时,会添加三个设计时属性:DynamicPropertiesNameModifiers。其中 Name 最有用,尤其当您想维护标准的命名约定。

private Test.SimpleItem_Component mi_Save;
private Test.SimpleItem_Component mi_Undo;

而不是

private Test.SimpleItem_Component simpleItem_Component17;
private Test.SimpleItem_Component simpleItem_Component21;

请记住:具有 System.String 类型名为 Name 的属性(如 BasicItem 所拥有……哎呀!!)可能会混淆设计器在代码生成过程中的处理(尤其当它位于默认的“Misc”类别下时)。这是错误消息的一个示例:“标识符 'Simple Item Comp' 无效。”

注意:实现 IComponent 接口并非强制要求,您也可以继承自 System.ComponentModel.ComponentSimpleItem_Component 实现该接口而不是使用继承,是因为出于演示目的,它必须继承自 BasicItem

创建 TypeConverter

(您可以在 SimpleItem_BasicTcSimpleItem_FullTc 项类以及相应的类型转换器类 SimpleItemBasicConverterSimpleItemFullConverter 中找到以下示例).

SimpleItemBasicConverter 是实现类型转换器进行序列化的最低要求示例,而 SimpleItemFullConverter 是一个更复杂的示例。通过查看 PropertyGrid 如何显示 TestControl 类的两个成员:SimpleItem_BasicTCSimpleItem_FullTC,您可以了解其中的区别。

为您的类创建自定义类型转换器将使您能够更精确地控制项的序列化方式,但也会增加您的编码工作量。最大的优势在于,您可以使用其任何构造函数来序列化您的项。现在,您可以在将项添加到集合之前完全初始化它。

当您拥有简单的项,只有两三个属性时,使用初始化所有属性的构造函数似乎很明显,但如果您拥有更复杂的项,拥有许多需要序列化的属性(如 ToolbarButton),这可能会变得不合适。一个优雅的解决方案是在构造函数中仅初始化那些对验证至关重要的属性(IdName 等),让其他属性以正常方式设置。

如果您想使用类型转换器来控制对象的序列化方式,那么您的类型转换器必须能够将您的对象转换为 InstanceDescriptor。这是通过重写 ConvertTo() 函数来实现的。InstanceDescriptor 类有两个构造函数,其中一个有三个参数。对于这个构造函数,第三个参数是一个布尔值,指示对象的初始化是否完成。也就是说,对象是否由其构造函数完全初始化,或者设计器是否必须检查公共属性和字段以确定它们是否应被序列化。

SimpleItemBasicConverterSimpleItem_BasicTc 返回的 InstanceDescriptor

return new InstanceDescriptor
    (typeof(SimpleItem_BasicTc).GetConstructor(new Type[0]), null,false);

SimpleItemFullConverterSimpleItem_FullTc 返回的 InstanceDescriptor

return new InstanceDescriptor
    (typeof(SimpleItem_FullTc).GetConstructor(new Type[]{typeof(int), 
    typeof(string)}), new object[]{((SimpleItem_FullTc)value).Id,
    ((SimpleItem_FullTc)value).Name},true);

在这里,您可以看到设计器是如何处理这两种情况的。

private void InitializeComponent()
{
Test.Items.SimpleItem_BasicTc simpleItem_BasicTc1 = 
                    new Test.Items.SimpleItem_BasicTc();
.
.
.
// 
// tc
// 
simpleItem_BasicTc1.Id = -10;
simpleItem_BasicTc1.Name = "SimpleItem BasicTC";
this.tc.SimpleItems.AddRange
  (new Test.Items.SimpleItem[]{ simpleItem_BasicTc1,
  new Test.Items.SimpleItem_FullTc(-10, "SimpleItem FullTC")}); 
}

这种序列化方式有一个小缺点,那就是项在 InitializeComponent() 之外是不可访问的。如果您想混合序列化您的项(在构造函数中初始化部分属性,然后逐个设置其他属性),最好将那些在构造函数中初始化的属性标记为 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)],否则它们将被设置两次。

集合有哪些要求

(您可以在 TestProject 的文件 CollectionEditorTest.cs 中找到配套的源代码。)

集合必须满足三个要求才能被 CollectionEditor 成功持久化:

  1. 首先,集合必须实现 IList 接口(在大多数情况下,继承自 System.Collections.CollectionBase 是最佳选择)。
  2. 其次,它必须有一个 Indexer(VB.NET 中为 Item)属性。该属性的类型被 CollectionEditor 用来确定将添加到集合的实例的默认类型。

    为了更好地理解这是如何工作的,请查看 CustomCollectionEditorFormGetItemType() 函数。

    protected virtual Type GetItemType(IList coll)
    {
        PropertyInfo pi= coll.GetType().GetProperty("Item",
                                               new Type[]{typeof(int)});
        return pi.PropertyType
    }
  3. 第三,集合类必须实现以下一个或两个方法:AddAddRange。尽管 IList 接口有一个 Add 成员并且 CollectionBase 实现 IList,您仍然需要为您的集合实现一个 Add 方法,因为 CollectionBase 声明了一个显式的成员实现 IListAdd 成员。设计器根据您实现的方法来序列化集合。如果您同时实现了两者,则 AddRange 优先。

使用 Add 方法进行序列化时,每个项占用一行。

this.tc.SimpleItems.Add(new Test.Items.SimpleItem_FullTc(-1, "Item1"));
this.tc.SimpleItems.Add(new Test.Items.SimpleItem_FullTc(-1, "Item2"));

当设计器使用 AddRange 方法时,所有项都添加在同一行。

this.tc.SimpleItems.AddRange
  (new Test.Items.SimpleItem[]{new Test.Items.SimpleItem_FullTc(-1, "Item1"),
new Test.Items.SimpleItem_FullTc(-1, "Item2")});

如果您想序列化嵌套项的集合,例如 System.Windows.Forms.Menu.MenuItemCollection,那么很明显您的集合必须有一个 AddRange 方法。(查看 TestControlComplexItems 集合是如何序列化的。)

CollectionEditor,如何操作

(配套源代码位于 TestProject 的文件 CollectionEditorTest.cs 中。)

CollectionEditor 位于 System.ComponentModel.Design 命名空间中,但前提是您必须将 System.Design.dll 添加到您的项目中。

如何将 CollectionEditor 与属性关联

(参见 TestControl 类作为示例。)

声明并初始化您集合类型的本地变量。

private SimpleItems _SimpleItems= new SimpleItems();

创建一个只读属性,并添加以下两个属性:DesignerSerializationVisibilityEditor

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Editor(typeof(System.ComponentModel.Design.CollectionEditor), 
                       typeof(System.Drawing.Design.UITypeEditor))]
public SimpleItems SimpleItems
{
get{return _SimpleItems;}
}

现在,SimpleItems 属性将使用 CollectionEditor 进行编辑。

如何向集合添加多种项类型

(参见 SimpleItem_CollectionEditor 类作为示例。)

默认情况下,CollectionEditor 仅向集合添加一种类型的项。在上述情况下,它将只添加 SimpleItem 类型的项(SimpleItems 集合的 Item 属性的类型)。由于 SimpleItems 集合可以容纳所有派生自 SimpleItem 的项(SimpleItem_FullTcSimpleItem_BasicTc 等),能够添加它们将会很有用。

为此,您需要继承 CollectionEditor 并重写 CreateNewItemTypes

public class SimpleItem_CollectionEditor: 
           System.ComponentModel.Design.CollectionEditor
{
    private Type[] types; 

    public SimpleItem_CollectionEditor (Type type):base(type )
    {
        types = new Type[] {typeof(SimpleItem),typeof(SimpleItem_BasicTc), 
                                                    typeof(SimpleItem_FullTc)

    ,typeof(SimpleItem_Component),    typeof(ComplexItem) }; 
    } 


    protected override Type[] CreateNewItemTypes() 
    {    
        return types; 
    } 
}

接下来,更改您集合的 Editor 属性,指示设计器使用您的自定义集合编辑器。

[DesignerSerializationVisibility (DesignerSerializationVisibility.Content)]
[Editor(typeof(SimpleItem_CollectionEditor,
     typeof(System.Drawing.Design.UITypeEditor))]
public SimpleItems SimpleItems
{
    get{return _SimpleItems;}
}

如何让 CollectionEditor 编辑嵌套集合

(参见 ComplexItemCollectionEditor 类作为示例。)

如果您尝试使用标准的 CollectionEditor 编辑嵌套集合,很可能会收到错误。错误的原因是设计器只使用 CollectionEditor 的一个实例,当您想编辑子项的集合时,它会尝试打开已经打开的同一个实例。

解决方案是检查 CollectionEditor 的实例是否已打开,如果已打开,则创建另一个实例。

public class ComplexItemCollectionEditor: 
         System.ComponentModel.Design.CollectionEditor
{
    private CollectionForm collectionForm; 

    public ComplexItemCollectionEditor (Type type):base(type ){}

    public override object EditValue( ITypeDescriptorContext context, 
                                 IServiceProvider provider,object value) 
    { 
        if(this.collectionForm != null && this.collectionForm.Visible) 
        { 
            ComplexItemCollectionEditor editor = 
               new ComplexItemCollectionEditor(this.CollectionType); 
            return editor.EditValue(context, provider, value); 
        }     

    else return base.EditValue(context, provider, value);
    } 

    protected override CollectionForm CreateCollectionForm() 
    { 
        this.collectionForm = base.CreateCollectionForm(); 
       return this.collectionForm;
     }
}

在项添加到集合之前,如何访问这些项

(参见 ComplexItemCollectionEditor 类作为示例。)

有时您可能需要在项创建后立即进行一些处理(设置一个标志,表明该项是通过 CollectionEditor 创建的,设置一个更合适的名称等)。在这种情况下,您只需要重写 CreateInstance 函数。

这是一个例子。

public class ComplexItemCollectionEditor : 
         System.ComponentModel.Design.CollectionEditor
{
    protected override object CreateInstance(Type ItemType)
    {
        ComplexItem ci=(ComplexItem)base.CreateInstance(ItemType);
        if ( this.Context.Instance!=null)
            {
               if (this.Context.Instance is ISupportUniqueName)
               {
                 ci.Name=((ISupportUniqueName) 
                     this.Context.Instance).GetUniqueName();
               }
         else{ci.Name="ComplexItem";  }

       return ci;
    }
}

如何在项被销毁之前进行清理

当然,最好的方法是重写项的 Dispose 方法,但这可能并不总是可行。在这种情况下,您可以重写 CollectionEditorDestroyInstance 方法。

protected override void DestroyInstance (object instance)
{
    //do some cleaning here
    base.DestroyInstance(instance);
}

CustomCollectionEditor

(您可以在 TestProject 的文件 CustomCollectionEditorTest.cs 中找到配套代码,并在 CustomControls 项目的 CustomCollectionEditor.csCustomCollectionEditorForm.cs 文件中找到类实现。)

正如您可能已经注意到的,CollectionEditor 完全有能力在设计时编辑和持久化集合,这里不需要另一个集合编辑器。然而,在 .NET 中,集合是存储少量数据的非常方便的方式,它们在应用程序用户界面中的使用可能非常有益。为了实现这一点,需要一个可以轻松集成到应用程序中的工具。当然,第一个想法是使用 CollectionEditor,但这会带来各种问题:

  • 您无法直接调用它,实际上唯一调用它的方式是通过 PropertyGrid 控件。
  • 您无法更改它的外观,也无法对其进行全局化。
  • 您无法控制用户编辑集合的方式(FullEditReadOnlyAddOnly...)。

需要创建一个新工具,最显而易见的做法是复制 CollectionEditor 的风格,因为许多人已经使用过它,对其非常熟悉,并且它已被证明是编辑集合的绝佳工具。

它的要求是:

  • 能够在运行时直接编辑集合。
  • 非常易于自定义(从设计角度出发,并且不仅仅是),并且易于集成到宿主应用程序中。
  • 保持 CollectionEditor 可以轻松调整以编辑不同类型集合的便捷性。

约定:术语集合项将用于表示集合中的一项,TVitem 用于表示 CustomCollectionEditor 用于视觉表示该集合项的 TreeView 节点。

为了满足上述要求,显而易见需要两样东西:一个用于运行时编辑的 Form,即 CustomCollectionEditorForm,以及一个包装该窗体的 UITypeEditor,即 CustomCollectionEditor,用于设计时。

CollectionEditor 相比,CustomCollectionEditor 具有一些新功能:

  1. 它允许您从同一个窗口编辑嵌套集合的所有代,通过使用 TreeView 来显示集合树。更具体地说,对于每个集合项,它可以根据您的选择显示子项的集合(如果您的项至少有一个子项集合)。
  2. 您可以专门设置 TreeView 显示的 TVitem 的名称。默认情况下,CustomCollectionEditorForm 会查看您的集合项是否具有 Name 属性,如果有,它将使用 Name 属性的值为每个 TVitem 命名。如果集合项没有 Name 属性,它将用集合项的类名命名所有 TVitem
  3. 它允许您设置不同的编辑级别(FullEditAddOnlyRemoveOnlyReadOnly)。可以为 CustomCollectionEditorForm 的一个实例定义编辑级别,也可以为集合类型定义编辑级别。窗体的编辑级别优先于集合的编辑级别。您可以通过将 EditLevel 属性设置为 CustomControls.Enumerations.EditLevel 枚举的上述值之一来指定 CustomCollectionEditorForm 的编辑级别。要为集合设置编辑级别,您必须重写 SetEditLevel() 函数。
  4. 对于每个集合项,您都可以直接访问 TreeView 中显示的 TVitem。这样,对于每个 TVitem,您可以根据集合项的状态设置诸如 ForeColorBackColorFont 等属性。
  5. 最后但同样重要的是,它是开源的。

注意:如果您对 CustomCollectionEditor 的全局化感兴趣,您需要对其中包含的 PropertyGrid 进行全局化。您可以在此处找到一篇解释如何操作的文章:Globalized property grid

工作原理

由于所有逻辑都实现在 CustomCollectionEditorForm 中,因此您可以在这里通过重写一些关键成员来设置所需的行为。

  • protected virtual void SetProperties(TItem titem, object reffObject)

    此过程接收两个参数:一个 TItem 类型的参数,它代表 TVitem;以及一个 object 类型的参数,它代表相应的集合项。

    Titem 派生自 TreeNode,并具有两个新属性:

    • SubItems

      表示集合项的子项集合,您希望将其显示为 TVitem 的子节点。所有子项集合都必须使用 CustomCollectionEditor 进行编辑(通过将 CustomCollectionEditor 关联为 Editor,请参见 tc.CustomItems 的情况)。

    • 表示与之关联的集合项。

      默认情况下,此属性设置 TVitem 的文本。重写它以进一步自定义 TVitem

    每次在 PropertyGrid 中显示的集合项的属性发生更改时,都会调用此方法。

    protected override void SetProperties(TItem titem, object reffObject)
    
    {
     if(reffObject is CustomItem)
     {
        CustomItem ci =reffObject as CustomItem;
        //Specifically set the name
        titem.Text=ci.Color.Name;
         //Associate a collection of sub items
        titem.SubItems=ci.SpecialItems;
        //if you associated a ImageList to
        // this CustomCollectionEditorForm, 
        //you can display some images in front
        // of the TreeNode item
        item.SelectedImageIndex=0;
        titem.ImageIndex=1;
        titem.ForeColor=Color.Red;
     }
    
    .
    
    .
    
    .
    else if(reffObject is ComplexItem)
    {
         ComplexItem ci =reffObject as ComplexItem;
         // you cannot associate a collection of sub items to this item
         //type, since it has as an editor a CollectionEditor, and not a 
         //CustomCollectionEditor
         //titem.SubItems=ci.ComplexItems;
         //Specifically set the name
         titem.Text=ci.Name;
         titem.ForeColor=Color.Black;
         titem.SelectedImageIndex=0;
         titem.ImageIndex=6;  
    }
    else
    {
      // set the default Text for the <CODE lang=cs>TVitem
      base.SetProperties(titem,reffObject);
    }
    }
  • protected virtual CustomControls.Enumerations.EditLevel SetEditLevel(IList collection)

    它允许您为集合指定一个编辑级别。与仅对 CustomCollectionEditorForm 的该实例有效的窗体编辑级别不同,集合的编辑级别在所有实例中都有效。

    protected override CustomControls.Enumerations.EditLevel 
                                        SetEditLevel(IList collection)
    {
     if(collection is SpecialItems)
     {
       return CustomControls.Enumerations.EditLevel.AddOnly;
     }
    
     return base.SetEditLevel (collection);
    
    }
  • protected virtual Type[] CreateNewItemTypes(System.Collections.IList coll)

    它的功能与其在 CollectionEditor 中的同名函数完全相同,但由于 CustomCollectionEditorForm 可以同时编辑多个集合,因此它接受一个额外的 System.Collections.IList 类型的参数,以指示可用于每个集合的项类型。

    protected override Type[] CreateNewItemTypes(System.Collections.IList coll)
    
    {
      if(coll is SimpleItems)
     {
          return new Type[]{typeof(CustomItem), typeof(SimpleItem_BasicTc),
             typeof(SimpleItem_FullTc), 
             typeof(SimpleItem_Component), typeof(ComplexItem)};
     }
     else
     {
          return base.CreateNewItemTypes(coll);
     }
    
    }

    那么 CustomCollectionEditor 呢?它打开的对话框窗体是 CustomCollectionEditorForm,因此您需要做的就是重写其 CreateForm() 函数,并返回您自定义的 CustomCollectionEditorForm 实例。

    public class CustomItemCollectionEditor: CustomCollectionEditor
    
    {
    
       protected override CustomCollectionEditorForm CreateForm()
       {
          return new CustomCollectionEditorDialog ();
          
       }
    
    }

结论

.NET 框架在 CollectionEditor 中提供了一个强大的工具,用于在设计时编辑和持久化集合。在大多数情况下,它已经足够了。但对于更高级的场景,尤其是在运行时,它无法提供太多帮助。这时 CustomCollectionEditorCustomCollectionEditorForm 就派上用场了,它们提供了一种直接灵活的方式来应对这些场景。

参考文献

修订历史

  • 解决了 CustomCollectionEditor 的一些问题
    • 解决了 ReadOnly 状态的 bug
    • 解决了“取消”按钮的问题。
    • SetProperties 方法在选定集合项的属性更改时被调用。现在在此设置 TVitem 的名称,因此 SetDisplayName 方法已过时,并被删除。
  • 对文章文本的一些更正和修改。
  • 原始文章。
© . All rights reserved.