Article Overview
This article demonstrates how to use a designer to pre-populate custom Windows controls with child controls having unique names.
Background
I am on a quest to create The Perfect Tab Control. To do that, I need to understand how they work.
CodeProject has a lot of fine custom tab controls, but all of the ones I've found are either based on Microsoft's TabControl
and TabPage
, or else they initialize empty when dropped on a form. I wanted to learn how the standard TabControl
initializes with two pages, and how those pages start out with unique names (drop one control on a form and you get TabPage1
and TabPage2
; drop a second and you get TabPage3
and TabPage4
.)
This is a simple procedure, it turns out, but it took a long time before I could find out how this was done. It was CodeProject member liron.levi[^] who pointed me in the right direction with his article, A MultiPanel Control in C#[^].
About the Code
This article is NOT about The Perfect Tab Control; sorry, you will have to wait for that one. The code in the article is VB but the example projects linked above come in C# as well as VB.
If you are going to cut and paste from the article, please note that your project will need a reference to System.Design
.
The Example
Let's start by looking at two controls, MyButton
and MySecondButton
.
<System.ComponentModel.DesignerCategory("code")> _
Public Class MyButton
Inherits Button
End Class
Public Class MySecondButton
Inherits MyButton
End Class
As you can see, MyButton
simply inherits from the standard Button
control, and MySecondButton
inherits from MyButton
. Note the use of the DesignerCategory
attribute. Normally, a class that inherits from a control will be treated by Visual Studio as a custom control, with a design interface. DesignerCategory("code")
directs the IDE to treat the class as ordinary code, with no interface. I did not need to use this attribute on MySecondButton
, because a control inherits the designer category of its base.
The code for MyButtonPanel
is almost as simple.
<System.ComponentModel.DesignerCategory("code")> _
<System.ComponentModel.Designer(GetType(MyButtonPanelDesigner))> _
Public Class MyButtonPanel
Inherits Panel
Protected Overrides Sub OnControlAdded(ByVal e _
As System.Windows.Forms.ControlEventArgs)
Dim TP As MyButton = TryCast(e.Control, MyButton)
If TP Is Nothing Then
Throw New ArgumentException_
("Attempted to add a control to MyButtonPanel that was not a MyButton.")
End If
TP.Dock = DockStyle.Top
MyBase.OnControlAdded(e)
End Sub
End Class
This control inherits from Panel
and is also marked with DesignerCategory
. The overridden method OnControlAdded
guarantees that only MyButton
and derivative controls (like MySecondButton
) can be added. This allows you to limit what controls go into your custom container; for example, I would want only tab pages to go into a custom tab control. After this check, the method docks the added control to the top of the container and calls the base OnControlAdded
method, which fires the ControlAdded
event. The class is also flagged with the Designer
attribute, which tells Visual Studio to use the custom designer.
At last, we come to MyButtonPanelDesigner
, which inherits from ParentControlDesigner
.
<PermissionSet(SecurityAction.Demand, name:="FullTrust")> _
Public Class MyButtonPanelDesigner
Inherits ParentControlDesigner
#Region " Storage "
Private _MBP As MyButtonPanel
Private _Verbs As DesignerVerbCollection
#End Region
#Region " Overrides "
Public Overrides Sub Initialize(ByVal component As IComponent)
_MBP = TryCast(component, MyButtonPanel)
If _MBP Is Nothing Then
DisplayError(New ArgumentException("Tried to use MyButtonPanelDesigner " + _
"with a class that does not inherit from MyButtonPanel.", _
"component"))
Exit Sub
End If
MyBase.Initialize(component)
Dim DH As IDesignerHost = TryCast(GetService_
(GetType(IDesignerHost)), IDesignerHost)
If DH IsNot Nothing Then
Dim MB As MyButton = TryCast(DH.CreateComponent(GetType(MyButton)), MyButton)
If MB Is Nothing Then
DisplayError(New Exception("Error creating new button."))
Exit Sub
End If
MB.Text = MB.Name
_MBP.Controls.Add(MB)
MB = TryCast(DH.CreateComponent(GetType(MyButton)), MyButton)
If MB Is Nothing Then
DisplayError(New Exception("Error creating new button."))
Exit Sub
End If
MB.Text = MB.Name
_MBP.Controls.Add(MB)
_MBP.Controls.SetChildIndex(MB, 0)
End If
End Sub
Public Overrides ReadOnly Property Verbs() _
As System.ComponentModel.Design.DesignerVerbCollection
Get
If _Verbs Is Nothing Then
_Verbs = New DesignerVerbCollection
_Verbs.Add(New DesignerVerb("Add Button", AddressOf AddButton))
End If
Return _Verbs
End Get
End Property
#End Region
#Region " Private methods "
Private Sub AddButton(ByVal sender As Object, ByVal e As EventArgs)
Dim DH As IDesignerHost = DirectCast_
(GetService(GetType(IDesignerHost)), IDesignerHost)
If DH IsNot Nothing Then
Dim DT As DesignerTransaction = Nothing
Try
DT = DH.CreateTransaction("Added new Button")
Dim OldControls As Control.ControlCollection = _MBP.Controls
RaiseComponentChanging(TypeDescriptor.GetProperties(Control)("Controls"))
Dim NewButton As MyButton = TryCast_
(DH.CreateComponent(GetType(MyButton)), MyButton)
NewButton.Text = NewButton.Name
_MBP.Controls.Add(NewButton)
_MBP.Controls.SetChildIndex(NewButton, 0)
RaiseComponentChanged(TypeDescriptor.GetProperties_
(Control)("Controls"), OldControls, _MBP.Controls)
Catch ex As Exception
DisplayError(ex)
DT.Cancel()
Finally
DT.Commit()
End Try
End If
End Sub
#End Region
End Class
The meat of the example is the Initialize
method. After getting a reference to IDesignerHost
, the method calls CreateComponent
to create a new instance of MyButton
. There are two overloads to this method, one which takes a name and assigns it to the new control, and one that does not. By choosing the one without a name, the designer will find the next available unique name and use that. The code then places the name of the new button into its Text
property and adds it to the parent's Controls
collection. The code sets the index of the second child control to 0: by setting the newer control at the start of the collection, we are actually ordering the child controls with the oldest at the top. (Comment this line out and see what I mean.)
The AddButton
method uses a similar technique when the designer is used to add a new button.
The Results
Now we have a very basic custom container control with some custom child controls. When you drop it on a form, this is what you get:
Very nice. Now, let's look at the form's InitializeComponent
method.
Private Sub InitializeComponent()
Me.MyButtonPanel1 = New InitializeCustomPanel_VB.MyButtonPanel
Me.MyButton1 = New InitializeCustomPanel_VB.MyButton
Me.MyButton2 = New InitializeCustomPanel_VB.MyButton
Me.MyButtonPanel1.SuspendLayout()
Me.SuspendLayout()
Me.MyButtonPanel1.Controls.Add(Me.MyButton2)
Me.MyButtonPanel1.Controls.Add(Me.MyButton1)
Me.MyButtonPanel1.Location = New System.Drawing.Point(28, 24)
Me.MyButtonPanel1.Name = "MyButtonPanel1"
Me.MyButtonPanel1.Size = New System.Drawing.Size(200, 100)
Me.MyButtonPanel1.TabIndex = 0
Me.MyButton1.Dock = System.Windows.Forms.DockStyle.Top
Me.MyButton1.Location = New System.Drawing.Point(0, 0)
Me.MyButton1.Name = "MyButton1"
Me.MyButton1.Size = New System.Drawing.Size(200, 23)
Me.MyButton1.TabIndex = 0
Me.MyButton1.Text = "MyButton1"
Me.MyButton2.Dock = System.Windows.Forms.DockStyle.Top
Me.MyButton2.Location = New System.Drawing.Point(0, 23)
Me.MyButton2.Name = "MyButton2"
Me.MyButton2.Size = New System.Drawing.Size(200, 23)
Me.MyButton2.TabIndex = 1
Me.MyButton2.Text = "MyButton2"
Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.ClientSize = New System.Drawing.Size(324, 152)
Me.Controls.Add(Me.MyButtonPanel1)
Me.Name = "Form1"
Me.Text = "Form1"
Me.MyButtonPanel1.ResumeLayout(False)
Me.ResumeLayout(False)
End Sub
Friend WithEvents MyButtonPanel1 As InitializeCustomPanel_VB.MyButtonPanel
Friend WithEvents MyButton2 As InitializeCustomPanel_VB.MyButton
Friend WithEvents MyButton1 As InitializeCustomPanel_VB.MyButton
Even nicer. The designer serialized the MyButtonPanel
control and its two component MyButton
controls exactly as I wanted. Even the call to SetChildIndex
was serialized properly; we see this because MyButton2
was added first.
Further Testing
Manually add a MyButton
control, and it will get docked properly (thanks to the override of OnControlAdded
) and have the name MyButton3
(thanks, I believe, to the default Form
designer.) Rename MyButton1
to something else and use the task panel to add another MyButton
; the new button will have the name MyButton1
(thanks to the AddButton
method in MyButtonPanelDesigner
.)
Drop a second MyButtonPanel
on the form, and you will see that the two buttons are named MyButton4
and MyButton5
. Go back to the first panel and add another MyButton
; it will have the name MyButton6
.
Conclusion
Like I said, the technique is very simple and straightforward, but it was difficult to track down. I hope this illustration will be of use, and if you find any bugs or have comments, please post them below.
History
- Revision 6 - March 29, 2010: Initial publication
- Revisions 7 & 8 - March 29, 2010: Why is it that, no matter how many times you proof an article, spelling and grammar errors will not manifest until after it has been posted?