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

Editing Multiple Types of Objects with Collection Editor and Serializing Objects

Rate me:
Please Sign up or sign in to vote.
4.76/5 (14 votes)
17 Feb 2005CPOL8 min read 94.2K   968   54   16
Explains use of Custom Collection Editors to create objects for a component during design time.

Sample Image - Article.gif

Introduction

With the introduction of .NET and VB.NET, developing components and controls with VB is much more possible compared to the previous versions. The developer of a component must understand the objects, and how there are related to each other, and must provide a way to enable their creation during design time. The created objects during design time must be serialized. CollectionEditors and TypeConverters can be customized to achieve these tasks. Most of the examples are given in C# and VB programmers can get confused like I do, when trying to understand the sample code.

In this article I will try to explain how to use custom CollectionEditors and TypeConverters to handle multiple objects in a collection.

Background

In nearly every component I developed or used, there are at least one collection of objects. Some does not require design time capabilities so they are not a problem at all. When you need the collections to be editable during design time, you need to provide ways to edit and save them. Saving can only be achieved by serializing objects so that they generate the required code to be run during InitializeComponent for the consumer of the developed component. So use of Custom CollectionEditors allows the users of your component to create and maintain the required objects during design time, and TypeConverters handle the generation of code.

There is an excellent Article by Daniel Zaharia in CodeProject, "How to Edit and Persist Collections with CollectionEditor " which explains the usage of Custom Collection Editors and TypeConverters. Apart from converting the code from this article to VB, I will explain my solution to the following requirements.

  1. During design of a ToolBar Control to use in my projects, I had to support different types of ToolBar Buttons (PushButton, ToggleButton, GroupButon and others such as dropdown button), separators and PlaceHolders in a collection. PushButton type must have a click event, but the other two has only ValueChanged Event which can be handled. But what about a separator or placeholder. I personally hate seeing menu separators in Class DropDown when I use a MainMenu component on a Form.
  2. Each Type of button must display an image, and the best way is, to provide a visual way to display and select ImageItems from an ImageList. "Extended ImageIndexConverter and ImageIndexEditor. By Steve Yam" in Code Project explains how to do that by passing a reference of the control to every object in the collection. My solution utilizes similar TypeConverter and UIEditor but I managed to do that without Passing a reference.

So the Code is as follows.

Using the code

Starting with the ButtonBase Object

VB.NET
<DesignTimeVisible(False)> _
Public Class ButtonBase 
    Inherits System.ComponentModel.Component

We Inherit from Component,

DesignTimeVisible(False) 
attribute prevents your objects created by your component or Control to appear in the component tray of the designer

We need the following Enum to differentiate between different objects, and ButtonType of inherited objects will use this enumeration to assign their type in their constructors.

VB.NET
Public Enum ButtonTypes
    PushButton = 0
    GroupButton = 1
    ToggleButton = 2
    PlaceHolder = 3
    Seperator = 4
End Enum

Memory Variables are as follows

VB.NET
'Memory Variables For Properties
Private m_ButtonType As ButtonTypes
Private m_ImageIndex As Integer = -1
Private m_Value As Boolean
Private m_Collection As Buttons
Private m_Width As Integer

m_Collection variable is used to reference the object to the collection it belongs. By using this we can raise an event to notify the parent a property has changed.

VB.NET
Public WriteOnly Property Collection() As Buttons
    Set(ByVal Value As Buttons)
        m_Collection = Value
    End Set
End Property

Other properties are defined like this

VB.NET
'We dont want the ButtonType for the object to be changed by _ 
the Collection Editor so it has a Browsable(False) Atribute.
<Browsable(False)> Public Property ButtonType() As ButtonTypes
        Get
            Return m_ButtonType 
        End Get
        Set(ByVal Value As ButtonTypes)
            m_ButtonType = Value
        End Set
    End Property
    Public Property ImageIndex() As Integer
        Get
            Return m_ImageIndex
        End Get
        Set(ByVal Value As Integer)
            m_ImageIndex = Value
            PropertyChanged()
        End Set
    End Property
    Public Property Value() As Boolean
        Get
            Return m_Value
        End Get
        Set(ByVal Value As Boolean)
            m_Value = Value
            PropertyChanged()
        End Set
    End Property
    Public ReadOnly Property Width() As Integer
        Get
            Return m_Width
        End Get
    End Property

Width is a dummy property for Place Holder objects. And

PropertyChanged 
Routine is as follows

