使用 CollectionEditor 编辑和持久化集合






4.99/5 (96投票s)
本文演示了如何使用 CollectionEditor 来编辑和持久化集合。
摘要
本文演示了如何使用 CollectionEditor
来编辑和持久化集合。此外,它还介绍了一个新的、非常通用且可自定义的集合编辑器,名为 CustomCollectionEditor
。
目录
引言
.NET 框架中,集合无处不在:ComboBox
、ListBox
、ListView
、DataGrid
、Menu
,所有这些对象以及其他许多对象都使用集合。不仅理解这些对象如何使用集合很重要,而且了解如何充分利用它们对您自己来说也极其有用。
CollectionEditor
是一个非常强大的工具,用于在设计时编辑和持久化集合,但它对于运行时编辑来说是一个糟糕的选择,因为在运行时,您通常需要高度的自定义和通用性来处理主题实现、多语言支持以及其他一些实际需求。需要此类工具的人可以在 CustomControls.CollectionEditor.CustomCollectionEditor
中找到。
为了使用 CollectionEditor
持久化集合,您需要做三件事:实现一个集合项,实现一个用于保存项的集合,并且很多时候,根据项和集合的实现方式,需要继承 CollectionEditor
以获得正确的行为。
本文附带的源代码结构如下:
CollectionEditingTest 解决方案
- CustomControls 项目
一个小型控件库,包含以下控件:
CTextBox
、DropDownCalendar
、DropDownColorPicker
、DropDownBool
、DropDownList
、PushButton
、ToggleButton
、DropDownListBoxButton
,以及最重要的CustomCollectionEditorForm
。 - TestProject 项目
一个测试项目,您可以在其中找到用于讨论的问题的支持代码。
- CollectionEditorTest.cs 包含用于演示使用
System.ComponentModel.Design.CollectionEditor
进行集合编辑和持久化技术的类。 - CustomCollectionEditorTest.cs 包含与
CustomControls.CollectionEditor.CustomCollectionEditor
功能相关的类。 - TestForm.cs。在这里,您可以测试由测试控件实现的集合的设计时序列化和编辑。
- CustomControlsForm.cs 作为
CustomControls
库中控件的展示厅。
- CollectionEditorTest.cs 包含用于演示使用
首次打开解决方案时,请先进行编译,然后再进行其他操作。
所有这些类都已组织在具有描述性的命名空间中,因此最好使用“类视图”窗口来探索解决方案。
如何实现集合项
(您可以在 TestProject 的文件 CollectionEditorTest.cs 中找到配套的源代码。)
任何项的第一个要求由 CollectionEditor
引入,它需要一个无参数构造函数,因为每次按下 CollectionEditor
的“添加”按钮时,CollectionEditor
必须仅根据类型创建一个您的项类型的对象,而不可能自动提供参数。这与持久化略有不同,持久化时您需要从现有对象创建一个对象,并且可以自动提供正确的参数。
您可以在 CustomCollectionEditorForm
的 CreateInstance
函数中看到远程创建对象的示例,该函数负责从类型创建新项。
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
的主体中,会设置对象的公共属性。这种序列化方式存在一个主要弊端,因为您无法知道在调用集合的 Add
或 AddRange
方法时,对象的所有(或至少部分)属性是否都已设置。(在下一个示例中……您无法知道代码生成引擎会先序列化哪个对象: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{}
访问器中验证值。但必须针对集合进行验证,而且正如您可能知道的,集合项本身没有对其集合的引用。您可以在项的构造函数中通过传递集合作为参数来设置对集合的引用,并确保您始终拥有对父集合的引用。但是,如果您想在不同集合之间移动项,这将无济于事。
请注意,当组件添加到窗体的容器时,会添加三个设计时属性:DynamicProperties
、Name
和 Modifiers
。其中 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.Component
。SimpleItem_Component
实现该接口而不是使用继承,是因为出于演示目的,它必须继承自 BasicItem
。
创建 TypeConverter
(您可以在 SimpleItem_BasicTc
和 SimpleItem_FullTc
项类以及相应的类型转换器类 SimpleItemBasicConverter
和 SimpleItemFullConverter
中找到以下示例).
SimpleItemBasicConverter
是实现类型转换器进行序列化的最低要求示例,而 SimpleItemFullConverter
是一个更复杂的示例。通过查看 PropertyGrid
如何显示 TestControl
类的两个成员:SimpleItem_BasicTC
和 SimpleItem_FullTC
,您可以了解其中的区别。
为您的类创建自定义类型转换器将使您能够更精确地控制项的序列化方式,但也会增加您的编码工作量。最大的优势在于,您可以使用其任何构造函数来序列化您的项。现在,您可以在将项添加到集合之前完全初始化它。
当您拥有简单的项,只有两三个属性时,使用初始化所有属性的构造函数似乎很明显,但如果您拥有更复杂的项,拥有许多需要序列化的属性(如 ToolbarButton
),这可能会变得不合适。一个优雅的解决方案是在构造函数中仅初始化那些对验证至关重要的属性(Id
、Name
等),让其他属性以正常方式设置。
如果您想使用类型转换器来控制对象的序列化方式,那么您的类型转换器必须能够将您的对象转换为 InstanceDescriptor
。这是通过重写 ConvertTo()
函数来实现的。InstanceDescriptor
类有两个构造函数,其中一个有三个参数。对于这个构造函数,第三个参数是一个布尔值,指示对象的初始化是否完成。也就是说,对象是否由其构造函数完全初始化,或者设计器是否必须检查公共属性和字段以确定它们是否应被序列化。
由 SimpleItemBasicConverter
为 SimpleItem_BasicTc
返回的 InstanceDescriptor
return new InstanceDescriptor
(typeof(SimpleItem_BasicTc).GetConstructor(new Type[0]), null,false);
由 SimpleItemFullConverter
为 SimpleItem_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
成功持久化:
- 首先,集合必须实现
IList
接口(在大多数情况下,继承自System.Collections.CollectionBase
是最佳选择)。 - 其次,它必须有一个
Indexer
(VB.NET 中为Item
)属性。该属性的类型被CollectionEditor
用来确定将添加到集合的实例的默认类型。为了更好地理解这是如何工作的,请查看
CustomCollectionEditorForm
的GetItemType()
函数。protected virtual Type GetItemType(IList coll) { PropertyInfo pi= coll.GetType().GetProperty("Item", new Type[]{typeof(int)}); return pi.PropertyType }
- 第三,集合类必须实现以下一个或两个方法:
Add
和AddRange
。尽管IList
接口有一个Add
成员并且CollectionBase
实现IList
,您仍然需要为您的集合实现一个Add
方法,因为CollectionBase
声明了一个显式的成员实现IList
的Add
成员。设计器根据您实现的方法来序列化集合。如果您同时实现了两者,则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
方法。(查看 TestControl
的 ComplexItems
集合是如何序列化的。)
CollectionEditor,如何操作
(配套源代码位于 TestProject 的文件 CollectionEditorTest.cs 中。)
CollectionEditor
位于 System.ComponentModel.Design
命名空间中,但前提是您必须将 System.Design.dll 添加到您的项目中。
如何将 CollectionEditor 与属性关联
(参见 TestControl
类作为示例。)
声明并初始化您集合类型的本地变量。
private SimpleItems _SimpleItems= new SimpleItems();
创建一个只读属性,并添加以下两个属性:DesignerSerializationVisibility
和 Editor
。
[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_FullTc
、SimpleItem_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
方法,但这可能并不总是可行。在这种情况下,您可以重写 CollectionEditor
的 DestroyInstance
方法。
protected override void DestroyInstance (object instance)
{
//do some cleaning here
base.DestroyInstance(instance);
}
CustomCollectionEditor
(您可以在 TestProject 的文件 CustomCollectionEditorTest.cs 中找到配套代码,并在 CustomControls 项目的 CustomCollectionEditor.cs 和 CustomCollectionEditorForm.cs 文件中找到类实现。)
正如您可能已经注意到的,CollectionEditor
完全有能力在设计时编辑和持久化集合,这里不需要另一个集合编辑器。然而,在 .NET 中,集合是存储少量数据的非常方便的方式,它们在应用程序用户界面中的使用可能非常有益。为了实现这一点,需要一个可以轻松集成到应用程序中的工具。当然,第一个想法是使用 CollectionEditor
,但这会带来各种问题:
- 您无法直接调用它,实际上唯一调用它的方式是通过
PropertyGrid
控件。 - 您无法更改它的外观,也无法对其进行全局化。
- 您无法控制用户编辑集合的方式(
FullEdit
、ReadOnly
、AddOnly
...)。
需要创建一个新工具,最显而易见的做法是复制 CollectionEditor
的风格,因为许多人已经使用过它,对其非常熟悉,并且它已被证明是编辑集合的绝佳工具。
它的要求是:
- 能够在运行时直接编辑集合。
- 非常易于自定义(从设计角度出发,并且不仅仅是),并且易于集成到宿主应用程序中。
- 保持
CollectionEditor
可以轻松调整以编辑不同类型集合的便捷性。
约定:术语集合项将用于表示集合中的一项,TVitem 用于表示 CustomCollectionEditor
用于视觉表示该集合项的 TreeView
节点。
为了满足上述要求,显而易见需要两样东西:一个用于运行时编辑的 Form
,即 CustomCollectionEditorForm
,以及一个包装该窗体的 UITypeEditor
,即 CustomCollectionEditor
,用于设计时。
与 CollectionEditor
相比,CustomCollectionEditor
具有一些新功能:
- 它允许您从同一个窗口编辑嵌套集合的所有代,通过使用
TreeView
来显示集合树。更具体地说,对于每个集合项,它可以根据您的选择显示子项的集合(如果您的项至少有一个子项集合)。 - 您可以专门设置
TreeView
显示的TVitem
的名称。默认情况下,CustomCollectionEditorForm
会查看您的集合项是否具有Name
属性,如果有,它将使用Name
属性的值为每个TVitem
命名。如果集合项没有Name
属性,它将用集合项的类名命名所有TVitem
。 - 它允许您设置不同的编辑级别(
FullEdit
、AddOnly
、RemoveOnly
、ReadOnly
)。可以为CustomCollectionEditorForm
的一个实例定义编辑级别,也可以为集合类型定义编辑级别。窗体的编辑级别优先于集合的编辑级别。您可以通过将EditLevel
属性设置为CustomControls.Enumerations.EditLevel
枚举的上述值之一来指定CustomCollectionEditorForm
的编辑级别。要为集合设置编辑级别,您必须重写SetEditLevel()
函数。 - 对于每个集合项,您都可以直接访问
TreeView
中显示的TVitem
。这样,对于每个TVitem
,您可以根据集合项的状态设置诸如ForeColor
、BackColor
、Font
等属性。 - 最后但同样重要的是,它是开源的。
注意:如果您对 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
中提供了一个强大的工具,用于在设计时编辑和持久化集合。在大多数情况下,它已经足够了。但对于更高级的场景,尤其是在运行时,它无法提供太多帮助。这时 CustomCollectionEditor
和 CustomCollectionEditorForm
就派上用场了,它们提供了一种直接灵活的方式来应对这些场景。
参考文献
- 设计时增强
PropertyGrid
文章- 左键单击 gadget 并拖动以移动它。左键单击 gadget 的右下角并拖动以调整其大小。右键单击 gadget 以访问其属性。
修订历史
- 解决了
CustomCollectionEditor
的一些问题- 解决了
ReadOnly
状态的 bug - 解决了“取消”按钮的问题。
SetProperties
方法在选定集合项的属性更改时被调用。现在在此设置TVitem
的名称,因此SetDisplayName
方法已过时,并被删除。
- 解决了
- 对文章文本的一些更正和修改。
- 原始文章。