Composite Controls

Occasionally, building custom controls by combining other controls is useful. A control that serves as a container for other controls is called a composite control, and it, too, is an important element of ASP.NET’s server control architecture.

All Control-derived classes have the innate ability to act as containers for other controls. Contained controls, or child controls, are exposed through the parent control’s Controls property, which is inherited from Control. Controls’ type is ControlCollection, which provides methods and properties for adding controls, removing controls, enumerating controls, and more. System.Web.UI.Page counts Control among its base types. Its Controls collection defines all the controls on the page.

Composite controls come in two basic varieties: declarative and programmatic. A declarative custom control contains other controls declared in a Web form. The FCL’s Panel control is one example of a declarative composite. It acts as a container for other controls and allows them to be manipulated programmatically, but it doesn’t create the controls that it contains: you create these controls by declaring them between <asp:Panel> and </asp:Panel> tags. By contrast, a programmatic composite creates the controls that it hosts programmatically. Both types of composites are discussed in the sections that follow.

Declarative Composites

Here’s the simplest composite control you can build:

using?System.Web.UI;

namespace?Wintellect
{
????public?class?CompositeControl?:?Control
????{
????}
}

On the surface, it doesn’t seem as if this control could do anything useful. But check this out:

<%@?Register?TagPrefix="win" Namespace="Wintellect" 
????Assembly="CompositeControl" %>

<html>
??<body>
????<form?runat="server">
??????<win:CompositeControl?ID="MyComposite" RunAt="server">
????????<asp:Label?Text="Hello!" RunAt="server" /><br>
????????<asp:Label?Text="Goodbye!" Runat="server" />
??????</win:CompositeControl>
????</form>
??</body>
</html>

In this example, CompositeControl serves as a container for a pair of Label controls. ASP.NET automatically adds the Label controls to the CompositeControl’s Controls collection. Furthermore, CompositeControl’s inherited Render method calls the child controls’ Render methods. If a server-side script prevents CompositeControl’s Render method from being called by setting the control’s Visible property (which it inherits from Control) to false, like this:

MyComposite.Visible?=?false;

the children’s Render methods won’t be called either. Consequently, the children will disappear from the Web page—all because you turned off the control that contains them.

The GroupBox Control

Windows developers are familiar with the group box control, which draws a stylish border around other controls and visually groups them together. HTML 4 and later versions support a <fieldset> element that looks very much like a group box. The following HTML displays three radio buttons surrounded by a group box:

<html>
??<body>
????<form>
??????<fieldset>
????????<legend>Colors</legend>
????????<input?type="radio" name="Color" value="red">Red<br>
????????<input?type="radio" name="Color" value="green">Green<br>
????????<input?type="radio" name="Color" value="blue">Blue
??????</fieldset>
????</form>
??</body>
</html>

The <fieldset> element is an ideal candidate to be encapsulated in a custom control, and a composite control fits the bill perfectly. Figure 8-19 contains the source code for an ASP.NET GroupBox control. It renders its children by calling the base class’s Render method, but it surrounds the child controls’ output with <fieldset> and </fieldset> tags. Figure 8-20 contains a GroupBox test page, and Figure 8-18 shows the output. Clicking the check box at the top of the page toggles the GroupBox control (and by extension, its children) on and off by alternately setting the GroupBox’s Visible property to true and false.

RadioButtonList is a composite control, too, so this example nests one composite control inside another. The ListItems are children of the RadioButtonList, and the RadioButtonList is a child of the GroupBox. That means—you guessed it—the GroupBox has grandchildren!

Figure 8-18
Controls grouped with a GroupBox control.
GroupBox.cs
using?System;
using?System.Web.UI;

namespace?Wintellect
{
????public?class?GroupBox?:?Control
????{
????????string?MyText?= "";

????????public?string?Text
????????{
????????????get?{?return?MyText;?}
????????????set?{?MyText?=?value;?}
????????}

????????protected?override?void?Render?(HtmlTextWriter?writer)
????????{
????????????//?Output?a?<fieldset>?tag
????????????writer.WriteBeginTag?("fieldset");
????????????if?(ID?!=?null)
????????????????writer.WriteAttribute?("id",?ClientID);
????????????writer.Write?(HtmlTextWriter.TagRightChar);

????????????//?Output?a?<legend>?element
????????????if?(Text.Length?>?0)?{
????????????????writer.WriteBeginTag?("legend");
????????????????writer.Write?(Text);
????????????????writer.WriteEndTag?("legend");
????????????}

????????????//?Output?the?content?between?<fieldset>?and?</fieldset>?tags
????????????base.Render?(writer);

????????????//?Output?a?</fieldset>?tag
????????????writer.WriteEndTag?("fieldset");
????????}
????}
}
Figure 8-19
GroupBox control.
GroupBoxPage.aspx
<%@?Register?TagPrefix="win" Namespace="Wintellect" 
????Assembly="GroupBoxControl" %>