VB.NET
    Private Sub PropertyChanged()
        'Check if the collection is a valid object if not during design time 
        'you and up with a message 
        'Object is not set to an Instance' But Your Program Works
        'By the way I am looking forward for the IsNot operator in VS 2005, 
        'because every time I forget 
        'the Not Operator and need to navigate back
        If Not m_Collection Is Nothing Then
            m_Collection.RaisePropertyChangedEvent()
        End If
    End Sub
End Class

Now the first Button Class is driven from ButtonBase, we make PushButton serializable and assign a type converter to control the serialization of the object. For code clarity it is advisable to have the Converter as a nested class

VB.NET
<Serializable(), TypeConverter(GetType(PushButton.PushButtonConverter))> _
Public Class PushButton
    Inherits ButtonBase

    Event Click As EventHandler
    'Constructors
    Public Sub New()
        MyBase.New()
        Me.ButtonType = ButtonBase.ButtonTypes.PushButton
    End Sub
    'We Dont Want a Push Button to Expose a Value Property so we shadow 
    'it with browsable(False) atribute
    <Browsable(False)> _
    Shadows Property Value()
        Get
            'No Code is Required for Get and Set
        End Get
        Set(ByVal Value)

        End Set
    End Property
    <Browsable(False)> Shadows Property Width()
        Get

        End Get
        Set(ByVal Value)

        End Set
    End Property
    Friend Sub OnClick()
        RaiseEvent Click(Me, New EventArgs)
    End Sub

OnClick is used to raise Click Event and apart from that the code is simple. Now the exiting part, the Nested PushButtonConverter Class. We inherit from

TypeConverter 

VB.NET
Friend Class PushButtonConverter
        Inherits TypeConverter
        Public Overloads Overrides Function CanConvertTo _
            (ByVal context As System.ComponentModel.ITypeDescriptorContext, _
          ByVal destinationType As System.Type) As Boolean

            'What we are saying to the serializor, if the seriazor asks for 
            'an InstanceDescriptor, we can handle it
            If destinationType Is GetType(InstanceDescriptor) Then
                Return True
            End If
            Return MyBase.CanConvertTo(context, destinationType)
        End Function
        Public Overloads Overrides Function ConvertTo(ByVal context _
            As System.ComponentModel.ITypeDescriptorContext, _
            ByVal culture As System.Globalization.CultureInfo, _
            ByVal value As Object, ByVal destinationType As _
            System.Type) As Object
            
            If destinationType Is GetType(InstanceDescriptor) Then
                
'PushButton object does not have a constructor with parameters so we just 
'return the Sub New Constructor First paramater returns the Constructor, 
'Second must be Nothing because Constructor does not have any parameters, 
'and Third parameter basically tell the serializor definition is not 
'complete and properties will be defined afterwards. This is required 
'because we want to see the generated Code as follows
                '******************************************************
                ' Friend WithEvents PushButton1 as PushButton

                ' In InitializeComponent

                ' Me.PushButton1 = new PushButton

                ' ...   .AddRange(new Object(),{me.PushButton1, ....  
                'other Buttons .... })

                ' Me.PushBotton1.ImageIndex = 0
                ' Other Properties follows
           '************************************************************
                Return New 
         InstanceDescriptor(GetType(PushButton).GetConstructor(New Type() {}),
                           Nothing, False)
            End If
            Return MyBase.ConvertTo(context, culture, value, destinationType)
        End Function
    End Class
End Class

We must always return TypeConverter's base methods if we cannot handle the conversion.

ToggleButton Class is very similar to

PushButton 
class with different TypeConverter and has a Value changed event instead of click event. The code is the source file so you can always look there.

The other two objects ButtonSeperator and

PlaceHolder 
are not inherited from ButtonBase, and their code is as follows

VB.NET
<Serializable(), 
        TypeConverter(GetType(ButtonSeperator.ButtonSeperatorConverter))> _
    Public Class ButtonSeperator
    Private m_Text As String
    Public Sub New()
        m_Text = "Seperator"
    End Sub
    Public ReadOnly Property Text() As String
        Get
            Return m_Text
        End Get
    End Property
    Friend Class ButtonSeperatorConverter
        Inherits TypeConverter
        Public Overloads Overrides Function CanConvertTo(ByVal context As _
 System.ComponentModel.ITypeDescriptorContext, ByVal destinationType As _
 System.Type) As Boolean
            If destinationType Is GetType(InstanceDescriptor) Then
                Return True
            End If
            Return MyBase.CanConvertTo(context, destinationType)
        End Function
        Public Overloads Overrides Function ConvertTo(ByVal context As _
System.ComponentModel.ITypeDescriptorContext, ByVal culture As _
System.Globalization.CultureInfo, ByVal value As Object, ByVal _
destinationType As System.Type) As Object
            If destinationType Is GetType(InstanceDescriptor) Then
                Return New 
        InstanceDescriptor(GetType(ButtonSeperator).GetConstructor(New Type()
                          {}), Nothing, True)
            End If
            Return MyBase.ConvertTo(context, culture, value, destinationType)
        End Function
    End Class
