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.
- 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.
- 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
<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.
Public Enum ButtonTypes
PushButton = 0
GroupButton = 1
ToggleButton = 2
PlaceHolder = 3
Seperator = 4
End Enum
Memory Variables are as follows
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.
Public WriteOnly Property Collection() As Buttons
Set(ByVal Value As Buttons)
m_Collection = Value
End Set
End Property
Other properties are defined like this
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
Private Sub PropertyChanged()
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
<Serializable(), TypeConverter(GetType(PushButton.PushButtonConverter))> _
Public Class PushButton
Inherits ButtonBase
Event Click As EventHandler
Public Sub New()
MyBase.New()
Me.ButtonType = ButtonBase.ButtonTypes.PushButton
End Sub
<Browsable(False)> _
Shadows Property Value()
Get
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
Friend Class PushButtonConverter
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(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
<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
<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)
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.
<Serializable()> _
Public Class Buttons
Inherits CollectionBase
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.
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.
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
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.
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:
- Declare a Private Variable with withevents keyword and for Buttons
collection with new keyword
- Write as readonly Property for your Collection with
DesignerSerializationVisibility
(DesignerSerializationVisibility.Content
)
and Editor attribute referencing your custom Collection editor as given Below.
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.
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.
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
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.
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
.
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.
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
.
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
.
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,
<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.
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,
<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
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.