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?