End Class
VB.NET
<Serializable(), TypeConverter(GetType(PlaceHolder.PlaceHolderConverter))> _
Public Class PlaceHolder
    Private m_Width As Integer
    Public Sub New()
    End Sub
    Public Sub New(ByVal Width As Integer)
        m_Width = Width
    End Sub
    Public Property Width() As Integer
        Get
            Return m_Width
        End Get
        Set(ByVal Value As Integer)
            m_Width = Value
        End Set
    End Property
    Friend Class PlaceHolderConverter
        Inherits TypeConverter
        Public Overloads Overrides Function CanConvertTo(ByVal context _
                              As ITypeDescriptorContext, _
        ByVal destType As Type) As Boolean
            If destType Is GetType(InstanceDescriptor) Then
                Return True
            End If
            Return MyBase.CanConvertTo(context, destType)
        End Function
        Public Overloads Overrides Function ConvertTo(ByVal context _
                As ITypeDescriptorContext, _
        ByVal culture As CultureInfo, ByVal value As Object, ByVal destType _
                As Type)
            If destType Is GetType(InstanceDescriptor) Then
                Dim MyObject As PlaceHolder = CType(value, PlaceHolder)

'The PlaceHolder Object has an overloaded constructor in which Width is set. 
'So we tell the serializer to use this constructor when creating the 
'instance of the object. This is achieved by passing the types of variables 
'in the argument list, in this case Integer. We dont want to see a place 
'holder in class dropdown so we return true as third parameter to tell the 
'serializer to define the object during AddRange method

                Return New
          InstanceDescriptor(GetType(PlaceHolder).GetConstructor(New Type() 
                       {GetType(Integer)}), _
                        New Object() {MyObject.Width}, True)
            End If
            Return MyBase.ConvertTo(context, culture, value, destType)
        End Function
    End Class
End Class
#End Region

The buttons collection must be inherited from CollectionBase for the CollectionEditor to handle object creation during design time. And also inheriting from collection base makes the collection Strong Typed.

VB.NET
<Serializable()> _
Public Class Buttons
    Inherits CollectionBase
    'Event To Notify Parent when a property is changed during Design or 
    'Run Time so the control can Paint itself
    Event PropertyChaged()

For the CollectionEditor and Serializer to do their jobs properly, the class must provide Add Method,

AddRange 
Method and Item Default Readonly Property. Notice that they set the collection property for the objects inherited from the button base.

VB.NET
'Provide Add and AddRange Methods and Item(Indexer)
Public Function Add(ByVal Item As Object) As Object
    If Not TypeOf Item Is ButtonSeperator And _
       Not TypeOf Item Is PlaceHolder Then
        CType(Item, ButtonBase).Collection = Me
    End If
    list.Add(Item)
    Return Item
End Function
Public Sub AddRange(ByVal Items() As Object)
    Dim Item As Object
    For Each Item In Items
    If Not TypeOf Item Is ButtonSeperator And _
       Not TypeOf Item Is PlaceHolder Then
        CType(Item, ButtonBase).Collection = Me
    End If
        list.Add(Item)
    Next
End Sub

The tricky part is the Item method. Since there are objects not inherited from ButtonBase in the collection I return a newly created object for those as follows. You may think why property does not return object instead of ButtonBase. If the return type is not a defined class, then the CollectionEditor does not display the properties for different type of buttons and you get an readonly object in the property grid which does not help at all.

VB.NET
Default Public ReadOnly Property Item(ByVal Index As Integer) As ButtonBase
        Get
            If TypeOf list(Index) Is PushButton Then
                Return CType(list(Index), ButtonBase)
            End If
            If TypeOf list(Index) Is ToggleButton Then
                Return CType(list(Index), ButtonBase)
            End If
            If TypeOf list(Index) Is PlaceHolder Then
                Return New ButtonBase(ButtonBase.ButtonTypes.PlaceHolder, _
                         CType(List(Index), PlaceHolder).Width)
            End If
            If TypeOf List(Index) Is ButtonSeperator Then
                Return New ButtonBase(ButtonBase.ButtonTypes.Seperator, 0)
            End If
        End Get
    End Property

Keep in mind that an object can always be converted to its base class so the first two types are handled this way. For ButtonSeperator and PlaceHolder I cheat by returning a dummy object created by ButtonBase Classes overloaded constructor. And RaisePropertyChangedEvent method

