Cutting Edge

ASP.NET Controls Templates

Dino Esposito

Code download available at:CuttingEdge0306.exe(141 KB)

Contents

The Common Ground of Web Controls
An HTML Chart Generator
The Rendering Engine
Designing a Data-bound Control
Resolving Data Sources
Designing a Templated Control

The simplest way to create a custom control in ASP.NET is to derive a new class from an existing control. Creating custom controls through inheritance this way is not really very different from creating any other derived class. Creating new ASP.NET controls from scratch, on the other hand, just may prove to be a more challenging undertaking.

When you need a Web server control and none of the existing ones meet your requirements, you can derive a new control from one of the base control classes—Control or WebControl. These classes provide all the basic properties and functionality of a Web server control and let you focus on the features you need.

From a high-level perspective, a Web server control is a software component that abstracts a piece of HTML markup and builds an object model on top of it. No matter how much programming power you have on the back end, remember that an ASP.NET control works within the boundaries of a Web page and, therefore can output only HTML and script code. If the functionality you have in mind cannot be obtained with a combination of HTML and client-side script, then I seriously doubt you will be able to make it work over the Web.

An ASP.NET custom control is a class with public methods, properties, and events that renders its state to an HTML text writer. The object model and the rendering engine are the key elements of any Web server control. In this column, I'll build an ASP.NET control from the ground up and then I'll expand it to support data binding and templates.

The Common Ground of Web Controls

The root class of all Web controls is System.Web.UI.Control. This class defines the properties, methods, and events common to all ASP.NET server controls. These include the methods and events that determine and govern the lifecycle of the control plus a few properties such as ID, UniqueID, Parent, ViewState, and the collection of child controls named Controls.

The WebControl class derives from Control and adds extra properties and methods, mostly concerning the user interface. These properties include ForeColor, BackColor, Font, Height, and Width. WebControl is the base class for the family of ASP.NET Web server controls, whereas Control serves as the base for all HTML controls.

If your control must render a rich user interface, then you should derive it from WebControl. If you are authoring a component that does not need to provide specific user interface features, your best bet for a base class is Control. Although these rules are appropriate in most cases, there might be situations in which other options are reasonable. For example, you can derive from Control if you want to provide a subset of the UI features, or when you combine multiple controls together to build a composite control. In this case, typical user interface properties such as BackColor or Font should be duplicated to let users address the background and the font of individual controls. In similar situations, you might want to get rid of the standard programming interface to roll your own.

Depending on the functionality of the control you're creating, you might have to implement some additional interfaces. Typically, a Web server control may implement any of the following three interfaces: INamingContainer, IPostBackDataHandler, and IPostBackEventHandler.

INamingContainer doesn't contain methods, but simply notifies the ASP.NET runtime that the control should be considered to be a naming container. Child controls contained within a naming container parent control have their UniqueID property prefixed with the ID of the container. The naming container, therefore, acts as a namespace and guarantees the uniqueness of the control within the specified naming scope. The use of the INamingContainer interface is essential if you're writing iterative or templated controls such as Repeater or DataList.

IPostBackDataHandler is needed whenever your control must examine postback data. If the user can execute actions that affect the state of the control, then you need to look into the data posted by the browser. For example, a TextBox control stores its configuration in the view state, but also needs to read what the user may have typed in. IPostBackDataHandler is also helpful if you need to raise events on the server based on changes to the posted data. Again, the TextBox is the perfect sample control; if the typed text changes between postbacks, the TextChanged event is raised.

IPostBackEventHandler serves to capture a client-side postback event (a click). Upon postback, the ASP.NET runtime first raises data change events, then looks for a server control whose UniqueID property matches the name of a posted value (for example, the name of the clicked button). If a match is found and the control implements IPostBackEventHandler, ASP.NET invokes the RaisePostBackEvent method on the control. RaisePostBackEvent is the only method defined on the IPostBackEventHandler interface. What a particular control does within the RaisePostBackEvent method can vary quite a bit. For example, the Button control—the typical control which would implement this interface—fires the Click event to the page.

Another important piece of the control's infrastructure is the HTML text writer, the companion class that generates HTML markup code. You seldom use such a writer if you derive the control from an existing one. However, whenever you build the control from the ground up you inevitably need to write the HTML output to the stream.

