Download EnhancedCollectionEds-DLLSrc.zip
Introduction
Implementing the basic CollectionEditor on is quite easy when the collection contains basic Types. The complexity increases a bit when you want to edit a collection of class objects requiring several Attributes and most likely a TypeConverter.
It escalates yet again when your class object inherits from an abstract/MustInherit base class. The base class cannot be instanced as the standard collection editor will tell you:
Bad Programmer!
Since the base class cant be instanced, it needs to be excluded from the collection editor. The normal solution is to write your own collection editor and specify the types allowed:
After doing this several times, I decided to develop a CollectionEditor base class to automatically detect and remove those abstract types. To make it more versatile, other capabilities were added that are periodically needed or wanted. The features in the resulting EnhancedCollectionEditor include:
- Automatically exempt abstract base classes
- Properties to tweak the collection form
- Ability to work with a variety of strongly typed collections
- Support nested/sub-collections
- Naming service(s) to provide unique names for new items
- Ability to invoke the
UITypeEditor
at runtime
Note that while filtering abstract base classes is an important aspect to the new editor, it works with any collection of classes, inherited or not. Above all, it is very easy to use, so you can use it in place of the standard NET CollectionEditor simply because you want to change the dialog form caption.
Context
It is near to impossible to explain how to implement a CollectionEditor without mentioning serialization and TypeConverters. There are perhaps more things to implement correctly on the collection owner as in a new collection editor. Since this is intended more for the novice and intermediate skill level, I did not want to write "be sure to use the correct TypeConverter" without explaining what that means or what is involved. So, prior to introducing the new collection editor, this article will review the requirements for the type (class), collection and designer serialization.
An excellent article on this topic comes from Daniel Zaharia: "How to Edit and Persist Collections with CollectionEditor" which presents an overview of collections and serialization -- it is a must read. The present article updates a few points from that article, clarify some others and expands on aspects particular to inherited classes and more in the format of a practical application than overview. Above all, the context is in Visual Basic to make it accessible to a wider audence. Reading Mr Zaharia's article, if you haven't already, is advised.
The Collection Item Classes
The demo includes several class sets each implementing a different type of inheritance or using a different collection type for storage. Each are stored in their own source file, XItems, ZItems and XooItems. A synopsis of ExtendedItems is shown below.
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
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
The ExtendedItem set is perhaps the most complex. it will control the Index
property, requiring it to be sequential and therefore read-only in the editor; a later class set will require item names to be unique. The inherited FooBarItem itself is host to 3 sub-collections: Foos, Bars and a collection of ZItems (Zoey, Zacky classes inheriting from Ziggy).
Notes:
- If your class does not implement a
Name
property, the type name will display in the ListBox on the CollectionForm
. The above ValueItem would display as "Plutonix.Test.ValueItem". An alternative when you do not need a Name is to override the ToString
method to return something to identify this item. When these are missing, the Type name is used.
- All CollectionEditors require a simple constructor (a
Sub New()
with no arguments), since the Editor can't know what values to pass or which one to use. You can add other constructors to expedite or simplify creating.
Choosing a Collection
The first consideration is how you want to store the items. The main consideration needs to be the needs of the application you are developing, but as Mr Zaharia points out, your collection must implement IList
, must implement the Add method and an Item property (indexer in C#).
As you will see, Item must be supported because NET needs it to be able to determine the type(s) your collection contains (as will we, it turns out). Add is required so the designer (VS) can deserialize (read/reload) your collection items. The most common collection types are:
Collection(Of T)
(from System.Collections.ObjectModel)
List(Of T)
(from System.Collections.Generic)
CollectionBase
(from System.Collections)
List(Of T)
is curious: since it is a container, it allows you to use a List(Of T)
variable as if it were a proper collection class because it meets all the requirements. These allow you to get a collection class implemented very quickly. The downside is that it also allows the collection variable to be set to Nothing and various methods accessed which you may not want. (The demo exposes 5 top level collections with several sub collections, two trivial subcollections use a List(Of T) variable in the interest of expediency. The ExtendedItems collection follows the above advice closely.)
When your collection class inherits from Collection(Of T)
, the required Add and Item members will already exist (as will other useful methods such as Contains and IndexOf). When inheriting CollectionBase
, you will have to add these members yourself. Be sure to specify the return type and implement Item as a Property -- it will work in your code as a function, but will also confuse the collection editor.
MSDN offers more Do's and Don'ts and other advice in Guidelines for Collections which generally prefers Collection(Of T)
.
Finally, your collection needs to be exposed through a property on the main class (NuControl in the demo):
Public Class NuControl
Inherits Panel
Implements ISupportInitialize
Friend XTDItems As New ExtendedItems
<DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
<Editor(GetType(CollectionEditor),
GetType(System.Drawing.Design.UITypeEditor))>
Public ReadOnly Property ItemExtenders As ExtendedItems
Get
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
Notes
- The collection absolutely must be instanced so the editor has somewhere to store new items you create. Since we will be adding items to the collection at design time, we need a 'design instance'.
- The
Editor
attribute designates the UITypeEditor
to use to edit this property. For now we are using the standard NET CollectionEditor
- The class for the items going into the collection must implement a simple constructor (
Sub New()
with no arguments) because the collection editor would not know how to use one which takes arguments.
- There are other typed collections which work, such as
ObservableList(Of T)
, but the VisualBasic.Collection
is not among them: it is not typed and returns read-only Objects
making it unsuitable.
- To use Collection(Of T), add a reference for System.Collections.ObjectModel and import it. This will allow Visual Studio to offer it in the autocomplete and not confuse it with the VB Collection.
A number of messages on various forums indicate that you must not include a setter for the collection property or you will have serialization problems. This is not true. MSDN has several examples implementing a setter. What is true is that the CollectionEditor does not use the setter to return the new collection to you, nor does the designer. It is also a bad idea to implement a setter since it will allow your collection to be set to Nothing. To avoid this, you can make the property ReadOnly, or use an empty Setter.
A little too much is sometimes made of this because a ReadOnly collection property simply prevents your collection object from being set to Nothing. The collection class you expose very likely also has methods to Clear and Remove items. This is not trivial though, because it is one thing for items to be removed, and quite another for your code to have test if the collection Is Nothing before each collection reference.
The ShouldSerializeXXX
function (where XXX is the name of the collection property) is how the designer (VS) determines when the property has changed and should be serialized. When there are items in the collection, the return from ShouldSerialize
provides the indicator to save the collection contents. Private
or Friend
is no matter, VS will find them (Private
makes more sense). This procedure also controls whether or not your collection will display in bold type in the property window when they contain items. As your project evolves, be sure to update the procedure names if/when the collection property name changes.
Not surprisingly, code in those forum messages warning of the property setter, rarely have ResetXXX
or ShouldSerializeXXX
implemented.
Serialization
I'd like to cover serialization as a separate topic -- or even article -- but you must implement this as you go along or you will get various error messages from Visual Studio that you are a bad programmer. Serialization - more accurately designer serialization - refers to saving the collection data to the form designer. This is the (formname).designer.vb
for VB forms. This is different from XML or other serialization. In his article, Mr. Zaharia refers to it as persisting the collection. A closer look at designer serialization may help you understand the process and diagnose serialization problems.
At various times, VS will write the contents of the collection out to the designer file. The return from your ShouldSerializeXXX
function is the trigger for serializing your collection items to the same file. The CollectionEditor does not pass the collection back to your class via the property setter (if there even is one) because there is nothing your idle class code can do with it. Remember, this is taking place at design time.
When you click Add in the editor, a new item is added to a copy of your collection. At the same time, VS marks the form designer as dirty, but it doesn't add items one by one. If you cancel a collection edit session, a copy of the original collection is used.
For persistence or designer serialization, when you exit the collection editor, VS adds code to the designer file for the new state of your collection, then runs it essentially rebuilding the form and your collection. The designer code is in the 'mysterious' InitializeComponent
procedure you see in a form's Sub New. The code you see there is the code to deserialize your form, the associated controls and ultimately, your collection:
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)
This is how VS adds items to your collection: it uses the .Add method of your collection.
The designer code runs at various times (Clean, Rebuild - which is why it flickers). As it rebuilds the form and controls, it also rebuilds your collection - that is, deserializes it. This process can reveal serialization problems rather quickly: If you have ever had the case where you could add objects to your collection and they would remain available in the collection only to disappear when you rebuilt or reloaded the project, it is because they were not serialized. The items remained in the collection only until it was rebuilt.
Notice the Add statement. XTDRItems is just a property, so an Add method on a property can look like madness (or magic). In this case, XTDRItems is just a property wrapper for an underlying class which does implement that method. The important point is that without an available Add method, your collection cannot be rebuilt or deserialized. If your collection class implements AddRange
, VS will use that to deserialize your collection. I tend to leave this off until later to make it easier to examine the designer serialization code for the various properties.
Serialization Requirements
Serialization must be accounted for as you develop the infrastructure for your collection and collection editor. The moment you exit the CollectionEditor, VS will need to serialize the contents. This is what we need to make our collection class serializable:
- As already described, the collection property (XTDRItems from the demo and above, using the
Editor
attribute) must have the DesignerSerializationVisibility
attribute, this time using the Contents
setting. The collection is an Object
so it cannot be serialized, but we can serialize the contents of the collection.
- The collection property must also include
ShouldSerializeXXX
and ResetXXX
as described.
- The item class intended for the collection must be marked with the
Serializable
attribute. For classes inheriting from an abstract class, you can mark the base class as Serializable
on behalf of the inherited types. This works because serialization works on types: an object which is of type TextItem for instance, will also be of type ExtendedItem. Alternatively, you can mark each class.
- This will eliminate the "<TypeName> is not marked as serializable" error.
- Since the collection itself cannot be serialized, this is handled through the Item Class. This will almost certainly require a TypeConverter (covered shortly).
- The Item Class properties must be tagged with the
DesignerSerializationVisibility
attribute, using either Visible
or Hidden
depending on whether it is used by the TypeConverter or not .
Example:
<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
The DefaultValue
attribute shown above does not do what you probably think it does. Rather than provide a default value, it works like ShouldSerializeXXX to inform the designer to serialize a property when the value varies from the specified default. It also sets the collection editor PropertyGrid display to bold when the current value does not match the default. If the attribute is not used, it appears that the VS Designer assumes String.Empty
for text, 0 for Integers
etc.
For all but the very simplest classes, NET requires the item being serialized to provide the information needed to write the code for the designer file. This is done using a TypeConverter, but while we are knee-deep in the designer file, there is a helper may want to know about.
It is worth noting that the designer code will add the items to your collection in the order specified in the CollectionEditor. You may have noticed with the DataGridView
that as Columns are added, each index is initially zero. Only once you save and revisit that CollectionEditor does the index represents the correct position. This is done in the collection's Add method (see lines 155-161 in the MS Reference Source):
int index = this.items.Add(dataGridViewColumn);
dataGridViewColumn.IndexInternal = index;
We noted that we want this capability for the Index
property on the items in our ExtendedItems collection class. Since the user cannot control the Index
, it uses the ReadOnly
attribute. Ok, there is no magic involved in controlling the Index
to make it sequential, but how can we perform more elaborate checks or actions?
ISupportInitialize - Handy Helper
This is a very, very handy interface for designing or subclassing controls. In cases where you need to qualify or validate one property setting against another, Sub New
is not the place to do it because the user's design time property values have not yet been set. One "trick" you see occasionally is to perform initialization in the HandleCreated
event.
ISupportInitialize
provides a better solution. When implemented, the designer will call a new BeginInit method on your class before any properties are set, then a new EndInit method after all the properties are set. So, in cases where you must qualify one property against another, EndInit is the perfect place to do so - Visual Studio is done setting properties on everything, so you don't have to worry about one of them being Nothing
. The designer code shows how this works:
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 is called both at runtime and design time. This is another place we could set the Index
for the items in the collection (but it seems better in the Add method before it is ever even in the collection). However, other requirements could benefit greatly from EndInit. Consider a collection with interdependent references with ItemA indicating a relationship to ItemB thru an index reference. EndInit is a great place to find ItemB by name for instance, then check or set an index value, because the complete collection is available when the method is called.
To implement ISupportInitialize
:
Partial Public Class NuControl
Inherits Panel
Implements ISupportInitialize
In more recent VS versions, pressing enter after ISupportInitialize
adds the 2 required methods, BeginInit and EndInit.
You can extend this functionality to other classes several ways. You could add similar methods on other classes which are called from NuControl.EndInit to perform similar actions. An extended version of the interface, ISupportInitializeNotification
allows other components to be notified via a new event.
The point of this side excursion into ISupportInitialize
, is that many things you may feel you need to do in the collection editor - like force an Index
property to be sequential - are better and more easily handled once the collection - and everything else - is initialized.
At this point, Visual Studio knows what we want to serialize. Next, we need to tell it how we want to serialize it. If we don't, VS will try to do the best it can and probably fail. Perhaps the simplest way to serialize your class items, is to have each collection item inherit from Component
, or implement IComponent
:
Public Class Foobar
Implments IComponent
This is very simple but it comes with overhead: VS will require code to support IDispose
(which you may not need), and it will also drag things along like ISite
and GenerateMember
. Your class items will also show in the form tray by default. On the other hand, it will enforce a unique name and handle designer serialization for you. If you want to avoid learning about TypeConverters a while longer, you can just do this. Refer to Mr Zaharia's article and demo where one of his collections does just that.
The rest of us will be over here learning that a minimal TypeConverter is not alchemy after all.
The TypeConverter
TypeConverters are all around you all the time. They can convert "Red" as property value to Color.Red
; or and X and Y value to the Location
of your form. Some are automatic: the ValueItem in XItems includes an Enum
property which VS will convert to use the Enum
names in the DropDown in the property window. The demo implements an EnumConverter to show how to provider even friendlier text for the DropDown (see ValueEnumConverter for ValueItem in XItems).
MSDN's sample TypeConverter project is comprehensive, but makes them look more complex and daunting than they are (and certainly more complex than ours needs to be). Even the previously mentioned article is slightly more ambitious than we need.
Here, we will present the minimum you needed to implement a TypeConverter in step by step fashion. This is an important concept to grasp because at the conclusion of this article you will have an easy to use EnhancedCollectionEditor, but in order to use it in projects, you will need to know how to add a TypeConverter for the collection class items.
Recall that we decorated the collection property with DesignerSerialization.Contents. Essentially, we told VS to not even bother with the collection object, but do worry about what is inside it. Having done that, we now have to provide the means to serialize the objects inside by means of a TypeConverter. Your TypeConverter will help Visual Studio to produce this for the designer file (first 2 lines):
Dim Ziggy1 As NuControl.Ziggy = New NuControl.Ziggy("NewZiggy", -1, "ZiggyName")
Dim Zoey1 As NuControl.Zoey = New NuControl.Zoey("NewZoey", 7)
...
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)
There is a rather clever aspect to this. When serialization code is needed, the instance of each collection item is polled to provide VS with the information required to recreate item. That is, VS is essentially asking, 'how would I go about recreating you just the way you are now?' The TypeConverter you write responds with constructor information and the actual values for any construction parameters.
These are associated with classes (Types) using the TypeConverter
attribute:
<Serializable, TypeConverter(GetType(ZoeConverter))>
Public Class Zoey
Inherits ZItem
In the designer code above, notice that Ziggy and Zoey each use a different constructor. Each of the ZItems in the demo uses a different constructor to provide different TypeConverter examples. Attributes
in general are not inherited, but as with the Serializable
attribute, the TypeConverter
applies to a Type. As a result, any TypeConverter associated with the base class will apply to the inherited classes (again, because an item which is type Zoey is also type ZItem.) The ZItem classes do not do this because they each (artificially) use a different constructor, so they require a different TypeConverter.
Note that we can specify the TypeConverter to use by name: <TypeConverter("ZoeyConverter")> but when doing this, VS can't inform us if the name is misspelled as it can when the wrong Type is specified.
A minimal TypeConverter only needs to implement 2 methods: CanConvertTo and ConvertTo. The first simply returns True
when VS queries the object to see if it can convert the current item to a specific type (String
etc) - in this case we will need to provide an InstanceDescriptor.
ConvertTo can look convoluted since it works through reflection using unusual terms, but the key is in just a few lines of code:
Friend Class ZoeConverter
Inherits TypeConverter
Public Overrides Function ConvertTo(context As ITypeDescriptorContext,
info As CultureInfo, Value As Object,
destType As Type) As Object
If destType = GetType(InstanceDescriptor) Then
Dim z As Zoey = CType(value, Zoey)
Dim ctor As Reflection.ConstructorInfo
ctor = GetType(Zoey).GetConstructor(New Type()
{GetType(String), GetType(Integer)})
Return New InstanceDescriptor(ctor,
New Object() {z.Name, z.ZCount}, False))
End If
Return MyBase.ConvertTo(context, info, value, destType)
End Function
End Class
Step by Step
Assuming this is one of your first TypeConverters, let take it step by step: The Item being converted for serialization is an item in your collection, with a Name of "NewZoey" and an Index
value of 7 (see the VS designer code above). Since it is a Zoey object, the ZoeConverter above is used. The converter already returned True when asked if it could provide an InstanceDescriptor
for a Zoey object, now it must actually do so - hopefully the correct one.
- The
Value
parameter passed in ConvertTo is the Object (instance of Zoey) being converted/serialized so CType
is used to cast it to the correct Type
. This will be used shortly to provide the constructor values.
GetConstructor
creates a ConstructorInfo
object which matches the desired signature for the type we are working with (Zoey)
- signature refers to the data type(s) and order of arguments in the constructor.
- The code specifies which constructor to use by passing a
Type
array containing the data types in the order needed.
- Any CollectionEditor will require a simple (no params) constructor for creating new items, you may have one you use in your code and will often add a constructor specifically for your TypeConverter. So, you will often have more than one constructor to pick from, be sure to specify the correct types and type order of arguments.
- The Type array in this case contains
String and Integer
types so in VB lingo the code is requesting this constructor:
Sub New(Name As String, Index As Integer)
- The converter prepares to return an
InstanceDescriptor
using the constructor descriptor we just created
- Next, it uses an object array filled with the actual argument values which we get from the instance object passed (and converted to the correct type). In this case,
z.Name
("NewZoey") and z.Index
(7). These values would have been recently entered via the CollectionEditor.
- The final
Boolean
argument is whether or not this object (collection item) is complete. This is covered next.
The VS designer converts what you give it to text for the result you see in the designer file (and it could not be correct without your TypeConverter):
Dim Zoey1 As NuControl.Zoey = New NuControl.Zoey("NewZoey", 7)
When you become familiar with the process, you can collapse the code.:
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
There are many types of TypeConverters (such as the EnumConverter mentioned) in the NET Framework, and they can be very useful. One worth mentioning is the ExpandableObjectConverter. These are used with properties with more than one value - such as a Point
(x and y values) or a Size
(width and height values). You can use a converter which inherits from ExpandableObjectConverter to expand your own objects in the Property Window.
Another interesting source for learning about TypeConverters comes from the MS NET Reference source. This link is the ListView
ColumnsHeaderCollection
TypeConverter.
Handling the Other Properties
But what if a class has 8 or 10 properties? You may miss it the first time you study Mr Zaharia's article, but you do not need to create a complex constructor and handle all the class properties through your TypeConverter. The TypeConverter should use the simplest constructor possible which provides the arguments the class must know upon creation or instancing. If only the Name property is essential then use a one argument constructor and let the rest be set as normal properties in the designer.
But what is the "normal" way and how do we do that? First if there are other properties to set besides those handled in the constructor, set the last argument to InstanceDescriptor
above to False
. This tells VS that the object is not complete and as a result, VS will go hunting for other properties tagged as DesignerSerializationVisibility.Visible
. Set any properties used in the constructor to Hidden
since those have already been handled ('Hidden' actually means don't do anything):
<DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
Public Property Name As String
<DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
Public Property Index As Integer
<DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)>
Public Property ZBar As String
<DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)>
Public Property ZFoo As String
TypeConverters are class specific and not terribly reusable. However, a converter for classes which use a parameterless constructor can all be the same. All the XItem classes use a constructor requiring only the name. Since they also all inherit from ExtendedItem, they can share the same TypeConverter with simple changes:
Return New InstanceDescriptor(value.GetType. _
GetConstructor(New Type() {GetType(String)}),
New Object() {CType(value, ExtendedItem).Name}, False)
Instead of GetType(Zoey)
, using value.GetType
for GetConstructor resolves back to the actual type being created. Similarly, converting Value
to ExtendedItem
(required under Option Strict
) for the Name
property value works because it is defined in the base class and therefore is present for all the inherited classes. It would not work for properties unique to an inherited class.
If you botch the TypeConverter, VS will do the best it can. Sometimes it will convert the collection items to a Base64 string and store it in the resource file, then fail when trying to deserialize it into a proper class object. If you drill into the errors, it will point to a <Data>
entry for the resx file (or Properties | Resources). Other times it may post a normal string to the resource file but still not be able to deserialize. Your designer file will include lines like this:
Dim resources As System.ComponentModel.ComponentResourceManager = New
System.ComponentModel.ComponentResourceManager(GetType(Form1))
Me.NuControl1.ZCollectionBase.Add(CType(resources._
GetObject("NuControl1.ZCollectionBase"), NuControl.ZItem))
This works just often enough that it sometimes seems like you do not need a TypeConverter for the item class. But eventually it will fail, often only after you reload the project into VS.
If you start to get odd CodeDom serialization exceptions: Stop. Figure out what is wrong with your serialization process, now (if allowed to continue, it can get so bad that you may be forced to recreate the Project and Solution files). This can happen as soon as your CollectionEditor starts to do something useful, so it may seem that the problem is with the editor and it can be tempting to tinker with - er, that is debug - the CollectionEditor code. But the real problem is more likely with your serialization methods.
This was meant as an overview of the TypeConverter's role in serializing your collection items and how to detect problems by examining the designer file - not a detailed look at designer serialization. For instance, the CollectionEditor will also use your TypeConverter each time you add an item - add Console.Beep
to the ConvertTo procedure and see how often it is called. You can also add a beep to EndInit to see how often and when the form and your collection are deserialized and reconstructed.
The key elements above are presented as a checklist in an Appendix.
Test What You Have
We seem to have everything we need: the collection class - ExtendedItems - is instanced and decorated for serializing; the classes it will contain are written and decorated for serialization; and the collection class is decorated for a Editor
and we have the TypeConverters. Lets see if VS/.NET/VB approves so far.
Recall that I left the property to use the standard .NET collection editor:
<DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
<Editor(GetType(CollectionEditor),
GetType(System.Drawing.Design.UITypeEditor))>
Public Property XTDRItems As ExtendedItems
There are 2 reasons for this: mainly, if the standard NET CollectionEditor doesn't work right, there can be no question that there is something amiss with the collection related code and not something in any custom collection editor. Conversely, if things start to fail after we start on the UITypeEditor, we will know it is there.
Compile your project, drag/draw an instance of NuControl on your form if needed, cross your fingers, then open the property window. The XTDRItems property should look like this:
The (Collection)
portion indicates that VS/NET recognizes XTDRItems as a valid collection, and the Ellipses (...)
indicate that VS/NET knows it can be edited. If not, most likely the IDE has no access to an instance of the collection or the collection class is improperly implemented.
If you are having problems, one way to narrow down where the problem is, is to change the property to expose the collection's inner list (Items for example). Recompile after all such changes, then check the entry in the property window. If it now shows, you know you have something wrong in your collection class. Be sure to change it back to protect your collection from being replaced or cleared outside your code.
Also check the property's ' As Type
', but if that is wrong, you are not using Option Strict
and deserve to go looking for snipes for an hour or so.
ControlDesigners / DesignerActionLists
There is one more step if you are implementing access to your collection via a DesignerActionList (those drop down panels on the upper right of the control iteself). The properties and actions presented on the panels are essentially pass-through properties and methods:
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
<span style="font-size: 9pt;"> End Property
...
</span><span style="font-size: 9pt;">End Class</span>
.NET invokes the properties and methods on your ActionList class which in turn gets the value using an instance of your class object. If your class uses a custom collection editor and you want - or need - to use it via the Smart Tags you will also have to decorate this property declaration like you did the one on your class:
<Editor(GetType(StartItemEditor), GetType(System.Drawing.Design.UITypeEditor))>
Public Property Items() As StartItems
...
DesignerSerializationVisibility is not needed because we are not saving data for this object, but you will need the Editor Attribute for any custom CollectionEditor. You can reference the same one used on your actual class property.
Another alternative is to create a DesignerActionListMethod which appears to be what MS does for the various "Edit Items..." entries.
The new EnhancedCollectionEditor inherits from the NET CollectionEditor then adds additional functionality via property options. This will not go into the new editor's DNA, but the interesting or major aspects we will examine are:
- Determining the collection type
- Determining the type(s) it can contain
- Filter out any abstract base class
- Override a few standard functions
- Tweaking the CollectionForm
Getting the Contained Types
The first step is to get the class collection Type
passed in the constructor, then the type it contains:
Public Sub New(t As Type)
MyBase.New(t)
myType = t
If MyBase.CollectionType Is GetType(Microsoft.VisualBasic.Collection) Then
DisplayError("VB Collection is not supported.")
Exit Sub
End If
mycolBaseType = MyBase.CollectionItemType
If mycolBaseType Is GetType(Object) Then
mycolBaseType = GetCollectionItemType(t)
End If
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))
Exit Sub
End If
If (mycolBaseType Is GetType(Object)) Then
DisplayError("Type [System.Object] is not supported.")
Exit Sub
End If
End Sub
- The Type T passed is whatever your property getter returns, the ExtendedItems class in this case. Note that this is the collection class, not the collection of items.
- This specifically looks for and rejects the VB Collection type since it comes thru as
Object
and no contained type can be determined (they too, are Object
).
- NuControl includes 5 top level collections in the demo. If any of them throws an exception in the constructor, they can all cease to work, so a MessageBox based error display is used.
- There are several ways to get the
Type
contained in the collection. The simplest is to use the CollectionItemType
property. This will return the base class type such as ExtendedItem in this case. The CreateCollectionItemType
function is similar: it searches the properties for an indexer (Item
property) to return the type; where CollectionItemType
returns a cached value (see MS Reference Source lines 213 - 220 vs 2328 -2338).
- VB is often too forgiving for your own good especially with
Option Strict
off. If you leave off the As <Type>
from a property, it will be an Object
type, which will not sit well with the collection editor. So there are traps for this as well.
Recall that I said is possible to define your Item accessor as a function and have it work fine in your code. When using a Collection(Of T)
, the base class implements an Item property, so the correct type can be identified. But both NET methods to get the contained type will fail for a collection class which uses CollectionBase
and implements Item as a method because there will be no indexer (Item) property. So, if all else fails, the GetCollectionItemType procedure is provided to check for an Item property or method, and scold you should you use it as a method.
Refining the Type List
Next, we want to create a list of all the legal types - TextItem, ValueItem etc - which the collection can contain, but we must filter out any abstract base class. This is done by overriding the CreateNewItemTypes function.
The "normal" way to do this is to simply tell the CollectionEditor the legal types by returning an array of types (classes) this editor can work with. But this is not the least bit reusable because the Types will be different for each collection. Besides, as the saying goes, "Why spend 15 minutes writing code to do something when you can spend two hours writing a system to do it for you?" The core code is:
Protected Overrides Function CreateNewItemTypes() As Type()
Dim ValidTypes As New List(Of Type)
If mycolBaseType.IsAbstract Then
Dim allTypes As Type() = Assembly.GetAssembly(mycolBaseType).GetTypes()
For Each t As Type In allTypes
If t.IsSubclassOf(mycolBaseType) And (t.IsAbstract = False) Then
If (ExcludedTypes IsNot Nothing) AndAlso
(ExcludedTypes.Contains(t) = False) Then
ValidTypes.Add(t)
End If
End If
Next
Return ValidTypes.ToArray()
Else
Return MyBase.CreateNewItemTypes
End If
End Function
The code comments should make clear how we are filtering out the base class: everything that inherits from mycolBaseType and itself is not an abstract class is okay. It also allows all concrete types thru, so the resulting editor can be used when there are no abstract classes in use. The System.Type
class has a rich set of procedures which makes this easy. The returned array contains only the types which the base CollectionEditor will use and attach to the Add button:
TaDa! - no ExtendedItem
base class and no scolding
Single Item Select
You are encouraged to use the VS Object Browser to explore the various methods and properties exposed by the NET CollectionEditor class if you want to jazz it up a few things. For instance, you can prevent the user from selecting multiple items in the ListBox
very simply with:
Protected Overrides Function CanSelectMultipleInstances() As Boolean
Return False
End Function
Modifying the CollectionForm
In one project, I took the time to add Description
attributes to properties for display in the PropertyGrid
help panel - then discovered the panel is off by default. The EnhancedCollectionEditor will allow you to toggle it via a property. This is implemented by getting a reference to the propertyBrowser control and toggling the property:
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
_propG.HelpVisible = ShowPropGridHelp
End If
EditorForm.Height += 40
EditorForm.Text = FormCaption
Return EditorForm
End Function
It is very simple once you know the form layout: See the MS Source Reference around lines 1233.
The main TableLayoutPanel
is the first control added to the form, so you could use a reference such as EditorForm.Controls(0).Controls(5)
to get the propertyBrowser reference, but magic numbers creep me out. The EnhancedCollectionEditor will allow you to get a references to any of the main controls on the form (by name). The demo includes an example; the names are exposed as constants and visible in IntelliSence.
Note that when your collection items are added to the ListBox, they are wrapped in an internal ListItem class (see lines 902-904), so trying to "help" edit the values with a reference to it may be more difficult than simply hooking to the PropertyGrid
value changed event. The EnhancedCollectionEditor also provides the means for you to subscribe to the propertyBrowser.PropertyValueChanged
event.
Using the EnhancedCollectionEditor
The final result is an abstract/MustInherit base class with several features and options to make it easy to use. And it will handle both abstract and concrete classes. Before we look at the more advanced capabilities, here is how to use it:
- create a class inheriting from the new base class
- If you are using the base editor from a DLL, be sure to add a reference and import
- Set the properties to activate any desired behaviors in the constructor:
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
The only other thing is to specify it as the editor for your collection:
<Editor(GetType(ZItemCollectionEditor), GetType(System.Drawing.Design.UITypeEditor))>
Public ReadOnly Property ZItemCollection As ZItems
This is literally all you need to create a customized editor. All the collection editors used in the demo are contained in CollectionEds.vb. But it does a bit more...
Properties
FormCaption - The text to show on the CollectionEditor form's title bar
ShowPropGridHelp - A Boolean whether to show the property grid's help panel.
AllowMultipleSelect - Whether multiple items can be selected in the editor's ListBox
UsePropGridChangeEvent - When set to true, a PropertyValueChanged
event is fired as property values change in the editor's PropertyGrid (details next).
GetControlByName - Provides a control reference for a control on the CollectionForm allowing you to attach event handlers to perform extended operations. The control names are defined in the base editor class which allows Visual Studio and IntelliSense to help. The names are: listbox, downButton, upButton, okButton, cancelButton, addButton, removeButton, propertyBrowser. To get your editor class will need to subscribe to the EditorFormCreated
event - see the example in XTDItemCollectionEditor.
NameService - Determines the item naming service you wish to implement to assure unique names (details below).
ExcludedTypes - A Type
you wish to exclude from the EnhancedCollectionEditor (see below).
DisplayError - A simple MessageBox wrapper you can use to debug and test your collection editor class.
BaseCollectionType (ReadOnly) - Returns the type passed to the editor (that is, the collection - ExtendedItems, ZItems etc). Your editor wrapper ought to know what type it is working with, but since one editor can handle multiple types, it may be helpful in determining the type for this instance.
BaseItemType (ReadOnly) - Returns the collection item type: ExtendedItem, ZItem, Xoobar etc. This can be an abstract type.
Note: BaseCollectionType and BaseItemType are of minimal value. If you are trying to define one editor for two types and try to use these to implement conditional logic, it may not work as expected. It would be better to define a separate editor for each type. They are included for completeness and because someone, somewhere might have a semi-legitimate need to know.
UsePropGridChangeEvent
When set to true, the EnhancedCollectionEditor will forward the PropertyValueChanged
event for the propertyBrowser control on the editor form. This prevents you from having to find it and hook it directly to access your items as the properties are edited:
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)
End Sub
Note that many things you might wish to do here may be much more easily accomplished in an EndInit procedure from ISupportInitialize. Not only will you have very limited access to the collection information, many things you might change/set, the user can re-edit
ExcludedTypes
This allows you to further refine the types which can be added to the collection. Assume a situation using the class set of {Ziggy, Zoey, Zacky}
where Ziggy is a legal member but perhaps only at runtime when certain required information is available. Or maybe the Ziggy Type can't be defined as an abstract class for some reason but is acting as one and therefore not meant to be instanced .
Since Ziggy is a legal Type
for the collection, just not yet or in this situation, when inheritance is used, the type would be available in the editor. In a fringe case such as this, any type can be excluded from the editor:
Public Sub New(t As Type)
MyBase.New(t)
MyBase.FormCaption = "Ziggy-less ZItem Collection Editor"
MyBase.ShowPropGridHelp = True
MyBase.AllowMultipleSelect = False
MyBase.ExcludedTypes.Add(GetType(Ziggy))
End Sub
The result is a Ziggy-less ZItem editor
ExcludedTypes is a List(of Type) to which you add those types to ignore, but this is likely a rather fringe/niche situation (unless you really need it).
NameService
The new EnhancedCollectionEditor class also provides support for unique naming of items. This is usually of more importance for components than collections, but since a Name
property is common on collection items, they often do take on some importance. It should be obvious, but using this requires a Name
property be present which is not read-only; the editor will scold you when it is not.
While parts of NameService are based on the same premise as ISupportUniqueName
presented in the previously mentioned article, this implementation is much more economical since it does not require each new item to have an entire copy of the collection.
The EnhancedCollectionEditor base class provides several ways to create a new name, using the NameService
property:
None - Nothing is done regarding names. (default)
Automatic - The EnhancedCollectionEditor automatically creates a new name based on the TypeName and current designer host collection count. e.g. For Plutonix.Test.XooBar with 3 items already created, the next one would be "XooBar4". This is the easiest because you just have to set the property in your collection editor.
NameProvider - Use this value when you want your code to provide names; you will need to implement INameProvider
(and interface exposed in the UIDesign.DLL) on the class which provides the collection property. When new items are created, the collection editor will call the required GetNewName method to obtain the new name.
The NameProvider method can be implemented two ways. Consider this class structure from the demo:
- NuControl provides a collection property named XooBars
- XooBars contains XooBar items with unique names
- Each XooBar item can include a (sub) collection property of ZItems also with unique names
When you add a new XooItem in the editor, the XooBars collection class is queried first to see if it implements INameProvider
. If so, the required GetNewName method is called to get the name. Since the collection will be the 'owner' of the new item, it knows the most about the current state of the collection, so it is polled first to provide the new name.
If XooBars does not implement INameProvider, then NuControl is queried to see if it does. This provides a second chance for several reasons. First, not all collections can implement an interface (such as a List(Of T)
variable, which are very expedient). Also, since NuControl hosts 5 (so far) collections; rather than implement INameProvider
on each class, the demo could do so only on NuControl and simply dispatch calls to other class methods for the new names.
In the case of the ZItem sub collection, first the collection class would be queried, then the XooBar item active in the collection editor. ZItem actually uses Automatic naming, but the code is there for NameProvider: just change the property setting in the collection editor.
Notes:
- NameService is specified in your editor class, so it can vary collection to collection. In the demo, XooBars is set to use
INameProvider
for new XooBar items while the ZItem sub-collection uses Automatic.
- XooBar is also constructed to emulate the style of
IComponent
with the Name
in parentheses and set to ReadOnly in the property editor for illustration purposes. (The Name
property is not ReadOnly on ZItems because as you have seen, the class is used frequently in the demo for sub collections.)
- In Automatic, the names are designed to be unique, not sequential. For
INameProvider
, your code supplies the rules and logic.
INameProvider
is part of the UIDesign Namespace
- There is an
INameCreationSerice
interface in .NET, but this seems to be more intended for Components
.
Events
The EnhancedCollectionEditor includes several useful events:
EditorFormCreated
This event is raised when the CollectionForm is created. If you need to add event handlers to controls on the form, this is where you can do it. Prior to this event, the form - and therefore its controls - do not exist. You can also make other changes to the form, such as setting the BackColor
. You should not store a reference to the form.
Event EditorFormCreated(sender As Object, e As EditorCreatedEventArgs)
The editor form reference is available via e.EditorForm
NewItemCreated
Event NewItemCreated(sender As Object, e As NewItemCreatedEventArgs)
This poor thing has been in an out of the project in various forms several times, until I actually needed it - now it is there to stay. This is called when the user presses the Add button in the Editor, but before the Automatic NameService
method to create a name is called.
The event must be handled in the editor you write, and there is not a lot you can do from there such as access the collection data. What you can do is change the Base Type Name to be used for Automatic naming or simply to soften or tweak the Type name to be used in the Editor.
In my case, I was trying to not the confuse the user. They could define collection items at design time, but these were a subset of the actual Type actually used at runtime and quite different. So, I changed the base name derived from the Type from StartUpItem to CheckItem (and yes, the real typename shows in the designer code). Valid uses for this are marginal and it is not used in the demo.
PropertyValueChanged
The event from the Editor Form's PropertyGrid. As the most likely control and event you might want access to, the collection editor passes the event thru to your code making it easy to subscribe to the event. The event only fires if UsePropGridChangeEvent is True
.
Protected Friend Event PropertyValueChanged(ByVal sender As Object,
ByVal e As PropertyValueChangedEventArgs)
Nested/Subcollections
Nested collections are not handled any differently. Designate the collection editor to use with the Editor
attribute on the collection property, make sure you have a TypeConverter and have things marked as Serializable
.
The biggest difference is when using the NameProvider naming service for these: the 'second chance' call for the NameProvider method would go to the 'property provider': that is, the class hosting the sub-collection property. See the XooBars collection in the Demo where a XooBar item can include a collection of ZItems (these start in the demo as Automatic naming). If you change the editor to use NameProvider, the first call will go to the collection, the second to the class which provides the collection property - XooBar item.
Use it at Runtime, Anytime
If you need to also provide the user with the means to edit collection items at runtime, rather than a separate runtime Form, a separate utility class provides the means to show the designated collection editor at run time. This is the result of an Aha! Moment related to some StackOverflow answers from Mark Gravell who seems to be a wizard regarding all things Type-related.
The utility class uses Reflection
to ferret out the UITypeEditor
attribute for the property name you pass, then creates an instance of that editor, shows it, then uses Reflection
again to set the value. The result allows you to invoke your collection editors at runtime with one line of code:
Shared Sub ShowEditor(owner As IWin32Window, component As Object, propertyName As String)
RunTimeTypeEdit.ShowEditor(Me, NuControl1, "ZItemCollection")
Owner is the form that will parent or own the Dialog
Component is the instance type (class or control) containing the property you wish to edit
propertyName is the exact name of the property to edit. This must be a collection property which includes the Editor
attribute.
- The
UITypeEditor
specified for the property given will run. This is limited to be either the standard NET CollectionEditor or one which inherits from EnhancedCollectionEditor.
- This is runtime, so data entered will not be persisted or serialized, but any data currently in the collection will be in the runtime collection.
In the demo the button runs the custom collection editor for the XItems class. The editors associated with the sub collections on FooBarItem will also run.
The RunTimeUIEdTools class which also provides 2 Shared methods to parse the Type / Name of an collection class item.
Public Shared Function BaseNameFromType(ItemType As Type) As String
Public Shared Function BaseNameFromType(ItemType As Type) As String
In the demo, XooItems.GetNewName uses BaseNameFromTypeName to provide a unique name depending on the actual type just created (Ziggy1, Zoey1, Ziggy2, Zacky1, Zoey2...) rather than a generic ZItem9 name.
One Collection Editor to Rule Them All
With all the smarts and functionality built into the base class, there is not much left for the 'little' editor classes you write to do except create an instance of it and set the desired options. But this doesn't prevent your collection editors from doing more such as getting involved in the editing by handling PropertyValueChange events or attaching to other controls.
In many cases, you may find that you can define one editor to handle all the collections in a project. That is almost the case in the demo. There are 6 editors defined with 1 handling multiple ZItem collections, the FooBarCollectionEditor also handles both Foos and Bars. This could have been reduced further, but as a demo it is set up so that each sub-collection editor illustrate different features.
The Demo
The Demo doesn't do much except act as a platform for exposing various collections to the EnhancedCollectionEditor. In order to be realistic, it is made up of 3 projects:
UIDesigner - Contains the EnhancedCollectionEditor and other UIEditors and tools described in this article.
NuControl - This represents some control or component which houses the collection(s) for your project. The project is a Class Library as may likely be the case in a real project. This contains all the code related to collections, Attributes, TypeConverters and so forth.
UIDesigner Test - There is almost nothing here, just a means to host an instance of NuControl.
Since the key element is a UIEditor, you must compile it before it can be used. You are likely to find the code more illustrative than the Demo itself except maybe when comparing implementations.
The demo has a fair amount of implementation notes and tips in the code. Each set of classes resides in its own project source file with NuControl using Regions
liberally to organize the properties exposed.
XTDRItems
This is the most complex version:
- It uses a
Collection(Of ExtendedItems)
as the collection, XTDItemCollectionEditor is the editor.
- Each item inherits from an abstract base class.
- These all use the same one-param TypeConverter.
- The editor subscribes to the
PropertyValueChanged
event
- NuControl implements ISupportInitialize and invokes a related procedure on XTDItems class
- The collection editor includes code to demonstrate getting a reference to a control on the CollectionForm
- The ValueItem class also includes a sample EnumConverter.
- The FooBarItem type includes two sub-collections:
- A collection of Foos and Bars with the BarItem inheriting from FooItem using simple inheritance.
- In the sub collection of ZItems, ExcludedTypes is used to exclude the
Ziggy
type from this subcollection
ZItemCollection
This is a collection of the ZItems (Ziggy, Zoey, Zacky) using a Collection(Of T)
and the collection editor ZItemCollectionEditor. Each item uses its own TypeConverter.
ZCollectionBase
Another usage of ZItems, this time using CollectionBase
with ZItemCollectionEditor again as the editor.
ZObserveList
Yet another ZItems collection, but using ObservableList
(Of T)
in the collection class. This also reuses the ZItemCollectionEditor. Two different collection types, but the same editor.
XooBar
This one is also rather complex, intended primarily to demonstrate the NameService. Both naming conventions are implemented:
NameProvider
- XooBar items are named by NuControl since it exposes the XooBars collection property
- NuControl Implements INameProvider
- The GetNewName function on NuControl provides the new names for all Xoobar items (but could name others as well as shown in the code)
Automatic
- Each XooBar item can include a sub collection of ZItems.
- For these, as the property provider, XooBar Item can provide the name but does not.
- XooBar also Implements INameProvider
- The ZItems subcollection editor specifies NameServices.Automatic
- Change this to NameServices.NameProvider in the ZSubCollectionEditorINP editor class (in CollectionEds.vb)
- This will cause the GetName function on XooItem to be called (it is already there) and provide names for these ZItems in whatever format you specify.
As mentioned, you really only need to inherit a new editor class when you need to implement special behavior such as a NameService or to exclude a special type. That is, the same editor can be used to edit XooBar items as well as ZItems. All the actual Type handling nitty-gritty work is tucked away in the EnhancedCollectionEditor base class.
Compiled DLL
For those who do not want to track and include the source files over and over in various projects, the EnhancedCollectionEditor is supplied in DLL form. Compiler settings:
- Option Strict On
- Any CPU
- NET Framework 4
- Code Analysis checked
The DLL name is the same as the one from this other UIEditor. The source for both articles is included and compiled to a combined DLL. The Flag type EnumEditor is included as well.
Working with UITypeEditors
The odd thing about working with UITypeEditors is that the code you are working on is in design mode, but your UITypeEditor code is executing. As such, Visual Studio needs a compiled version to work with. So should you tinker with the code, be sure to recompile often - and always before testing a change - so that Visual Studio is working with the correct version. A great deal of time can be wasted tracking non existent bugs due to VS working with stale code.
Usually, the code compiled for AnyCPU seems towork fine for any project. There are times when a cache somewhere doesn't get cleared/updated though and rather than your editor/the base class editor executing, the default NET CollectionEditor runs. If Clean/Rebuild does not work, restart Visual Studio.
Appendix - Collection Editor Implementation Checklist
First and foremost, if you are having trouble getting VS to save (serialize) your collection items, Clean and Rebuild early and often. At times, with numerous fundamental changes (such as refactoring your TypeConverter and refining the constructor for your Items), Visual Studio can get confused and something just doesn't get reset/cleared out as advertised. In such cases. exit and restart VS.
Use this checklist of the key concepts from the article to help find what might be amisss with the Collection you are tring to implement. The list covers designer serialization and Type Converter aspects as well. If your custom Collection Editor will not start up, will it start with the default NET one? If not, something may be fundamentally wrong with something related to your classes.
Stupid stuff which Option Strict will catch is omitted.
Collection Class
- Is the collection variable instanced?
- Is the collection class using a typed collection such as Collection(Of T) or one which implements IList?
- Is the Collection Property decorated with the DesignerSerializationVisibility Attribute and set for Content?
- Is a correct and valid Collection Editor spcified using the Editor Attribute?
- Do you have ShouldSerializeXXX and ResetXXX implemented and properly named?
- If your Collection Class inherits CollectionBase, do you have a proper Item property:
Default Public Property Item(ndx As Integer) As <your_Type)
Collection Item Class
- Is the Items Class for the things which go into the collection marked as Serializable?
- Is each property to be saved decorated for DesignerSerializationVisibility?
- Regular properties should be Visible
- Properties handled through the constructor by the TypeConverter should be Hidden
- Does it have a simple constructor for the Collection Editor to use? (No params, but it may initialize properties to default values)
EnhancedCollection Editor
-
If you are unsure whether it is the default NET Editor rather than yours, use a custom title to be sure. (Console.Beep works well too - if the constructor fires (beeps) your Editor is running).
-
Be sure the name of the Editor Class to use is the one specified by the Editor Attribute on the Collection Property.
TypeConverter
- Does the Class have the TypeConverter Attribute? If specifying by name, is it the correct name?
- Does the TypeConverter convert the value parameter passed to it to the right Type (double check if you copied it from a similar class)?
- Does the TypeConverter return True for InstanceDescriptor?
- Does the Item class actually have a constructor matching what your call to GetConstructor asks for (does the Item Class really have a constructor matching the order and Type specified)? Check again.
- Does the Object array passed to create the InstanceDescriptor match the GetConstructor order and Type (and does such a constructor on the Item Class exist, obviously) ?
Clean and Rebuild after every change!
References and Resources
Developing .NET Custom Controls & Designers using C# By James Henry
Windows Forms Programming in C# By Chris Sells
How to Edit and Persist Collections with CollectionEditor By Daniel Zaharia
MSDN:
Microsoft .NET Source Reference:
For more on TypeConverters: Creating a custom TypeConverter... by Richard Moss
Assorted StackOverflow Answers from Mark Gravell (they aren't even my questions)
Designer Serialization Overview from MSDN (not as practical as it could be)
Having fun with custom collections by Sander Rossel is dense but demonstrates several very interesting techniques
History
2014.07 - Article and initial release v. 1.03