<html>
??<body>
????<form?runat="server">
??????<asp:CheckBox?ID="Toggle" Text="Show?colors" 
????????OnCheckedChanged="OnToggle" AutoPostBack="true"
????????Checked="true" RunAt="server" /><br>
??????<win:GroupBox?ID="MyGroupBox" Text="Colors" RunAt="server">
????????<asp:RadioButtonList?RunAt="server">
??????????<asp:ListItem?Text="Red" Selected="true" RunAt="server" />
??????????<asp:ListItem?Text="Green" RunAt="server" />
??????????<asp:ListItem?Text="Blue" RunAt="server" />
????????</asp:RadioButtonList>
??????</win:GroupBox>
????</form>
??</body>
</html>

<script?language="C#" runat="server">
??void?OnToggle?(Object?sender,?EventArgs?e)
??{
??????MyGroupBox.Visible?=?Toggle.Checked;
??}
</script>
Figure 8-20
GroupBox test page.
Programmatic Composites

Programmatic composite controls create child controls programmatically. All controls inherit a virtual CreateChildControls method from Control that can be overridden in a derived class. The .NET Framework calls CreateChildControls very early in the control’s lifetime, so it’s the perfect place to instantiate child controls. The following control is the programmatic equivalent of the declarative control presented a few moments ago—the one containing Label controls with the greetings “Hello!” and “Goodbye!”

using?System.Web.UI;
using?System.Web.UI.WebControls;

namespace?Wintellect
{
????public?class?CompositeControl?:?Control
????{
????????protected?override?void?CreateChildControls?()?
????????{
????????????Label?label1?=?new?Label?();
????????????label1.Text?= "Hello!";
????????????Controls.Add?(label1);
????????????Controls.Add?(new?LiteralControl?("<br>"));
????????????Label?label2?=?new?Label?();
????????????label2.Text?= "Goodbye!";
????????????Controls.Add?(label2);
????????}
????}
}

In its override of CreateChildControls, CompositeControl instantiates the Label controls with new and adds them to its Controls collection with ControlCollection.Add. It also uses a Literal control to insert a <br> element between the Labels.

The LoginControl

The Web page in Figure 8-21 is built around a composite control named LoginControl. LoginControl is functionally similar to the login control presented in Chapter 7. When instantiated, it creates two TextBox controls and a Button control and adopts them as child controls. If the user enters a user name and password and clicks the button, the control fires a Login event. A server-side script can process the event and retrieve the user name and password by reading the control’s UserName and Password properties. LoginControl’s source code appears in Figure 8-22. You’ll find a page to test it with in Figure 8-23.

One nuance of composite controls that you should be aware of regards the INamingContainer interface. All but the most trivial composite controls should derive from INamingContainer. If they don’t, they’re liable to suffer strange maladies, including events that don’t fire properly. INamingContainer’s purpose is to allow ASP.NET to assign child controls names that are unique with respect to other controls on the page. Fortunately, INamingContainer requires no implementation. It’s a signal interface, meaning it has no methods. Simply including INamingContainer in a control’s list of base types is sufficient to “implement” the interface.

Figure 8-21
Custom login control.
LoginControl.cs
using?System;
using?System.Web.UI;
using?System.Web.UI.WebControls;

namespace?Wintellect
{
????public?class?LoginControl?:?Control,?INamingContainer
????{
????????TextBox?MyUserName?=?new?TextBox?();
????????TextBox?MyPassword?=?new?TextBox?();
????????public?event?EventHandler?Login;

????????public?string?UserName
????????{
????????????get?{?return?MyUserName.Text;?}
????????????set?{?MyUserName.Text?=?value?;?}
????????}

????????public?string?Password
????????{
????????????get?{?return?MyPassword.Text;?}
????????????set?{?MyPassword.Text?=?value?;?}
????????}

????????protected?override?void?CreateChildControls?()?
????????{
????????????Controls.Add?(MyUserName);
????????????Controls.Add?(new?LiteralControl?("<br>"));
????????????Controls.Add?(new?LiteralControl?("<br>"));

????????????MyPassword.TextMode?=?TextBoxMode.Password;
????????????Controls.Add?(MyPassword);
????????????Controls.Add?(new?LiteralControl?("<br>"));
????????????Controls.Add?(new?LiteralControl?("<br>"));

????????????Button?button?=?new?Button?();
????????????button.Text?= "Log?In";
????????????Controls.Add?(button);

????????????button.Click?+=?new?EventHandler?(OnLogin);
????????}

????????protected?void?OnLogin?(Object?sender,?EventArgs?e)
????????{
????????????if?(Login?!=?null?&&?UserName.Length?>?0?&&
????????????????Password.Length?>?0)
????????????????Login?(this,?new?EventArgs?());
????????}
????}
}
Figure 8-22
LoginControl source code.
LoginControlPage.aspx
<%@?Register?TagPrefix="win" Namespace="Wintellect"
??Assembly="LoginControl" %>							?

<html>
??<body>
????<h1>Login?Control?Demo</h1>
????<hr>
????<form?runat="server">
??????<win:LoginControl?ID="Login" OnLogin="OnLogin" RunAt="server" />
????</form>
????<hr>
????<asp:Label?ID="Output" RunAt="server" />
</html>
<script?language="C#" runat="server">
??void?OnLogin?(Object?sender,?EventArgs?e)
??{
??????Output.Text?= "Hello, " +?Login.UserName;
??}
</script>
Figure 8-23
LoginControl test page.