The HtmlTextWriter class writes HTML-specific characters and text on the logical canvas of an ASP.NET page. The class also provides formatting capabilities that ASP.NET server controls can take advantage of when rendering HTML content to clients. If you're familiar with XML writers (see my May 2003 article, "Real-World XML: Manipulate XML Data Easily with Integrated Readers and Writers in the .NET Framework"), then you won't find the programming model of HTML writers to be anything new.

An HTML Chart Generator

Several third-party controls provide highly specialized charting components for use with Web pages. In addition, the new features of the System.Drawing namespace make it possible for you to exploit GDI+ to create your own graphics and output them as images. If you don't need to create special types of charts (for example, pie or 3D charts), a lightweight but equally effective alternative exists—2D charts created with HTML tables. As long as you're content with a plain 2D bar chart, a pure HTML solution is possible and might even be considered somewhat snazzy and elegant.

An HTML bar chart control is made of a sequence of table elements with two rows and one cell. The height and colors of each row are calculated so that the output looks like a vertical gauge. Figure 1 lists the properties exposed by the control.

Figure 1 HTML Bar Chart Properties

Property Description
BackImageUrl Indicates the URL of the image used to tile the background
BarCount Gets the number of bars that form the chart
Caption Title of the chart
CaptionTemplate Template that represents the header of the chart; if not specified, Caption and SubTitle are used
ChartColor Default color used to fill the bars in the chart
DataSource Data source associated with the control
DataTextField Field in the data source to use for the label of the bar
DataValueField Field in the data source to use for the value of the bar
FooterTemplate Template that represents the footer of the chart
Maximum Maximum value the chart can represent; set to 100 by default
SubTitle Subtitle of the caption; displays below the caption with a smaller font

In addition to the properties listed in Figure 1, the control will use ForeColor and BackColor as well as Width, Height, and Font. All of these are inherited directly from WebControl. Each bar in the chart is fully represented with a custom data structure called BarChartItem. Instances of this class are created during the data binding process. Chart items are stored in an internal collection (similar to the Items collection of many data-bound controls) that is persisted to the view state:

[Serializable] protected class BarChartItem { public string Text; public float Value; }

Note that the class must be marked as serializable if you want to cache it in the control's view state. More precisely, to be persisted in the view state, a type must either be serializable (marked with the Serializer attribute or implementing ISerializable) or have a TypeConverter object defined. Of the two possibilities, having a type converter is the most efficient from the view state's perspective. In the Microsoft® .NET Framework, type converters are a sort of lightweight and highly specialized serializer. Storing a serializable type that doesn't provide a converter in the view state results in slower code and may generate a significantly larger output. Type converters are also useful to give a design-time behavior to controls and to integrate them seamlessly with Visual Studio® .NET.

The view state is serialized using an object serialization format. This format is optimized for a few types, including primitive types, strings, arrays of primitives or strings, and HashTable types. As a result, you can either write a type converter or use any of these simple types. A control can also customize the way in which its property data is stored in the ViewState collection. To obtain this, the control must override the SaveViewState and LoadViewState methods on the Control class. These methods have the following signatures:

protected virtual object SaveViewState(); protected virtual void LoadViewState(object savedState);

The BarChart control persists its data source in the view state. I could have chosen another design, but a bar chart contains a few elements at most. After all, the DropDownList control works that way. The data source that you bind to a DropDownList control is saved to the view state, although not directly by the control. All items in the dropdown list are converted into ListItem objects and these objects are individually saved to the view state. If you think it over for a moment, you'll realize that in terms of internal structure and memory footprint, the DropDownList and BarChart controls are similar. Now that you understand my reasoning, take a look at parts of the source code for the new control, shown in Figure 2. Note that Figure 2 doesn't include the rendering code, nor does it contain data binding or templates.

Figure 2 BarChart