VB.NET
Friend Sub RaisePropertyChangedEvent()
     RaiseEvent PropertyChaged()
 End Sub

If required another readonly property can be defined to get the real object.

How do we tell the collection editor to display a dropdown image near the add button allowing different types of objects to be created?

The answer to this question is we need a custom

CollectionEditor 
Inheriting from CollectionEditor and the code is very easy.

VB.NET
Friend Class ButtonCollectionEditor
    Inherits System.ComponentModel.Design.CollectionEditor
    Private Types() As System.Type
    Sub New(ByVal type As System.Type)
        MyBase.New(type)
        Types = New System.Type() {GetType(PushButton), _
                GetType(ButtonSeperator), GetType(PlaceHolder) _
                , GetType(ToggleButton)}
    End Sub
    Protected Overrides Function CreateNewItemTypes() As System.Type()
        Return Types
    End Function
End Class

All needed is to define an Array of our object types and and return it in the overridden function CreateNewItemTypes when the base class needs that information.

And How we manage our component to use all this definitions is as follows:

  1. Declare a Private Variable with withevents keyword and for Buttons collection with new keyword
  2. Write as readonly Property for your Collection with DesignerSerializationVisibility(DesignerSerializationVisibility.Content) and Editor attribute referencing your custom Collection editor as given Below.
VB.NET
Private WithEvents m_Buttons As New Buttons

<DesignerSerializationVisibility(DesignerSerializationVisibility.Content),_
      Editor(GetType(ButtonCollectionEditor), GetType(UITypeEditor))> _
      Public ReadOnly Property Buttons() As Buttons
      Get
          Return m_Buttons
      End Get
End Property

Now the answer to the second Requirement.

To be able to select an image from an ImageList, Custom ImageIndexConverter and an UITypeEditor is Required. Fist one converts the Value of the ImageIndex property To Integer, or From Integer to string, and second one Paints the Image on property grid and the dropdown list. To Find out the Images in an ImageList we must provide a to pass the ImageList to Converter and Editor. Steve Yam passes a reference of Parent to every Item in the collection. My Solution is to define public variable in a module which holds the ImageList and public variables in the modules are shared in all classes in the project as follows.

VB.NET
Module Module1
    Public mm_ImageList As ImageList
End Module

The ImageList Property for the control will asign a the reference of control's ImageList during Property Set Procedure.

VB.NET
Private m_ImageList As ImageList
Public Property ImageList() As ImageList
    Get
        Return m_ImageList
    End Get
    Set(ByVal Value As ImageList)
        m_ImageList = Value
        mm_ImageList = Value
    End Set
End Property

And the TypeConverter

VB.NET
Friend Class EImageIndexConverter
    Inherits ImageIndexConverter

First thing we tell the designer is we are supporting standard values for the property and to display a combo dropdown on the property page.

VB.NET
Public Overloads Overrides Function GetStandardValuesSupported _
     (ByVal context As System.ComponentModel.ITypeDescriptorContext) _
     As Boolean
    If context.Instance Is Nothing Then
        Return False
    Else
        Return True
    End If
End Function

Second we need to override ConvertFrom and

ConvertTo 
Methods. ConvertFrom Converts the value from
String 
to Integer.

VB.NET
Public Overloads Overrides Function ConvertFrom _
     (ByVal context As System.ComponentModel.ITypeDescriptorContext, _
     ByVal culture As System.Globalization.CultureInfo,_
     ByVal value As Object) As Object

    If TypeOf value Is String Then
        If value <> "(none)" And value <> vbNullString Then
            Try
                Return CInt(value)
            Catch ex As Exception
                Return -1
            End Try
        Else
            Return -1
        End If
    Else
        Return Nothing
    End If
End Function

ConvertTo convert integer value of the property to

string 
to display in property grid.

VB.NET
Public Overloads Overrides Function ConvertTo _
     (ByVal context As System.ComponentModel.ITypeDescriptorContext, _
     ByVal culture As System.Globalization.CultureInfo,
     ByVal value As Object, _
     ByVal destinationType As System.Type) As Object

    If TypeOf value Is Integer Then
        If value <> -1 Then
            Return CStr(value)
        Else
            Return "(none)"
        End If
    Else
        Return "(none)"
    End If
End Function

And we must return an ArrayList containing a -1 for a not selected image index and range of values from 0 to imagelists image count -1 in the overridden GetStandardValues Function. Notice that we are using public Variable defined in the Module for the Imagelist.

