Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Generate InitializeComponent Code for Custom Containers

4.83/5 (4 votes)
29 Mar 2010CPOL4 min read 3   572  
Demonstrate how to use a designer to pre-populate custom Windows controls with child controls having unique names.

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.

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

Explorer.jpg

The code for MyButtonPanel is almost as simple.

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

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

        'Attempt to add two buttons
        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:

Form1.jpg

Very nice. Now, let's look at the form's InitializeComponent method.

VB.NET
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()
        '
        'MyButtonPanel1
        '
        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
        '
        'MyButton1
        '
        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"
        '
        'MyButton2
        '
        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"
        '
        'Form1
        '
        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.

Form2.jpg

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?

License

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