public class BarChart : WebControl { public BarChart() : base() { _barChartItems = new ArrayList(); Items = _barChartData; Caption = ""; SubTitle = ""; BackImageUrl = ""; BackColor = Color.White; ForeColor = Color.Black; ChartColor = Color.Orange; Maximum = 100; Font.Name = "verdana"; Font.Size = FontUnit.Point(8); } public string BackImageUrl { get {return Convert.ToString(ViewState["BackImageUrl"]);} set {ViewState["BackImageUrl"] = value;} } public int BarCount { get { if (Items != null) return Items.Count; return 0; } } public Color ChartColor { get {return (Color) ViewState["ChartColor"];} set {ViewState["ChartColor"] = value;} } public string Caption { get {return Convert.ToString(ViewState["Caption"]);} set {ViewState["Caption"] = value;} } public string SubTitle { get {return Convert.ToString(ViewState["SubTitle"]);} set {ViewState["SubTitle"] = value;} } public float Maximum { get {return Convert.ToSingle(ViewState["Maximum"]);} set {ViewState["Maximum"] = value;} } private ArrayList _barChartItems; public void Add(string theLabel, float theValue) { BarChartItem bci = new BarChartItem(); bci.Text = theLabel; bci.Value = theValue; // Copy data to the view state if (_barChartItems != null) _barChartItems.Add(bci); DataSource = _barChartItems; } protected virtual ArrayList Items { get {return (ArrayList) ViewState["Items"];} set {ViewState["Items"] = value;} } }

The Add method lets you populate the control without resorting to complicated data binding techniques. You simply add values explicitly in much the same way you use <asp:listitem> elements with list controls, as shown here:

BarChart1.Caption = "Total Sales"; BarChart1.SubTitle = "(Far-East)"; BarChart1.Add("Q1", 5.8f); BarChart1.Add("Q2", 12.9); BarChart1.Add("Q3", 10.1); BarChart1.Add("Q4", 8.4f); BarChart1.Maximum = 15;

This code produces the chart shown in Figure 3.

Figure 3 Bar Chart

Figure 3** Bar Chart **

The Rendering Engine

The HTML structure of the BarChart control is a bit complex; it's comprised of a few nested tables (see Figure 4). The outermost table counts three rows—title, subtitle, and chart table. The chart table includes three rows—the chart, the separator, and the label. Each bar in the chart table is rendered as a standalone table with three rows and a single cell. The bottommost cell represents the value of the bar; the middle cell is the written value; the topmost cell is empty to denote what is left to the maximum.

Figure 4 HTML Structure of Bar Chart

Figure 4** HTML Structure of Bar Chart **

The BarChart control renders its output by building a graph of Table objects and rendering the output to HTML using the RenderControl method. Note that from a performance perspective, building a graph of objects is not necessarily the most efficient way to generate the output of a Web control. However, a graph of objects results in much more manageable code. The outermost table object mirrors many of the base properties of the WebControl, including colors, borders, and font. The code in Figure 5 shows the main portion of the Render method.

Figure 5 Rendering the BarChart

protected override void Render(HtmlTextWriter output) { // Create the outermost table Table outer = new Table(); outer.BorderColor = BorderColor; outer.BackColor = Color.White; outer.BorderStyle = BorderStyle; outer.BorderWidth = BorderWidth; outer.GridLines = GridLines.None; outer.CssClass = CssClass; outer.ForeColor = ForeColor; outer.Font.Name = Font.Name; outer.Font.Size = Font.Size; outer.BorderStyle = BorderStyle.Solid; // Create the caption row TableRow captionRow = new TableRow(); if (!Caption.Equals(String.Empty)) { TableCell captionCell = new TableCell(); captionCell.ColumnSpan = BarCount; captionCell.HorizontalAlign = HorizontalAlign.Center; captionCell.Font.Bold = true; captionCell.Font.Size = FontUnit.Larger; captionCell.Text = Caption; captionRow.Cells.Add(captionCell); } outer.Rows.Add(captionRow); // Create the subtitle row TableRow subtitleRow = new TableRow(); if (!SubTitle.Equals(String.Empty)) { TableCell subtitleCell = new TableCell(); subtitleCell.ColumnSpan = BarCount; subtitleCell.HorizontalAlign = HorizontalAlign.Center; subtitleCell.Font.Size = FontUnit.Smaller; subtitleCell.Text = SubTitle; subtitleRow.Cells.Add(subtitleCell); } outer.Rows.Add(subtitleRow); // Create the chart row if (Items != null) { TableRow chartRow = new TableRow(); TableCell chartCell = new TableCell(); CreateBarChart(chartCell); chartRow.Cells.Add(chartCell); outer.Rows.Add(chartRow); } // Render the output outer.RenderControl(output); }

The values inherent in the individual bar are packed in the BarChartItem structure and are used to draw and configure the chart table. The ratio between the current and maximum values is conveniently rendered with percentages (0 to 100), which simplifies working with HTML tables. When finished, the new table is connected to the parent table's cell. Figure 6 contains the source code that creates the bars.

Figure 6 Creating the Bars

protected virtual void CreateSingleChart(TableCell parent, BarChartItem bci) { // Calculate the value to represent in a 0-100 scale float valueToRepresent = 100*bci.Value/Maximum; // Create the bar Table t = new Table(); t.Font.Name = Font.Name; t.Font.Size = Font.Size; t.Width = Unit.Percentage(100); t.Height = Unit.Percentage(100); // The still-to-do area TableRow todoArea = new TableRow(); todoArea.Height = Unit.Percentage(100-valueToRepresent); todoArea.Cells.Add(new TableCell()); t.Rows.Add(todoArea); // The row with the value TableRow valueArea = new TableRow(); valueArea.Height = 10; TableCell cell = new TableCell(); cell.HorizontalAlign = HorizontalAlign.Center; cell.Text = bci.Value.ToString(); valueArea.Cells.Add(cell); t.Rows.Add(valueArea); // The row with bar TableRow barArea = new TableRow(); barArea.ToolTip = bci.ToolTip; barArea.Height = Unit.Percentage(valueToRepresent); barArea.BackColor = bci.BackColor; barArea.Cells.Add(new TableCell()); t.Rows.Add(barArea); // Connect to the parent parent.Controls.Add(t); }

The CreateSingleChart method is marked as protected and overridable to allow potential derived classes to modify how the control is rendered. Declaring protected and virtual methods is a good technique to allow customization from derived classes. As an alternative, you can consider firing events in much the same way the DataGrid and other iterative controls do. For example, you could fire a ChartTableCreated event when the chart table is created, or BarChartItemCreated when an individual bar has been created. Both events will pass references to tables and rows allowing users more specific customization, such as specifying more attractive styles or deciding chart colors on the fly according to runtime conditions.

To draw lines behind the bars to simulate a scale, you can specify a background image for the chart table. I used this trick to obtain the graphics in Figure 3. The picture doesn't have to be particularly large or complex; a simple transparent background and a one-pixel line would do the trick.

Designing a Data-bound Control

A data-bound control is primarily characterized by a data source property that references an enumerable object—that is, a type that implements ICollection or IEnumerable. A data-bound control also overrides the DataBind method and defines a few satellite properties to let developers better configure the mapping between data and control properties. The number and the type of these properties depend on the specific features you're implementing.

When overriding the DataBind method, a data-bound control must call the base implementation on the parent class, import data from the source, and prepare for rendering.

The typical name for the property that contains the data source is DataSource. Bear in mind that this is only a naming convention, not a requirement. The DataSource property represents the collection of data you want to display through the control. You can force it to be of any particular type that suits your needs; however, to comply with the .NET Framework standard, you should allow users to use an instance of a type that implements the IEnumerable or ICollection interface, an array, or a type that exposes the ITypedList or IListSource interface. DataTextField and DataValueField are string properties whose value is cached in the view state, as the following code shows:

public string DataTextField { get {return Convert.ToString(ViewState["DataTextField"]);} set {ViewState["DataTextField"] = value;} } public string DataValueField { get {return Convert.ToString(ViewState["DataValueField"]);} set {ViewState["DataValueField"] = value;} }

The DataBind method represents the entry point in the binding mechanism of ASP.NET. The method is responsible for firing the DataBinding event and for preparing the control for rendering. To make sure that the DataBinding event regularly fires, you only have to issue a call to the base method:

public override void DataBind() { // Call the base method base.DataBind (); // Import bound data into internal structures LoadBarChartData(); }

The LoadBarChartData method imports the bound data into the internal structure—the Items collection—where the Render method expects to find it (see Figure 7).

Figure 7 LoadBarChartData

void LoadBarChartData() { IEnumerable data; data = ResolveDataSource(DataSource, ""); if (data == null) return; // Enumerate the data items IEnumerator e = data.GetEnumerator(); while (e.MoveNext()) { object dataItem = e.Current; string theLabel = (string) DataBinder.Eval(dataItem, DataTextField); float theValue = Convert.ToSingle(DataBinder.Eval(dataItem, DataValueField)); Add(theLabel, theValue); } }

The ResolveDataSource method is a helper routine that returns a generic object reference to let the code manage data through a common programming interface. The actual objects that you can use for binding can be quite varied indeed—arrays, collections, DataSet, DataTable, DataView. Not all of them, however, implement the required interfaces. The DataSet object, for instance, doesn't implement the IEnumerable interface, but contains data that can be enumerated and processed as a collection. The same can be said for the DataTable class. The DataView object, on the other hand, is a collection of data and can be referenced as an IEnumerable component.

The call to ResolveDataSource normalizes all possible differences between the available data sources and gives the data-bound control a chance to process data in a uniform manner. The code for the method gets an enumerator for the data source and walks its way through the data.

Information from individual records is extracted using the DataBinder.Eval method. The values of the DataTextField and DataValueField properties are used at this time to obtain the label and the value for the bar. The two objects are then passed to the Add method and copied into the bar chart items list.

Resolving Data Sources

All in all, as long as you're building only data-bindable controls and disregarding templates, the trickiest part of development is not how you bind the data, but how you resolve the data source type. The algorithm that resolves a data source object to an IEnumerable object reference can be written in many ways. The difference between implementations would be given by the order in which you check for types. The code in Figure 8 shows one possible way of proceeding.

Figure 8 Resolving the Data Source

IEnumerable ResolveDataSource(object dataSource) { IListSource listSource; IList list; if (dataSource == null) return null; // First try: IListSource (i.e., DataSet, DataTable) listSource = (dataSource as IListSource); if (listSource != null) { // Get an object that represent the list list = listSource.GetList(); return list; } // Does the source implement IEnumerable? // (i.e., DataView, array, collections) if ((dataSource as IEnumerable) != null) return (IEnumerable) dataSource; return null; }

The code first attempts to cast the data source object to IListSource. The interface is designed to allow objects that do not implement ICollection to return a bindable list of data for data-bound controls. In the entire .NET Framework, only two classes implement this interface: DataSet and DataTable. Objects that implement IListSource provide a method called GetList, which returns the internal list of data that makes the object a bindable source. The DataSet returns the contents of its DataViewManager property; the DataTable returns its default view, which is also contained by the DefaultView property.

In general, the GetList method can return a collection of IList objects. This is the case when a DataSet object is used as the data source and a data member property indicates which table is to be used. The ContainsListCollection Boolean property specifies whether the IList object is a simple list or a collection of lists. In the latter case, more work is needed to match the member name with the contained tables.

The DataView objects, as well as arrays and collections, are cast directly to IEnumerable. Notice the use of the C# as operator in Figure 8. The as operator is like a cast, except that it returns null if the conversion can't be accomplished. The operator never throws an exception. A similar operator doesn't exist in Visual Basic® .NET, but can be simulated using the TypeOf...Is construct.

Figure 9 Sample Bar Chart

Figure 9** Sample Bar Chart **

You can now execute a query against a SQL Server database and bind the results to the new BarChart control. Let's consider the following SQL query:

SELECT e.lastname AS Employee, SUM(price) AS Sales FROM (SELECT o.employeeid, od.orderid, SUM(od.quantity*od.unitprice) AS price FROM Orders o, [Order Details] od WHERE Year(o.orderdate)=@TheYear AND od.orderid=o.orderid GROUP BY o.employeeid, od.orderid ) AS t1) INNER JOIN Employees e ON t1.employeeid=e.employeeid GROUP BY t1.employeeid, e.lastname

It returns a resultset of two columns—Employee and Sales. The Employees column contains the last name of the employee; the Sales column stores the employee's total sales for a given year. Here's how you bind that information to the control:

BarChart1.Maximum = 150000; BarChart1.Caption = "Northwind Sales"; BarChart1.SubTitle = "(Year 1997)"; BarChart1.DataSource = ExecuteQuery(1997); BarChart1.DataTextField = "Employee"; BarChart1.DataValueField = "Sales"; BarChart1.DataBind();

The DataSource property is set with the DataTable that results from the query execution. The DataTextField and DataValueField properties are set with the names of the fields to use for the label and the value. Make sure you set the Maximum property with an appropriate value. Figure 9 and Figure 10 show a sample in action.

Figure 10 Scale Factor 150,000

Figure 10** Scale Factor 150,000 **

Designing a Templated Control

In ASP.NET, you can import templates in two ways—through properties of type ITemplate or by dynamically loading Web user controls. If you employ user controls, the template is insulated in a separate file, which may or may not be an advantage. Having the template available right in the source code of the page greatly simplifies page authoring. Templates do not have to be assigned at design time, however. To load templates at run time you need an instance of a class that implements ITemplate.

Adding templates to the BarChart control allows for a more effective customization of those parts of the UI that the user controls. By default, the BarChart control contains a header and a footer with standard formats. The header is made of text displayed in two rows with slightly different font and size settings. The footer is limited to displaying labels at the base of each bar. If you want a richer caption bar, or want to add information that helps people reading the chart, the BarChart control isn't the answer.

What about using templated controls? A templated control must follow a few practical rules. First, it must implement the INamingContainer interface so that any child controls can be given a unique name. You should also pay attention to the ParseChildrenAttribute attribute, applying it to your control so that the page parser knows that all child tags are to be parsed:

[ParseChildren(ChildrenAsProperties = true)] public class MyTemplatedControl : Control, INamingContainer { ••• }

However, if you derive the control from WebControl, using the ParseChildrenAttribute is unnecessary because the WebControl class is already marked with this attribute.

A templated control must have one or more properties of type ITemplate. A template property represents a collection of text and controls that is hosted within a container. The container is also responsible for exposing properties that page authors can use to create data-bound expressions. The following code snippet shows how to define a template property named CaptionTemplate:

[TemplateContainer(typeof(ChartTemplateContainer))] public ITemplate CaptionTemplate {...}

The argument of the TemplateContainer attribute is the type of the container control in which you want the template to be instantiated. The container control is different from the templated control that you're building. A container that is independent from the main templated control lets you iterate the template many times using different data. The template container must be a naming container, too.

Finally, a templated control should override CreateChildControls and instantiate the template within the container. The container class is then added to the root control. A local event handler for the DataBinding event is also necessary to ensure that all child controls are created before the ASP.NET runtime attempts to process data-bound expressions. Again, note that overriding CreateChildControls is not mandatory, but certainly represents a programming best practice.

The <CaptionTemplate> property is aimed at providing a custom representation of the topmost part of the chart. If no template is specified, the caption is composed using the values of the Caption and the SubTitle properties. The CaptionTemplate property is declared and implemented as follows:

private ITemplate __captionTemplate = null; [TemplateContainer(typeof(ChartTemplateContainer))] public ITemplate CaptionTemplate { get {return __captionTemplate;} set {__captionTemplate = value;} }

The instantiation of the template takes place in the CreateChildControls overridden method:

private ChartTemplateContainer _captionTemplateContainer; protected override void CreateChildControls() { if (CaptionTemplate != null) { captionTemplateContainer = new ChartTemplateContainer(this); CaptionTemplate.InstantiateIn(_captionTemplateContainer); Controls.Add(_captionTemplateContainer); } }

The method creates a new instance of the container and instantiates the template within. Next, the container is added to the Controls collection of the BarChart control. When the control is ready for rendering, the newly created instance of the template container class is linked to its physical container—the table cell that is located just above the bars:

if (CaptionTemplate != null) captionCell.Controls.Add(_captionTemplateContainer);

The template container is a wrapper control class used to decouple the root templated control—the BarChart in this case—from the classes behind template properties. Instead of instantiating the template directly within the root control, you use this intermediate class—the template container. In doing so, you gain the ability to repeat the template multiple times with different data.

The following is the source of the ChartTemplateContainer class that I'm using as a template for the CaptionTemplate property:

public class ChartTemplateContainer : WebControl, INamingContainer { private BarChart __parent; public ChartTemplateContainer(BarChart parent) { _parent = parent; } public BarChart BarChart { get {return __parent;} } }

You should derive the template container class from WebControl and make it implement the INamingContainer interface. (Note that being a naming container is necessary only if you're hosting multiple templates or controls of the same type.) The structure of the class is completely up to you—the one depicted earlier is reasonable. The constructor takes a reference to the parent control—the BarChart in this case—and another public property exposes that object as a whole or simply mirrors some of its properties. Note that only the public properties of the container class can be called from the data-bound expressions you may use within the template:

<CaptionTemplate> <table width="100%"><tr><td> <%# "<b>" + Container.BarChart.Caption + "</b>&nbsp;" + Container.BarChart.SubTitle %> </td><td align="right"> <%# "Scale Factor <b>" + Container.BarChart.Maximum + "</b>"%> </td></tr><table> </CaptionTemplate>

The first cell displays the contents of the Caption and the SubTitle properties; the second cell is right-aligned and indicates the maximum displayable value—the Maximum property. You use the Container property of the template to obtain an instance of the ChartTemplateContainer class. Any public properties exposed by the class can be used in the data-bound expressions.

The output of the BarChart control may not be cool and attractive like a pie chart or a 3D chart, but it only requires HTML code, doesn't consume server resources to generate a picture and, more importantly, doesn't require a second round-trip to download the image. Try it out!

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is an instructor and consultant based in Rome, Italy. He is the author of Building Web Solutions with ASP.NET and ADO.NET and Applied XML Programming for .NET and is currently writing Programming ASP.NET—all from Microsoft Press. Get in touch with Dino at dinoe@wintellect.com.