Summary
This article demonstrates how to edit and persist collections with CollectionEditor
. In addition, it presents a new, very versatile and customizable collection editor, CustomCollectionEditor
.
Contents
Introduction
In .NET framework, collections are omnipresent: ComboBox
, ListBox
, ListView
, DataGrid
, Menu
, all of these objects, and many others, make use of collections. Not only is it important to understand how these objects use them, but can be extremely useful to know how to take the most out of them yourself.
CollectionEditor
is a very powerful tool for editing and persisting collections at design time, but is a poor option for run time editing, when you often need a high level of customization and versatility for implementation of themes, multilingual support and some other real world requirements. Those who need such a tool can find one here in CustomControls.CollectionEditor.CustomCollectionEditor
.
In order to persist a collection with CollectionEditor
, you need to do three things: implement a collection item, implement a collection to hold the items, and many times, depending on the item and collection implementation, subclass the CollectionEditor
to obtain the right behavior.
This article is accompanied by source code structured as follows:
CollectionEditingTest solution:
- CustomControls project
A small controls library containing the following controls:
CTextBox
, DropDownCalendar
, DropDownColorPicker
, DropDownBool
, DropDownList
, PushButton
, ToggleButton
, DropDownListBoxButton
, and most important, CustomCollectionEditorForm
.
- TestProject project
A test project where you can find support code for the issues discussed here.
- CollectionEditorTest.cs contains classes used for demonstrating collection editing and persisting techniques with
System.ComponentModel.Design.CollectionEditor
.
- CustomCollectionEditorTest.cs contains classes related with
CustomControls.CollectionEditor.CustomCollectionEditor
functionality.
- TestForm.cs. Here you can test the design-time serialization and editing of collections implemented by a test control.
- CustomControlsForm.cs serves as a showroom for the controls in CustomControls library.
When you open the solution for the first time, you should compile it before doing anything else.
All these classes are well organized in suggestive namespaces, so it is a good idea to explore the solution with the Class View window.
How to implement a Collection Item
(You can find the accompanying source code in the TestProject�s file, CollectionEditorTest.cs.)
A first requirement for any item is introduced by CollectionEditor
which needs a parameter-less constructor, simply because every time the Add button of CollectionEditor
is pushed, the CollectionEditor
must create an object of the type of your item, from nothing else but the type, and it would be impossible to automatically provide parameters. This is slightly different from persisting, where you have to create an object from an existing one, and it is possible to automatically provide the right parameters.
You can see an example of how to remotely create an object in the CustomCollectionEditorForm
's CreateInstance
function, which is responsible for creating new items from a type:
protected virtual object CreateInstance(Type itemType)
{
object instance=Activator.CreateInstance(itemType,true);
OnInstanceCreated(instance);
return instance ;
}
The process of creating a collection item class depends very much on what you want to do with your collection. If you are interested only in editing collections, then all that you must do is to assure that your item is of the same type as the collection�s Item
type (or vice versa). But if you want to persist (that is, automatically generate source code to describe your component state) collections during design time, things are a little bit more difficult.
To persist an object, code generation engine must first know how to create an instance of that object, and it is exactly here where most of collection item's implementation fail. There are two main options: implementing IComponent
or creating a custom TypeConverter
for your class.
Implementing IComponent
(See SimpleItem_Component
class for an example.)
This is enough for the code generation engine, since any class that implements IComponent
must provide a basic constructor that requires no parameters or a single parameter of type IContainer
. Knowing this, it�s possible to create an instance of that class.
Implementing IComponent
has its advantages and disadvantages. First, you should decide if implementing IComponent
helps you, or is it just another rock that you have to carry with you. Immediately after creation, the CollectionEditor
adds the newly created item to the form's container. This can be useful since it will appear in the Component Tray and you will be able to edit it with the PropertyGrid
, but can bring a lot of confusion in the case of menu items, which are usually many. To avoid this, you can add the DesignTimeVisible(false)
attribute to your item class.
The serialization of an object that inherits from Component
is a standard one, like for normal controls: first a variable is declared, after that in the first part of the InitializeComponent
procedure, an object is created, and somewhere later inside the body of InitializeComponent
, the public properties of the object are set. This kind of serialization brings a major setback, because you have no way of knowing if by the time the Add
or AddRange
collection�s methods are called, all (or at least some) of the object properties were set. (in the next example� you can�t know what object the code generation engine will serialize first: compItem
or tc
.)
private void InitializeComponent()
{
this.compItem = new Test.Items.SimpleItem_Component();
.
.
.
this.tc.SimpleItems.AddRange (new Test.Items.SimpleItem[]{this.compItem});
.
.
.
this.compItem.Id = 45;
this.compItem.Name = "SimpleItem Comp";
}
For this case, the solution is to validate the value in the Set{}
accessor of the Id
property. But it must be validated against the collection, and as you probably know a collection item doesn�t have any reference to its collection. You can set a reference to the collection in the item�s constructor (by passing the collection as parameter), and certify that you�ll always have a reference to the parent collection. However, this won�t help if you want to be able to change items between different collections.
Please notice that when a component is added to the form�s container, three design time properties are added: DynamicProperties
, Name
and Modifiers
. Of most use is Name
, especially if you want to maintain a standard naming:
private Test.SimpleItem_Component mi_Save;
private Test.SimpleItem_Component mi_Undo;
instead of:
private Test.SimpleItem_Component simpleItem_Component17;
private Test.SimpleItem_Component simpleItem_Component21;
Keep in mind: Having a property named Name
of type System.String
(as BasicItem
has.. ooopps!!) can confuse the designer in the code generation process (especially if it�s under the default, �Misc�, category). This is an example of the error message: " Identifier 'Simple Item Comp' is not valid."
Note: It is not mandatory to implement the IComponent
interface, you can also inherit from System.ComponentModel.Component
. SimpleItem_Component
implements the interface instead of using inheritance, because, for demonstration purposes, it must inherit from BasicItem
.
Creating a TypeConverter
(You can find an example of the following in SimpleItem_BasicTc
and SimpleItem_FullTc
item classes and the correspondent type converter classes SimpleItemBasicConverter
and SimpleItemFullConverter
).
SimpleItemBasicConverter
is an example of the minimum requirements when implementing a type converter for serialization, while SimpleItemFullConverter
is a more complex example. You can see the difference by looking how PropertyGrid
displays the two members of TestControl
class: SimpleItem_BasicTC
and SimpleItem_FullTC
.
Creating a custom type converter for your class, will give you more control about how your items are serialized but it will also require more coding from you. The biggest advantage of this, is that you can serialize your items using any of its constructors. Now, you can fully initialize your item before adding it to the collection.
When you have simple items, with only two or three properties, using a constructor that initializes all of the properties seems obvious, but if you have more complex items, with many properties that need to be serialized (like ToolbarButton
) this can become inappropriate. An elegant solution is to initialize in the constructor, only those properties that are crucial for a validation (Id
, Name
etc.), and let the other ones to be set in a normal way.
If you want to use a type converter to control how your object is serialized, then your type converter must be able to convert your object to an InstanceDescriptor
. This is done by overriding the ConvertTo()
function. InstanceDescriptor
class has two constructors, one of which has three parameters. For this constructor, the third parameter is a boolean value indicating if the initialization of the object is or not complete. That is, if the object is completely initialized by its constructor or if the designer must check the public properties and fields to see if they should or not be serialized.
The InstanceDescriptor
returned by SimpleItemBasicConverter
for a SimpleItem_BasicTc
:
return new InstanceDescriptor
(typeof(SimpleItem_BasicTc).GetConstructor(new Type[0]), null,false);
The InstanceDescriptor
returned by SimpleItemFullConverter
for a SimpleItem_FullTc
:
return new InstanceDescriptor
(typeof(SimpleItem_FullTc).GetConstructor(new Type[]{typeof(int),
typeof(string)}), new object[]{((SimpleItem_FullTc)value).Id,
((SimpleItem_FullTc)value).Name},true);
Here you can see how the designer serializes the two cases:
private void InitializeComponent()
{
Test.Items.SimpleItem_BasicTc simpleItem_BasicTc1 =
new Test.Items.SimpleItem_BasicTc();
.
.
.
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")});
}
This kind of serialization has a small disadvantage in the fact that the item is not accessible outside the InitializeComponent()
. If you want a mixt serialization of your items, (initialize part of the properties in the constructor, and set the other properties one by one), it is better to mark those properties that are initialized in the constructor with the [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
attribute, or they will be set twice.
What are the requirements for a Collection
(You can find the accompanying source code in the TestProject�s file CollectionEditorTest.cs.)
There are three requirements that a collection should meet in order to be successfully persisted with the CollectionEditor
:
- First, the collection must implement the
IList
interface (inheriting from System.Collections.CollectionBase
is in most of the cases the best option).
- Second, it must have an
Indexer
(Item
in VB.NET) property. The type of this property is used by the CollectionEditor
to determine the default type of the instances that will add to the collection.
To better understand how this works, take a look at GetItemType()
function of the CustomCollectionEditorForm
:
protected virtual Type GetItemType(IList coll)
{
PropertyInfo pi= coll.GetType().GetProperty("Item",
new Type[]{typeof(int)});
return pi.PropertyType
}
- Third, the collection class must implement one or both of the following methods:
Add
and AddRange
. Although IList
interface has an Add
member and CollectionBase
implements IList
, you still have to implement an Add
method for your collection, given that CollectionBase
declares an explicit member implementation of the IList
�s Add
member. The designer serializes the collection according to what method you have implemented. If you have implemented both, the AddRange
is preferred.
When serializing with the Add
method, for every item it uses a new line.
this.tc.SimpleItems.Add(new Test.Items.SimpleItem_FullTc(-1, "Item1"));
this.tc.SimpleItems.Add(new Test.Items.SimpleItem_FullTc(-1, "Item2"));
When the designer uses the AddRange
method, all the items are added on a single line.
this.tc.SimpleItems.AddRange
(new Test.Items.SimpleItem[]{new Test.Items.SimpleItem_FullTc(-1, "Item1"),
new Test.Items.SimpleItem_FullTc(-1, "Item2")});
If you want to serialize a collection of nested items, like System.Windows.Forms.Menu.MenuItemCollection
, it is obvious that your collection must have an AddRange
method. (Check how ComplexItems
collection of the TestControl
is serialized.)
CollectionEditor, How to
(The accompanying source code is in the TestProject�s file CollectionEditorTest.cs.)
CollectionEditor
can be found in the System.ComponentModel.Design
namespace, but only if you add a reference to System.Design.dll in your project.
How to associate a CollectionEditor with a property
(See TestControl
class for an example.)
Declare and initialize a local variable of the type of your collection.
private SimpleItems _SimpleItems= new SimpleItems();
Create a read only property and add these two attributes: DesignerSerializationVisibility
and Editor
.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Editor(typeof(System.ComponentModel.Design.CollectionEditor),
typeof(System.Drawing.Design.UITypeEditor))]
public SimpleItems SimpleItems
{
get{return _SimpleItems;}
}
Now, SimpleItems
property will be edited with the CollectionEditor
.
How to add more than one Item type to the Collection
(See SimpleItem_CollectionEditor
class for an example.)
By default, the CollectionEditor
, adds only one type of item to the collection. Considering the above case, it will add only items of the SimpleItem
type (the type of the Item
property of the SimpleItems
collection). Since the SimpleItems
collection can hold all the items derived from SimpleItem
(SimpleItem_FullTc
, SimpleItem_BasicTc
, etc.), it would be nice to be able to add them too.
To do this, you have to inherit from CollectionEditor
and override 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;
}
}
Next change the Editor
attribute of your collection, instructing the designer to use your custom collection editor:
[DesignerSerializationVisibility (DesignerSerializationVisibility.Content)]
[Editor(typeof(SimpleItem_CollectionEditor,
typeof(System.Drawing.Design.UITypeEditor))]
public SimpleItems SimpleItems
{
get{return _SimpleItems;}
}
How to make CollectionEditor to edit a nested Collection
(See ComplexItemCollectionEditor
class for an example.)
If you try to edit a nested collection with the standard CollectionEditor
, most probably you will get an error. The error is caused by the fact that the designer is using only one instance of CollectionEditor
, and when you want to edit the collection of sub items, it tries to open the same instance that is already opened.
The solution for that consists in checking if an instance of the CollectionEditor
is already opened, and if so, create another instance:
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;
}
}
How can someone access the Items before they are added to the Collection
(See ComplexItemCollectionEditor
class for an example.)
Sometimes you may need to do some processing immediately after the items are created (setting a flag indicating that the item was created with a CollectionEditor
, setting a more appropriate name, etc.). In this case all you need to do is to override CreateInstance
function.
Here is an example:
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;
}
}
How to do some cleaning before an Item is destroyed
Of course, the best way to do that is to override the item�s Dispose
method, but this might not be always possible. In that case, you can override the DestroyInstance
method of the CollectionEditor
.
protected override void DestroyInstance (object instance)
{
base.DestroyInstance(instance);
}
CustomCollectionEditor
(You can find the accompanying code in the TestProject�s file CustomCollectionEditorTest.cs and the class implementation in CustomCollectionEditor.cs and CustomCollectionEditorForm.cs files of CustomControls project.)
As you probably noticed by now, the CollectionEditor
is perfectly capable of editing and persisting a collection during design time, and here is no need for another collection editor. However, in .NET, collections are a very convenient way of storing small pieces of data, and their use in the user interface of an application can be very rewarding. To make this possible, a tool that can be easily integrated in the application is needed. Of course, the first thought is to use the CollectionEditor
, but this raises various problems:
- You can�t call it directly, actually the only way to call it is through the
PropertyGrid
control.
- You can�t change its look, neither globalize it.
- You can�t control the way of how the user can edit the collection (
FullEdit
, ReadOnly
, AddOnly
...).
A new tool has to be created, and the most obvious thing to do is to copy the CollectionEditor
style since a lot of people used it, is already familiar with it, and it proved to be an excellent tool for editing collections.
The requirements for it are:
- to be able to directly edit collection in run time.
- to be very customizable (from the design point of view, and not only) and easily incorporable into the host application
- to maintain the facility with what the
CollectionEditor
can be tuned to edit different types of collections.
Convention: The term collection item will be used to represent an item in the collection and TVitem for the TreeView
node that CustomCollectionEditor
use to visually represent that collection item.
In order to satisfy the above requirements, it became obvious that there are two things needed: a Form
, CustomCollectionEditorForm
for run time editing, and a UITypeEditor
that wraps the form, CustomCollectionEditor
, for design time.
Compared with CollectionEditor
, CustomCollectionEditor
presents some new features:
- It allows you to edit all generations of a nested collection from the same window, by using a
TreeView
to display the collections tree. To be more specific, for each collection item it can display a collection of sub items, at your choice (in the case that your item has at least one collection of sub items).
- You can specifically set the name of the
TVitem
that TreeView
displays. By default, CustomCollectionEditorForm
will look to see if your collection item has a Name
property, and if so, it will name each TVitem
with the value of the Name
property. If the collection item doesn�t have a Name
property, it will name all TVitem
s with the collection item�s class name.
- It allows you to set different editing levels (
FullEdit
, AddOnly
, RemoveOnly
, ReadOnly
). An edit level can be defined for an instance of CustomCollectionEditorForm
, but it can also be defined for a collection type. The edit level of the form has prevalence over the collection edit level. You can specify the edit level for a CustomCollectionEditorForm
by setting the EditLevel
property to one of the above values of the CustomControls.Enumerations.EditLevel
enumeration. To set an edit level for a collection, you have to override the SetEditLevel()
function.
- For each collection item, you have a direct reference to the
TVitem
displayed in the TreeView
. Like this, for each TVitem
you can set properties like ForeColor
, BackColor
, Font
, etc., according with the collection item state.
- Finally, but not less important, it is open source.
Note: If you are interested in globalizing CustomCollectionEditor
, you�ll have to globalize the PropertyGrid
that it contains. Here you can find an article explaining how to do that: Globalized property grid.
How it works
Because all the logistic is implemented in CustomCollectionEditorForm
, it is here that you can set the desired behavior by overriding some key members:
protected virtual void SetProperties(TItem titem, object reffObject)
This procedure receives two parameters, one of type TItem
which is the TVitem
, and one of type object
which is the correspondent collection item.
Titem
is derived from TreeNode
, and has two new properties:
SubItems
Represents the sub items collection of a collection item that you want to display as child nodes of the TVitem
. All collection of sub items must be edited with CustomCollectionEditor
(by associating a CustomCollectionEditor
as Editor
, see the case of tc.CustomItems
).
Value
Represents the collection item associated with it.
By default this property sets the text for the TVitem
. Override it to customize even more the TVitem
.
This method is called every time a property of the collection item displayed in the PropertyGrid
is changed.
protected override void SetProperties(TItem titem, object reffObject)
{
if(reffObject is CustomItem)
{
CustomItem ci =reffObject as CustomItem;
titem.Text=ci.Color.Name;
titem.SubItems=ci.SpecialItems;
item.SelectedImageIndex=0;
titem.ImageIndex=1;
titem.ForeColor=Color.Red;
}
.
.
.
else if(reffObject is ComplexItem)
{
ComplexItem ci =reffObject as ComplexItem;
titem.Text=ci.Name;
titem.ForeColor=Color.Black;
titem.SelectedImageIndex=0;
titem.ImageIndex=6;
}
else
{
base.SetProperties(titem,reffObject);
}
}
protected virtual CustomControls.Enumerations.EditLevel SetEditLevel(IList collection)
It allows you to specify an edit level for a collection. Unlike the edit level of the form which is valid only for that instance of the CustomCollectionEditorForm
, a collection�s edit level is valid in all instances.
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)
It does exactly what it�s homolog from CollectionEditor
does, but because CustomCollectionEditorForm
can edit more than one collection at the same time, it takes an additional parameter of type System.Collections.IList
, to indicate the item types to be available for each collection.
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);
}
}
But what about the CustomCollectionEditor
? The dialog form that it opens is a CustomCollectionEditorForm
, so all that you have to do is to override its CreateForm()
function, and return an instance of your customized CustomCollectionEditorForm
.
public class CustomItemCollectionEditor: CustomCollectionEditor
{
protected override CustomCollectionEditorForm CreateForm()
{
return new CustomCollectionEditorDialog ();
}
}
Conclusions
The .NET framework has in CollectionEditor
, a powerful tool for editing and persisting collections at design time. For most situations, it is more than enough. But for more advanced scenarios, especially at run time, it can�t help you too much. It is here that CustomCollectionEditor
and CustomCollectionEditorForm
enter into action, providing you a straightforward and flexible way to cover those scenarios.
References
- Design-Time enhancement
PropertyGrid
articles
- General
Revision history
- Solved some problems of the
CustomCollectionEditor
- Solved bug with
ReadOnly
state
- Solved problems with Cancel button.
- The
SetProperties
method is called every time a property of the selected collection item is changed. The name of the TVitem
is now set here, so the SetDisplayName
method became obsolete and I removed it.
- Some corrections and modifications to the article's text.
- Original article.