ControlTower
LANGUAGES: C# | VB
TECHNOLOGIES: Custom Controls
Build Composite Controls
Combine the power of HTML controls or Web controls, or use a little of both.
By Doug Seven
The ASP.NET Framework enables rapid Web application development through a number of tools, including user controls and server controls. Although the framework ships with several included server controls, user controls must be built to suit a specific need and they are limited in scope to a single Web application (see my article User Controls). Server controls are compiled controls that provide predefined functionality, such as a Label control or Calendar control. Of course, not every control you might want or need is shipped with the .NET Framework. Fortunately, building your own controls is not too difficult.
Server controls can be broken into three categories: composite controls, templated controls, and data-bound templated controls. In this article, I will introduce you to composite controls and show you how to build one. Specifically, I will show you a couple different techniques for building composite controls, and I'll teach you how to build controls that won't conflict when multiple instances exist on the same Web form by implementing the INamingContainer interface.
What is a Composite Control?
A composite control is simply a control that combines multiple existing controls. This could be a combination of HTML controls or Web controls, or a combination using both kinds of controls. A composite control enables a single server control to encapsulate the functionality of multiple server controls.
To build a composite control, begin by creating a class that derives from the System.Web.UI.WebControls.WebControl class (or another control class that you want to derive from). By deriving from the WebControl class, your composite control inherits all the basic functionality of a standard server control, including basic properties such as Visible, Enabled, Controls, etc., and basic methods such as Render and CreateChildControls among others. I mention these two methods specifically because they provide two different means of rendering a composite control to a Web form. The Render method enables you to specify the exact rendering of the HTML to be output, while CreateChildControls enables you to build a control hierarchy and lets each control you add to the parent composite control's Controls collection handle the rendering. Like many things in ASP.NET, the Render and CreateChildControls methods exist to provide different functionality - they are not mutually exclusive, one is not necessarily a better choice than the other, and you can decide to use one method or the other (or both methods) based on the correct choice for the control you are building.
In this article, I will show you how to build the same control using the Render method and the CreateChildControls method by building an Order control for displaying order information from the Northwind database. Depending on the approach, the code for the controls ranges from 360 to 525 lines, so I will not list all of it in this article. I'll introduce you to each important topic with a small code sample, but you can download the complete code.
Build the Control Structure
Building a control is similar to building any class file - you start by declaring your namespace and class. For a control, however, you inherit from the System.Web.UI.WebControls.WebControl class, which provides your control with the basic functionality of a Web control:
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Data;
namespace CompositeControls
{
[ToolboxData("<{0}:Order runat=server>{0}:Order>")]
public class Order : System.Web.UI.WebControls.WebControl
{
}
}
I used the ToolboxData attribute to specify how the control tag will be added to a Web form in the IDE of an editor such as Visual Studio .NET. I could add additional attributes to ToolboxData. This control tag is added to a Web form when a developer drags my control from the IDE toolbox to the Web form designer. The ToolboxData attribute is not required, but it does provide a nice benefit for developers working with a tool such as Visual Studio .NET.
Within the control class I created, I can create properties that my control will expose. Because I am building a control to render a single order (and the order detail) from the Northwind database, I want to add some properties that describe the order. Figure 1 shows a list of the properties I want to expose.
Property |
Data Type |
Description |
OrderID |
Int32 |
The OrderID value from the database. |
CustomerID |
String |
The CustomerID value from the database. |
OrderDate |
DateTime |
The OrderDate value from the database. |
RequiredDate |
DateTime |
The RequiredDate value from the database. |
ShippedDate |
DateTime |
The ShippedDate value from the database. |
ShippedToName |
String |
The ShipName value from the database. |
ShippedToAddress |
String |
The ShipAddress value from the database. |
ShippedToCity |
String |
The ShipCity value from the database. |
ShippedToRegion |
String |
The ShipRegion value from the database. |
ShippedToPostalCode |
String |
The ShipPostalCode value from the database. |
ShippedToCountry |
String |
The ShipCountry value from the database. |
Products |
DataTable |
A DataTable of products shipped in the order. |
Figure 1. Here is a list of properties you might want your control to expose.
To expose a property, you create a private variable instance to hold the property value and a public property with set and get accessors:
private Int32 _orderID;
[Bindable(true), Category("Appearance"), DefaultValue("")]
public Int32 OrderID
{
get
{
return _orderID;
}
set
{
_orderID = value;
}
}
The public property instance is preceded with the Bindable, Category, and DefaultValue attributes. The Bindable attribute specifies that this property supports two-way data binding, and it will be listed in the DataBindings Window in Visual Studio .NET (see Figure 2). The Category attribute specifies which category the property will be listed under in the Properties Window in Visual Studio .NET. The DefaultValue attribute specifies the default value for the property.
Figure 2. These are the properties supporting two-way data binding,
listed in the Data Bindings Window.
Override the Render Method
The Render method is a method of the base class, WebControl. You can use this method to provide specific output to render in the Web page generated when the Web form the control is a part of is requested. The Render method takes one argument, HtmlTextWriter, which sends the output (HTML tags and text) to the browser. HtmlTextWriter has many methods useful for controlling the output, such as WriteBeginTag(String), WriteAttribute(String, String), Write(String), and WriteLine. You can override the Render method and specify the exact output to send to the browser in a linear fashion.
Figure 3 shows the Order control as it renders to a Web page. The Order control renders a table with three columns. In the first row, only the first and third column contain text (the Customer ID and the Order ID); the second row spans all three columns and is filled with the shipping information; the third row has the Order Date, Required Date, and Shipped Date, each in one of the three columns; and the fourth row has another table embedded in it, displaying all the products included in the order.
Figure 3. Here is how the Order control is rendered to a Web page.
The output format in Figure 3 is managed by providing specific instructions in the overridden Render method (see Figure 4).
protected override void Render(HtmlTextWriter _output)
{
//Write the opening
_output.WriteFullBeginTag("td"); _output.WriteLine(); _output.Indent++;
//Customer ID _output.WriteFullBeginTag("b"); _output.Write("Customer ID: "); _output.WriteEndTag("b"); _output.Write(" "); _output.Write(this.CustomerID);
// |
_output.WriteFullBeginTag("td"); _output.WriteLine(); _output.Indent++;
//This is an empty cell
// |
_output.WriteBeginTag("td"); _output.WriteAttribute("align", "right"); _output.Write(HtmlTextWriter.TagRightChar); _output.WriteLine(); _output.Indent++;
//Order ID _output.WriteFullBeginTag("b"); _output.Write("Order ID: "); _output.WriteEndTag("b"); _output.Write(" "); _output.Write(this.OrderID.ToString());
// |
_output.Indent--;
_output.WriteEndTag("table");
_output.WriteLine();
}
Figure 4. You manage the output format of the Order control by providing specific instructions in the overridden Render method.
The Render method takes the HtmlTextWriter (named _output) as its input argument, which is used to write the HTML output to the browser. Figure 5 lists some of the methods the HtmlTextWriter class provides.
Method |
Description |
||||||||||||
WriteBeginTag(String) |
Writes the opening of an HTML element's beginning tag.
For example: _output.WriteBeginTag("table") renders WriteAttribute(String, String) Writes the attribute name and value. For example: _output.WriteAttribute("border",
"0") renders border="0". Write(String) Writes out the test passed to it to the browser. You can
pass the public field HtmlTextWriter.TagRightChar
as the argument, which outputs the closing bracket of the HTML tag (>). WriteFullBeginTag(String) Writes the full (opening and closing bracket) HTML tag
for the String argument passed in. For example: _output.WriteFullBeginTag("b")
renders . WriteEndTag(String) Writes the closing tag for the String argument passed
in. For example: _output.WriteEndTag("b") renders . Indent A property of the
HtmlTextWriter class. Specifies how many spaces to indent at the
beginning of a line of HTML. Used to provide clean HTML source code. Double
plus signs (++) indicate that the indent should go into the next tab stop; a
minus sign (-) indicates that the indent should move back to the last tab
stop. WriteLine Writes the String argument passed in (if there is one)
followed by a line break in the HTML. Figure 5. Here is a list of some of the methods HtmlTextWriter provides.
Once you build the control, you can compile it into a dll and distribute it for use in any ASP.NET Web application. You can use a command-line compiler to build the dll with a command similar to this:
csc.exe /t:library /out:CompositeControls.dll *.cs /r:System.dll,System.Web.dll,System.Data.dll
If you are using Visual Studio .NET, you can build the dll by selecting the Build menu option and choosing Build Solution.
Use a Custom Control on a Web Form Once you create the control dll, you can place it in the \bin folder of an ASP.NET Web application, which gives you access to the control in that application (you also can register the control in the Global Assembly Cache to have access to it from all Web applications on the Web server). On a Web form, you register the control using the @ Register directive, which allows you to use the control throughout the form:
<%@ Register TagPrefix="cc1" Namespace="CompositeControls" Assembly="CompositeControls" %>
You can then place the control anywhere on the form:
Add Controls in Visual Studio .NET If you are using Visual Studio .NET, you can make a reference to the control assembly, add the control to the toolbox in the IDE, and drag and drop the control onto a Web form. Here's how you add a reference to the assembly. Right-click on the References folder below the project name in the Solution Explorer and click on Add Reference.... Next, while the .NET tab is selected, click on Browse and navigate to the dll in the \bin folder. Then, select the dll and click on Open. Finally, click on OK.
Next, add the control to the toolbox. Right-click on the toolbox and select Customize Toolbox. Then select the.NET Framework Components tab. Click on Browse and navigate to the dll in the \bin folder. Select the dll and click on Open, then click on OK.
A generic icon for the control should appear in the toolbox. You now can drag and drop this control onto a Web form. Visual Studio .NET adds the @ Register directive for you automatically, places the control tag in the appropriate place in the Web form presentation code, and maps an object instance to the control instance in the code-behind class.
In the code of the Web form, you can get the order information for an order and set the appropriate properties of the control in the Page_Load event handler (see Figure 6).
Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load ' Data access code removed for text - see code ' download for complete code Order1.OrderID = _ _dataSet.Tables("OrderInfo").Rows(0).Item(0) Order1.CustomerID = _ _dataSet.Tables("OrderInfo").Rows(0).Item(1) Order1.OrderDate = _ _dataSet.Tables("OrderInfo").Rows(0).Item(2) Order1.RequiredDate = _ _dataSet.Tables("OrderInfo").Rows(0).Item(3) Order1.ShippedDate = _ _dataSet.Tables("OrderInfo").Rows(0).Item(4) Order1.ShippedToName = _ _dataSet.Tables("OrderInfo").Rows(0).Item(5) Order1.ShippedToAddress = _ _dataSet.Tables("OrderInfo").Rows(0).Item(6) Order1.ShippedToCity = _ _dataSet.Tables("OrderInfo").Rows(0).Item(7) Order1.ShippedToRegion = _ _dataSet.Tables("OrderInfo").Rows(0).Item(8) Order1.ShippedToPostalCode = _ _dataSet.Tables("OrderInfo").Rows(0).Item(9) Order1.ShippedToCountry = _ _dataSet.Tables("OrderInfo").Rows(0).Item(10) End Sub Figure 6. In Visual Basic .NET, you can obtain order information and set control properties in the Page_Load event handler.
Override the CreateChildControls Method The CreateChildControls method is another method of the base class Control that allows you to build a control tree of child controls that render when the parent control is rendered. Overriding the CreateChildControls method enables you to build a control tree of both HTML controls and ASP.NET Web Controls and add them to the parent control's Controls collection. The control's basic structure is the same as the previous example, but rather than overriding the Render method you override the CreateChildControls method and build the composite control using ASP.NET Web Controls (see Figure 7).
protected override void CreateChildControls() { //Define the child controls Table _table = new Table(); TableCell _tableCell; TableRow _tableRow;
Table _productsTable; TableHeaderCell _productsHeader; TableCell _productsCell; TableRow _productsRow;
_table.ID = "InfoTable"; _table.CellPadding = 4; _table.CellSpacing = 0; _table.BorderColor = Color.Tan; _table.BorderWidth = Unit.Pixel(1); _table.BorderStyle = BorderStyle.Solid; _table.GridLines = GridLines.None;
//Create the header row _tableRow = new TableRow();
_tableCell = new TableCell(); _tableCell.Text = "Customer ID: " + this.CustomerID; _tableRow.Cells.Add(_tableCell);
_tableCell = new TableCell(); _tableRow.Cells.Add(_tableCell);
_tableCell = new TableCell(); _tableCell.Text = "Order ID: " + this.OrderID; _tableCell.HorizontalAlign = HorizontalAlign.Right; _tableRow.Cells.Add(_tableCell);
_table.Rows.Add(_tableRow);
//Remaining code removed for space //See code download for complete code
//Add the Table to the Controls collection this.Controls.Add(_table); } Figure 7. To create a composite control of ASP.NET Web Controls, you override the CreateChildControls method instead of the Render method.
Figure 7 contains the same HTML layout as the first Order control from Figure 4, but this time I used ASP.NET Web Controls to create the control. The resulting output is identical to the original Order control, but the actual HTML rendering is handled by the ASP.NET Controls I added to the parent control's Controls collection.
Implement INamingContainer Implementing the INamingContainer interface ensures unique naming of all child controls in a composite control by prefixing the child control names with the name of the parent control and an underscore.
In Figure 7, I added an ID value to the parent Table
control. When the OrderToo control was rendered, the parent cellpadding="4" bordercolor="Tan"
border="0" style="border-color:Tan;border-width:1px; border-style:Solid;border-collapse:collapse;"> The public class OrderToo : System.Web.UI.WebControls.WebControl, INamingContainer Now the resulting HTML output is rendered like this: cellpadding="4" bordercolor="Tan"
border="0" style="border-color:Tan;border-width:1px; border-style:Solid;border-collapse:collapse;"> Note that the ID value of the The INamingContainer interface is used on composite
and complex controls such as DataGrid and DataList to provide
unique naming to the child controls in the parent control's Controls
collection. It is a good practice to implement INamingContainer on any
composite control. You can create reusable controls by building a class that
derives from the System.Web.UI.WebControls.WebControl class, or any
other control class you want to inherit from. By overriding the Render
method, you can control the HTML output of your control explicitly. Alternately,
you can use the CreateChildControls method to build a control tree of
both HTML and ASP.NET Web Controls and allow the child controls to control
their own rendering. By implementing INamingContainer, you can guarantee
unique naming for all the child controls in your composite control, preventing
any naming conflicts when multiple instances of your controls are used on a
single Web form. The code referenced in this
article is available for download. Doug Seven is a co-founder of the .NET training company
DotNetJunkies (http://www.dotnetjunkies.com).
He has had previous technical roles at Nordstrom, Microsoft, and
GiftCertificates.com, and he has experience as a training specialist at Seattle
Coffee Company. Doug co-authored Programming Data-Driven Web Applications with ASP.NET and ASP.NET: Tips, Tutorials
& Code (Sams), and he has several titles in the works. E-mail Doug
at mailto:[email protected]. Tell us what you think! Please send any comments about
this article to [email protected].
Please include the article title and author. |