Click here to Skip to main content
15,867,141 members
Articles / Programming Languages / Visual Basic

Enhanced CollectionEditor Framework

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
21 Jul 2014CPOL46 min read 29.3K   664   18   5
An easy to use, customizable Collection Editor; with inheritance support

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:

Image 1

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:

Image 2

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.

VB.NET
 ' the basic data type
 Public Enum ItemTypes
     TextType
     ValueType
     DateType
     FooleanType
 End Enum

Public MustInherit ExtendedItem
     Public Property ItemType as ItemTypes

     Public Property Name As String

     Public Property Index As Integer

     ' all inherited classes must have a type:
     Public Sub New(st as ItemTypes)
          Itemtype = st
     End Class

End Class

Public Class Textitem
     Inherits ExtendedItem

     Private myType As ItemTypes = ItemTypes.TextType

     Public Property Text as String

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

End Class

Public Class Valueitem: Inherits ExtendedItem

Public Class FooBarItem: Inherits ExtendedItem

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):

VB.NET
Public Class NuControl
     Inherits Panel
     Implements ISupportInitialize

     ' this is the collection class of Extended items...
     ' it **must** be instanced for the Collection Editor
     Friend XTDItems As New ExtendedItems

     <DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
     <Editor(GetType(CollectionEditor),
             GetType(System.Drawing.Design.UITypeEditor))>
     Public ReadOnly Property ItemExtenders As ExtendedItems
         Get
             ' MS pretty regularly does this in their code
             ' see ListView Groups collection:
             If XTDItems Is Nothing Then
                 XTDItems = New ExtendedItems
             End If

             Return XTDItems
         End Get
     End Property

     Private Sub ResetItemExtenders()
         XTDItems.Clear()
     End Sub

     Private Function ShouldSerializeItemExtenders() As Boolean
         Return (XTDItems.Count > 0)
     End Function

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 (<span style="font-size: 14px;">formname).designer.vb</span> 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:

VB.NET
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:

  1. 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.  
  2. The collection property must also include ShouldSerializeXXX and ResetXXX as described.
  3. 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.
  4. Since the collection itself cannot be serialized, this is handled through the Item Class. This will almost certainly require a TypeConverter (covered shortly).
  5. 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:
VB.NET
' alternatively, mark the ExtendedItem base class (and so, all inherited types)
<Serializable>
Public Class TextItem
    Inherits ExtendedItem

    Private myType As ItemTypes = ItemTypes.TextType

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

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

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

End Class

The <a href="http://msdn.microsoft.com/en-us/library/system.componentmodel.defaultvalueattribute(v=vs.110).aspx" target="_blank">DefaultValue</a> 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):

C#
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:

VB.NET
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:

VB.NET
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:

VB.NET
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):

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

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:

VB.NET
<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:

VB.NET
Friend Class ZoeConverter
   Inherits TypeConverter

   ' CanConvertTo omitted for brevity

   Public Overrides Function ConvertTo(context As ITypeDescriptorContext,
                   info As CultureInfo, Value As Object,
                   destType As Type) As Object

           If destType = GetType(InstanceDescriptor) Then
               ' convert value (Object) to correct type
               Dim z As Zoey = CType(value, Zoey)

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

               ' get the ctor info matching the sig desired
               ctor = GetType(Zoey).GetConstructor(New Type()
                                   {GetType(String), GetType(Integer)})

               ' create inst descriptor using the ctorInfo and instance values
               Return New InstanceDescriptor(ctor,
                           New Object() {z.Name, z.ZCount}, False))
          End If
          Return MyBase.ConvertTo(context, info, value, destType)
    End Function
End Class

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 <span style="color: rgb(17, 17, 17); font-family: 'Segoe UI', Arial, sans-serif; font-size: 14px;">and</span><span style="color: rgb(17, 17, 17); font-family: 'Segoe UI', Arial, sans-serif; font-size: 13.63636302947998px;"> </span>Integer types so in VB lingo the code is requesting this constructor:
VB.NET
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):

VB.NET
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):

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

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

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

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

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:

VB.NET
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:

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

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:

VB.NET
<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:

 

Image 3

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:  

VB.NET
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. 

 

 

Part II: The Collection Editor

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:

  1. Determining the collection type
  2. Determining the type(s) it can contain
  3. Filter out any abstract base class
  4. Override a few standard functions
  5. 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:

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

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

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

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

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

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

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

    End Sub 
  • The Type T passed is whatever your property getter returns, the ExtendedItems class in this case. Note that this is the collection classnot 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:

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

    If mycolBaseType.IsAbstract Then

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

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

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

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

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

End Function

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:

 

Image 4

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:

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

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

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

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

     ' set the form title
     EditorForm.Text = FormCaption

     ' return form ref to work with
     Return EditorForm

 End Function 

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 <span style="font-size: 14px;">propertyBrowser.</span>PropertyValueChanged<span style="font-size: 14px;"> </span>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:
VB.NET
 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:

VB.NET
<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:

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

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

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

End Sub

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

End Sub

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:

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

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

Image 5

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

VB.NET
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:

VB.NET
Shared Sub ShowEditor(owner As IWin32Window, component As Object, propertyName As String)

' runtime usage:
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.

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


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

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 Observable<code>List(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.

 

Image 6

 

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

 

 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAdjusting the "listbox" and "propertyBrowser" size Pin
Tim8w19-Oct-20 4:55
Tim8w19-Oct-20 4:55 
QuestionGood one Pin
Midi_Mick14-Feb-17 21:52
professionalMidi_Mick14-Feb-17 21:52 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA13-Aug-14 3:23
professionalȘtefan-Mihai MOGA13-Aug-14 3:23 
QuestionCollectionEditors & Interface....small contribution Pin
a_pess21-Jul-14 10:18
a_pess21-Jul-14 10:18 
QuestionVery nice Pin
a_pess17-Jul-14 14:32
a_pess17-Jul-14 14:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.