VB.NET
   Public Overloads Overrides Function GetStandardValues (ByVal context As _
        System.ComponentModel.ITypeDescriptorContext) _
        As System.ComponentModel.TypeConverter.StandardValuesCollection
        Dim Ar As New ArrayList
        Ar.Add(-1)
        Dim m_imagel As ImageList
        m_imagel = mm_ImageList
        If mm_ImageList Is Nothing Then
            m_imagel = Nothing
        Else
            m_imagel = mm_ImageList
        End If
        If Not m_imagel Is Nothing Then
            For i As Integer = 0 To m_imagel.Images.Count - 1
                Ar.Add(i)
            Next
        End If
        Return New StandardValuesCollection(Ar)
    End Function
End Class

The Editor is inherited from UITypeEditor and has an overridden Function GetPointValueSupported, which tells the editor we are going to support a visual representation for the value of the property and it provides a small rectangle on the left of the property grid for the edited item. An overridden Method PaintValue actually does the painting on that graphics surface again using the public variable of ImageList.

VB.NET
Friend Class EImageIndexEditor
    Inherits UITypeEditor

    Public Overloads Overrides Function GetPaintValueSupported _
(ByVal context As System.ComponentModel.ITypeDescriptorContext) As Boolean
        Return True
    End Function

    Public Overloads Overrides Sub PaintValue(ByVal e _
                                As System.Drawing.Design.PaintValueEventArgs)
        Dim m_imageIdx As Integer
        m_imageIdx = CInt(e.Value)
        Dim m_imagel As ImageList
        If mm_ImageList Is Nothing Then
            m_imagel = Nothing
        Else
            m_imagel = mm_ImageList
        End If
        If Not m_imagel Is Nothing Then
            If m_imageIdx >= 0 And m_imageIdx < m_imagel.Images.Count Then
               e.Graphics.DrawImage(m_imagel.Images(CInt(e.Value)), e.Bounds)
            End If
        End If
    End Sub
End Class

We need to change ImageIndex property for the

ButtonBase 
object to tell the designer to use the new Converter and Editor as follows,

VB.NET
<DefaultValue(-1), TypeConverter(GetType(EImageIndexConverter)), _
    Editor(GetType(EImageIndexEditor), GetType(UITypeEditor))> _
    Public Property ImageIndex() As Integer

It works, But what happens if you have more done one instance of your control on a design surface with different ImageLists. The public variable for the ImageList will hold the reference for only one of the Controls so design time and run time images will be different. The Solution to this problem is to define a custom Designer and assign a event handler when will be raised when the Control is selected and we can then assign the correct image list to public Variable.

VB.NET
Public Class UserControl1Designer
    Inherits System.Windows.Forms.Design.ControlDesigner
    Public Overrides Sub Initialize(ByVal component _
                                         As System.ComponentModel.IComponent)
        MyBase.Initialize(component)
        Dim ss As ISelectionService
           = CType(GetService(GetType(ISelectionService)), ISelectionService)
        If Not (ss Is Nothing) Then
            AddHandler ss.SelectionChanged, AddressOf OnSelectionChanged
        End If

    End Sub
    Private Sub OnSelectionChanged(ByVal sender As Object,
                                   ByVal e As EventArgs)
        Dim ss As ISelectionService = CType(sender, ISelectionService)
        If Not ss Is Nothing Then
            If TypeOf ss.PrimarySelection Is UserControl1 Then
                mm_ImageList = CType(ss.PrimarySelection, 
                                      UserControl1).ImageList
            End If
        End If
    End Sub
End Class

As you can follow we override the Initialize Method to assign the AddressOf OnSelectionChange method, to the event handler of the selection service.

Last Thing to do is to tell our control to use the CustomDesigner,

VB.NET
<DesignerAttribute(GetType(UserControl1Designer))> Public Class UserControl1

Points of Interest

I think we always try about thousand new ways to solve a problem which are not a solution to the problem at all. But that's how we gain experience in our job and life.

Unfortunately examples in MSDN for Custom designers are not adequate and I hope my solution will help you in your work.

History

  • Posted Feb 17, 2005

License

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


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

Comments and Discussions

 
QuestionWhat I'm missing? Pin
carloqueirolo21-Jun-05 23:35
carloqueirolo21-Jun-05 23:35 
AnswerRe: What I'm missing? Pin
Oktay126-Jun-05 7:46
Oktay126-Jun-05 7:46 
GeneralRe: What I'm missing? Pin
carloqueirolo26-Jun-05 8:02
carloqueirolo26-Jun-05 8:02 
GeneralRe: What I'm missing? Pin
aaroncampf13-Jan-11 21:20
aaroncampf13-Jan-11 21:20 

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.