Creating Your Own Controls, Part II: Composite Controls...

Composite controls are great for grouping "child" controls together into a singular "parent" control. Any attributes specified at the "parent" level can then be replicated at the "child" level.


By: Brian Mains Date: April 20, 2004 Download the code.

Composite controls are great for grouping "child" controls together into a singular "parent" control. Any attributes specified at the "parent" level can then be replicated at the "child" level. Please read the Creating your own controls, Part I document to understand more about composite controls in detail.

Let's go through an example composite control, which will be a user name/password control. The user can enter their user name and password and submit it to the server for processing. A Login event is raised, which uses a custom event argument named LoginInfoEventArgs, which contains the login credentials. A message will appear stating that the login information is or isn't valid. This control will inherit from the WebControl class, and implement the INamingContainer interface. This interface ensures that any duplicate ID issues in the web form will be resolved.

Imports System.ComponentModel
Imports System.Web.UI
Imports System.Web.UI.WebControls

Public Class Login
  Inherits WebControl
  Implements INamingContainer

The control renders all of the content of the control within a table structure. To create a table element and add it to the controls collection, the LiteralControl is used. This control receives an HTML string in its constructor, which allows it to be created on the fly. This control renders the HTML string directly to the browser without performing any processing, when used in this manner. The purpose of the table is to format the controls neatly in the browser. The final layout looks like this:

<table>
  <tr>
    <td colspan='2'><Message Label Control></td>
  </tr>
  <tr>
    <td>User Name:</td>
    <td><User Name Control></td>
  </tr>
  <tr>
    <td>Password:</td>
    <td><Password Control></td>
  </tr>
  <tr>
    <td colspan='2'><Submit Control></td>
  </tr>
</table>

The LiteralControl is instantiated in the CreateChildControls method, along with all of the controls. To render the entire control to the browser, each control is added to the composite control's Controls collection. The execution of this method cannot be determined, so the EnsureChildControls method and ChildControlsCreated property are used to verify that the CreateChildControls method was called.

The first control to be added to the composite (other than the LiteralControls) is a Label control. This control will either hold an error or success message when logging in through the control. The LoginFailure() and LoginSuccess() methods will alter the properties of this control (later below). The Add method is used to add the control to the collection.

'Create a label that will display error information
Dim objMessage As New Label
With objMessage
  .ID = "lblMessage"
  .ForeColor = System.Drawing.Color.Red
  .Font.Bold = True
  .Text = ""
End With
Me.Controls.Add(objMessage)

Next, the labels for the textboxes (the "User Name:" and "Password:" text) could be rendered in the HTML. However, the use of a Label control allows the alteration of the font, size, border, etc. properties for the label. The CopyBaseAttributes method makes this happen; it receives a parameter of the control to inherit properties from, and assigns the appropriate property values accordingly.

Dim objUIDLabel As New Label
With objUIDLabel
  .ID = "lblUserName"
  .Text = "User ID:"
  .CopyBaseAttributes(Me)
End With
Me.Controls.Add(objUIDLabel)

The Textbox control that will receive the input is created below. Depending on requirements, the MaxLength property controls the maximum number of characters that can be typed into the Textbox. The Width property controls how wide the control is, which uses the Unit class to render this correctly (using the pixel measurement). Please note that if a width isn't specified, the Textbox control inherits the width from the MaxLength property.

Dim objUID As New TextBox
With objUID
  .ID = "txtUserName"
  .MaxLength = 20
  .Width = New Unit(200)
  'Add any additional parameters here
End With
Me.Controls.Add(objUID)

To access the Textboxes for the user name and password, two properties parse the controls collection using the FindControl method, which searches the collection via the ID name. Then the reference is returned and the Text property is retrieved. Before all of this can happen, the CreateChildControls method must be called, which is why the EnsureChildControls method is first in the Get and Set statements.

In addition, attributes are declared for these properties that affect them in design time. The first property, named Category, defines which category that the property will appear under in the Visual Studio .NET property window (when not in A-Z mode). The DesignOnly attribute for the Password property determines the appearance of the property in the properties window. If set to true, the property will not appear in that window. Additional attributes can be defined, such as whether the property is databindable or browsable, what the description of the control is, or what type converter to use when needed in the property window.

<Category("Data"), DesignOnly(True)> _
Private Property Password() As String
  Get
    EnsureChildControls()
    Return CType(Me.FindControl("txtPassword"), TextBox).Text
  End Get
  Set(ByVal Value As String)
    EnsureChildControls()
    CType(Me.FindControl("txtPassword"), TextBox).Text = Value
  End Set
End Property

Private m_strUserName As String
<Category("Data"), Description("User Name of the individual"), TypeConverter(GetType(StringConverter))> _
Public Property UserName() As String
  Get
    EnsureChildControls()
    Return CType(Me.FindControl("txtUserName"), TextBox).Text
  End Get
  Set(ByVal Value As String)
  EnsureChildControls()
    CType(Me.FindControl("txtUserName"), TextBox).Text = Value
  End Set
End Property

In addition to the password label and textbox, a button is added to generate the login event to the system. The credentials of the user are passed as a custom EventArgs object. To connect the button to the event, the AddHandler method is used, which receives the event to handle and the delegated function to receive focus when this event fires. The LinkButton is below:

Dim objLink As New LinkButton
With objLink
  .ID = "lnkLogin"
  .Text = "Login"
  .CopyBaseAttributes(Me)

  'link the click event to a function (delegate)
  AddHandler .Click, AddressOf lnkButton_Click
End With
Me.Controls.Add(objLink)

The AddHandler method above connects the LinkButton click event to a method below. This method is a delegate, which now acts as the event handler for the LinkButton. This delegate then triggers the Login event, using the RaiseEvent method. For the event argument, the custom LoginInfoEventArgs class receives the user name and password in it's constructor, which is created on the fly.

Private Sub lnkButton_Click(ByVal sender As Object, ByVal e As EventArgs)
  RaiseEvent Login(Me, New LoginInfoEventArgs(UserName, Password))
End Sub

The Login event declaration for the class is declared using the event keyword. There are two ways to declare parameters for the event method: declaring them specifically, or using a delegate (similar to the process above). Both ways are shown below:

Public Event Login As LoginInfoEventHandler
Public Event Login(ByVal sender As Object, ByVal e As LoginInfoEventArgs)

Public Delegate Sub LoginInfoEventHandler(ByVal sender As Object, ByVal e As LoginInfoEventArgs)

The Render method passes the HTMLTextWriter object received from the parameter and passes it to the RenderChild() method. This method handles the rendering of each child control in the Controls collection.

Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
  EnsureChildControls()
  RenderChildren(writer)
End Sub

That is a composite control as an example. It isn't that difficult to create one; however, sometimes it can be confusing and/or complex to the amount of code involved. If you look at the code and see it all together, you will see that it isn't that complicated.

Future Considerations

First and foremost, this composite control was built without validation controls, which is an absolute must. These would be declared and instantiated in the CreateChildControls method. Validation controls assist in the proper formatting of data (never assume the user will enter correct data) and protect against hacking attacks (by only allowing certain character data). Also, when accessing a database for login credentials, use stored procedures over ad hoc SQL statements where possible. This prevents against SQL Injection attacks against your application.

You may download the code here.