Appreciation is a wonderful thing: It makes what is excellent in others belong to us as well.
-Voltaire
If none of the existing Web server controls meet your requirements, you can create your own control by deriving from one of the control base classes—Control and WebControl. Even in this case, though, you don't have to build your control completely from scratch. The Control and WebControl classes provide all the basic functionality of a Web server control and let you focus mostly on the specific features you need to implement.
In its simplest form, a Web server control is a software component that abstracts a piece of HTML markup and exposes it through an object model. You can write controls using any .NET-compliant language and accessing the entire .NET Framework; however, a Web server control still works within the boundaries of an ASP.NET page and, as such, must output HTML markup. A Web server control encapsulates native or composite functionality that you can obtain through HTML and script code. If the functionality you have in mind cannot be obtained with a combination of HTML and client script code, we seriously doubt you'll ever make it work over the Web. An ASP.NET custom control is a class with public methods, properties, and events that renders through an HTML text writer. The final output of an ASP.NET Web server control is HTML text. Keep this in mind as you make your way through this chapter. In general, though, ASP.NET controls can output to other markup languages. For example, ASP.NET mobile controls generate WML.
In this chapter, we'll first demonstrate how to build controls from scratch that implement a simple object model and render it to HTML. Then we'll examine composite controls—a sort of middle way between derived and new controls.
Several programming aspects support the development of a custom control in ASP.NET. First, there are the two base classes, Control and WebControl. Each class provides a common set of base properties that address and fit into a particular use case. In addition to base classes, interfaces help you to better characterize the behavior and programming model of the control. Three interfaces are worth mentioning. They are INamingContainer, IPostBackDataHandler, and IPostBackEventHandler. We've already encountered them repeatedly throughout the book. In this chapter, we'll see them in action from the perspective of a control's developer. Finally, styles and the HTML writer are fundamental components to define and enhance the look and feel of the control and render it to the output stream. We'll review them too.
The Control 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 regarding control styles which affect rendering. These properties include ForeColor, BackColor, Font, Height, and Width. WebControl, in particular, is the base class for the family of Web server controls in ASP.NET.
In Chapter 3, "ASP.NET Core Server Controls," we provided a detailed description of the properties and methods of the WebControl and Control classes and discussed how HTML and Web controls inherit from them. In this chapter, we'll focus on understanding which class you should consider when developing a brand new ASP.NET control.
When developing a new ASP.NET control, there's just one guideline to follow. If your control renders a user interface (UI), you should derive it from WebControl. If you're authoring a component that doesn't provide specific user-interface features, you're better off using Control as your base class. Although these rules are effective in most cases, there might be exceptional situations in which you would reasonably do otherwise. For example, you can derive from Control also if you want to provide a subset of the UI features or when you are combining multiple controls. When using these derived controls—known as composite controls—typical user-interface properties such as BackColor should be duplicated to let users address the background of individual controls. In similar situations, you might want to get rid of the typical programming interface and build your own completely from scratch. Note that you should never use UserControl as a base class for a custom control.
Depending on the functionality of your control, you might have to implement additional interfaces. Typically, a server control will implement some of the following three interfaces.
The HtmlTextWriter is a helper class that turns out to be quite useful in the development of custom controls. You seldom use the writer if you derive controls from existing ones. However, when you build the control from the ground up, you inevitably need to write the HTML output to the stream.
The HtmlTextWriter class writes a sequential series of HTML-specific characters and text on 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 (discussed in Chapter 16, "Working with the File System"), you'll find nothing new about the programming model of HTML writers. The class provides a bunch of methods that you use as ad hoc tools to handcraft the HTML code. Here's how to write a table using an HtmlTextWriter.
// outputs writer.WriteFullBeginTag("table"); writer.WriteFullBeginTag("tr); // outputs writer.Write("Hello"); writer.WriteEndTag("td"); // outputs
writer.WriteBeginTag("td"); writer.WriteAttribute("valign", "top"); writer.Write(HtmlTextWriter.TagRightChar); // outputs Hello |
writer.WriteEndTag("tr"); writer.WriteEndTag("table");
The WriteFullBeginTag and WriteBeginTag methods perform a similar task and differ in that WriteBeginTag does not write the closing character (>) of the opening tag. In this way, attributes can be appended to the tag by other methods such as WriteAttribute. To close the tag, you write the TagRightChar constant, or the SelfClosingTagEnd constant if the element is a self-closing one (/>).
Writing Attributes
To render the attributes of a Web server control in HTML client code, you typically use the WriteAttribute method of the HtmlTextWriter object. The method takes the name of the attribute and its value and writes both to the output stream, adding a blank and using the common syntax of attributes.
// Generates border="1" output.WriteAttribute(border, "1");
The value of the attribute must be a string, and the method takes care of quotes too.
An alternative technique you can use to add attributes is based on the combined use of the AddAttribute and RenderBeginTag methods. The AddAttribute method adds the value to the list of attributes for the outermost element. When the RenderBeginTag method is finally called, any attributes in the internal stack are popped out and rendered to the opening tag of the element. The list of attributes is then cleared. Generally, the pattern is to add attributes to the element, call RenderBeginTag, render the contents, and then call the RenderEndTag method. This technique is used internally in many standard ASP.NET controls.
Working with Styles
A bit more specific than AddAttribute is the AddStyleAttribute method. The two methods work in roughly the same way, but AddAttribute can manage any attribute whereas AddStyleAttribute is specific to the Style attribute.
// Generates style="font-name:verdana;font-size:10pt;" output.AddStyleAttribute("font-name", "verdana"); output.AddStyleAttribute("font-size", "10pt");
The most commonly used style properties are grouped by name in the HtmlTextWriterStyle enumeration. By using a particular overload of the AddStyleAttribute method, you can take values from this type to indicate the style properties to render to the element.
Important |
It might seem rather bizarre that while emphasizing the role of components in general and the ASP.NET object model in particular, Microsoft recommends the use of stream writers to let server controls generate their output. Objects are important and fundamental for a number of reasons. However, rendering controls to HTML is one of the common tasks that ASP.NET applications accomplish repeatedly, one request after the next. It goes without saying that such tasks are to be optimized as much as possible. Writing to the response stream—the HTML writer works on top of the response stream—is much faster than creating an object-based representation of the output and then asking the root control to render itself (and the children) to HTML. Using the HTML text writer is a matter of optimizing performance. |
The way in which a style is rendered in ASP.NET varies according to the selected client target—a downlevel or uplevel browser. ASP.NET addresses differences between HTML 4.0 and HTML 3.2 by employing two distinct rendering engines—the HtmlTextWriter and Html32TextWriter classes. In particular, the Html32TextWriter class converts HTML 4.0 style attributes into the equivalent tags and attributes compatible with HTML 3.2 and ensures that attributes, like colors and fonts, are consistently and correctly propagated to older browsers. The version and the capabilities of the underlying browser are detected by looking at the HttpBrowserCapabilities class.
ASP.NET controls are made of two logical components—an object model and a rendering engine—that can be, in turn, implemented through various layers of code and topped with handlers for the events fired by the container and registered HTTP modules. When building a new control (as opposed to deriving a control), the most relevant difference lies in the fact that you have to render any needed HTML yourself. In a derived control, as we've seen in Chapter 18 ("Extending Existing ASP.NET Controls"), you normally don't need to provide HTML rendering and deal with text writers.
To get a grip on building new ASP.NET controls, let's start by creating a simple control with a limited state and a not-too-complex rendering engine. The control, named GaugeBar, represents a gauge and can be used to implement a rating system that represents the progress made for certain tasks, or in general it could be used to give a friendly user interface to measurable quantities.
A gauge control needs to have at least two properties—one to indicate the value being rendered and one that provides the scale. In addition, we also give users a chance to control the ruler and the descriptive text for the gauge. Table 19-1 lists the properties of a gauge control.
Property |
Description |
---|---|
FormatString |
Formats the string that the control will render alongside the control. The string can contain up to two placeholders. The first placeholder is set with the value; the second placeholder is set with the scale. The default string has the following form: {0}out of {1}. |
Maximum |
Indicates the maximum value the gauge can represent. Set to 100 by default. |
Segments |
Indicates the number of notches to draw on the gauge ruler. Set to 4 by default. |
Value |
Indicates the value to represent. Set to 0 by default, and cannot be higher than the scale. |
The set accessor of the Value properties cuts any value set that exceeds the current Maximum. The value stored in Maximum is the highest value you can assign to Value. The format string should be formed using two parameters in a fixed order—value and maximum. In the format string, you can use any HTML formatting and even reference the parameters in the reverse order. The following code snippet shows possible ways of setting the format string:
GaugeBar1.FormatString = "{0} ({1})"; GaugeBar2.FormatString = "Maximum is {1}. Value is {0}";
The gauge control has no methods and doesn't fire any events.
Implementing the Object Model
Internally, the control renders the gauge using an HTML table. The Value and Maximum pair are translated in percentages, and the ruler is drawn using table cells. Figure 19-1 shows the control within the Microsoft Visual Studio .NET designer.
Figure 19-1: The GaugeBar control in action in the Visual Studio .NET designer.
The notches on the ruler are obtained simply by adding as many cells to the underlying table as there are units in the Segments property. The following listing shows the C# source code of the control:
using System; using System.Web.UI; using System.Web.UI.WebControls; using System.ComponentModel; using System.Drawing; namespace ProAspNet.CS.Ch19 { public class GaugeBar : WebControl { public GaugeBar() : base() { ForeColor = Color.Orange; BackColor = Color.LightYellow; Width = 100; Height = 15; Maximum = 100; Value = 0; Segments = 4; FormatString = "{0} out of {1}"; } public float Value { get {return (float) ViewState["Value"];} set { float f = (value > Maximum ? Maximum :value); ViewState["Value"] = f; } } public float Maximum { get {return (float) ViewState["Maximum"];} set {ViewState["Maximum"] = value;} } public int Segments { get {return (int) ViewState["Segments"];} set {ViewState["Segments"] = (value <1 ? 1 :value);} } public string FormatString { get {return ViewState["FormatString"].ToString();} set {ViewState["FormatString"] = value;} } protected override void Render(HtmlTextWriter output) { // Calculate the value to represent float valueToRepresent = 100f*Value/Maximum; // Begin the rendering of the table output.WriteBeginTag("table"); output.WriteAttribute("bgcolor", "gray"); output.WriteAttribute("border", "1"); output.WriteAttribute("cellspacing", "0"); output.WriteAttribute("cellpadding", "0"); output.WriteAttribute("width", Width.ToString()); output.WriteAttribute("height", Height.ToString()); output.Write(HtmlTextWriter.TagRightChar); output.WriteFullBeginTag("tr"); // Draw segments (cells of the table) DrawRuler(output, valueToRepresent); // Draw the text below (use another table) output.WriteEndTag("tr"); output.WriteEndTag("table"); output.AddStyleAttribute("font-family", "verdana"); output.AddStyleAttribute("font-size", "8pt"); output.RenderBeginTag("span"); output.Write(FormatString, Value, Maximum); output.RenderEndTag(); } } }
Note |
In the previous code, we set default values for the various properties in the constructor. However, all standard ASP.NET controls follow a different but equivalent design pattern. Each property is assigned the default value in the get accessor. Consider this when designing your own controls. |
The control maintains some state by using the view state collection. All the properties, in fact, are persisted using ViewState. Because all the persistent properties are marked as public, you can disable the view state altogether and still keep the control fully functional by explicitly setting properties upon page loading.
Caution |
A page can disable the view state for all embedded controls or for individual controls. Note, though, that disabling the view state for a control results in a loss of functionality if the control stores in the view state private or protected properties. Unlike public properties, in fact, private or protected properties cannot be programmatically set from within a host page. |
As you can see, the GaugeBar control features a few properties that form its object model and override the protected member Render. As the name and the implementation suggest, the Render method generates the HTML output of the control. The control is actually rendered as a table built by outputting raw text to the system-provided HTML text writer.
Setting Up the Ruler
The ruler divides the area of the control in segments, which are filled proportionally based on the current value of the gauge. Each segment of the ruler corresponds to a cell in the underlying table. If no ruler is used, rendering the control consists of creating a single row table with two cells. The first cell is given a different color and a width expressed as a percentage. The second cell has the default background color and a width that denotes how much is still left to do to reach the maximum. The following HTML code snippet shows the code needed to render a value of 80 out of 100:
When more than one segment is used, each segment renders its portion of the bar and displays the value (or its portion of the value) in a cell. The cell is totally filled if the gauge value is greater than the portion the segment represents. For example, suppose that we use four segments to represent a total value of 100. In this case, the logical width of each segment is 25. If the gauge value is, say, 60, the first two segments are filled up, the third segment is divided in two, and the last one is empty, as shown in the following code:
|
Figure 19-2 shows how this gauge will look (four segments displaying a value of 60 out of 100). Figure 19-2 also shows gauges with different ruler settings.
Figure 19-2: The effect of different settings on the gauge ruler.
Setting Up the Control's Site
As you might have guessed already from the preceding figures, other properties get into the game aside from those discussed in Table 19-1. Admittedly, the grayscale rendering used in this book doesn't do justice to the actual capabilities of the GaugeBar control in terms of color support. However, the control exploits a few color-related properties defined on the base class. These properties are BackColor, ForeColor, Width, and Height.
Width and Height are used to delimit the control's site—that is, the area within the container the control is assigned for rendering. The control is assigned a default size that can be changed either programmatically or through the Visual Studio .NET Properties window.
The value of the ForeColor property is used to render the text of the label that accompanies the gauge. The value of the BackColor property determines the color to be used for the progress bar. Note that the implementation we just discussed assumes that only known colors can be used. In other words, if you set any color properties to a custom value such as #eeeeee, the final result you get is radically different than what you'd expect. We'll dig into this topic in detail in Chapter 21, "Design-Time Support for Custom Controls," while covering design-time features of custom controls.
The user interface of a Web control is pure HTML, sometimes topped off with a bit of client script. There are basically two ways in which this HTML can be generated. You can compose the HTML code in the supplied writer, or you can build an in-memory representation of the output using existing HTML and Web server controls and then have them recursively render their contents to the writer. Let's discuss these two options in more detail.
Generating the HTML for a Custom Control
Earlier in this chapter, we discussed the capabilities of the HTML text writer object and how it simplifies the creation of complex HTML structures. However, the downside of this approach you should consider is that you end up making several calls to the writer. This has repercussions in terms of both the performance and the size of the code. Let's consider a quick but significant example.
To write the content of a string in a table cell, you would need the following code if you decide to opt for the rich interface of the writer:
output.WriteFullBeginTag("table"); output.WriteFullBeginTag("tr"); output.WriteFullBeginTag("td"); output.Write(text); output.WriteEndTag("td"); output.WriteEndTag("tr"); output.WriteEndTag("table");
However, as long as you don't have a full bag of attributes to render, or a really complex structure to build, the following code is equally effective and even slightly faster:
output.Write("
"); output.Write(text); output.Write(" |
");
In general, neither of these two approaches is always the best possible approach. A good compromise between the two is recommended to optimize performance while producing compact code. Taking the first approach to the limit, you end up with tons of lines of code. Taking the second approach further, you resort to building the control using strings, which is indeed not the best thing you can do.
In ASP.NET, every piece of HTML code can be managed on the server as an instance of a class. This pattern results in extreme flexibility and ease of development. However, it doesn't come free of problems either. The rub lies in the fact that you instantiate lots of controls, which always impact performance.
Tip |
In general, in ASP.NET using controls is the first option; you shouldn't use them though in situations in which the control is just overkill. If you need to output constant text, there's no reasonable justification for using a Label control instead of a simpler Response.Write. |
Using Child Controls For Rendering
Sometimes the custom control needs to build up a complex infrastructure with nested tables and elements. In this case, it makes sense to build an in-memory representation of the overall tree and then render everything to HTML using the RenderControl method of individual controls. We'll demonstrate this technique in detail later in the "Rendering the Control" section while discussing a more complex bar chart example.
To obtain and, optionally, manipulate the HTML text for a particular control, you can proceed as illustrated in the following code snippet:
StringWriter writer = new StringWriter(); HtmlTextWriter output = new HtmlTextWriter(writer); GaugeBar1.RenderControl(output); display.Text = writer.ToString();
The RenderControl method of Web controls needs an HTML text writer to generate the output. A writer, though, is only the topmost layer of a pile of objects at the bottom of which there's always a stream. In the simplest case, the HTML writer just lies on the top of the response output stream. In other cases, you can use intermediate writers such as a string writer. In the code just shown, you first create a string writer object and then create an instance of the HtmlTextWriter class that writes to the parent writer, which in turn, outputs to the backing store of the StringWriter class—a memory stream. The net effect of the code is that RenderControl just writes its output to a memory string. The string is then accessible using the ToString method.
Tip |
The trick just described greatly simplifies the development of custom controls because it gives you a quick way to snoop into the actual HTML being generated for your control. (Sometimes the resulting HTML is not obvious if you generate rather entangled markup.) Alternatively, you can use the View Source menu of the browser to see the whole generated HTML. But the trick just described shows you only a small portion of HTML—usually just what really matters. |
Once compiled, the gauge control can be installed in the Visual Studio .NET toolbox and dragged and dropped onto any Web Forms page you're developing. The control is automatically registered and provides a preview of its final output.
The properties of the control that feature simple types can be set using the Properties window; for complex types, such as classes, you need to write a type converter and configure the property for the design-time environment of Visual Studio .NET. (We'll discuss this in Chapter 21.)
The following code shows how to set properties on the gauge control programmatically. You should try to set the Maximum property first because, in this way, the control automatically validates the value. However, by also adding some validation code to the set accessor of the Maximum property, you can perform a cross-check of the two values and always stay on the safe side.
private void Button1_Click(object sender, System.EventArgs e) { GaugeBar1.Maximum = Convert.ToSingle(TheMax.Text); GaugeBar1.Value = Convert.ToSingle(TheVal.Text); }
Maximum and Value are stored in the view state and are automatically restored when the page posts back. If the host page disables the view state, you should modify the code that relates to the control so that the needed properties are set on each request. Figure 19-3 shows a GaugeBar control in action.
Figure 19-3: A page that makes use of the GaugeBar control.
Several third-party controls provide highly specialized charting components for use with Web pages. In addition, the new features built into 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, a lightweight but equally effective alternative exists—use HTML tables. As long as you content yourself with a plain two-dimensional bar chart, a pure HTML solution is possible and, to the extent possible, even snazzy and elegant. Let's discover how to build such a solution.
Our 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 in such a way that the resultant output looks like a vertical gauge. The control, named BarChart, features a few properties and one method—Add—that you'll use to bind data to the control.
Table 19-2 shows the properties exposed by the control.
Name |
Description |
---|---|
BackImageUrl |
Indicates the URL of the image to use to tile the background of the control. |
BarCount |
Read-only property, gets the number of bars that form the chart. |
Caption |
The string that represents the title of the chart. |
ChartColor |
The default color used to fill the bars in the chart. |
Maximum |
The maximum value the chart can represent. Set to 100 by default. |
SubTitle |
The subtitle of the caption. Displayed below the caption with a smaller font. |
The Maximum property is nearly identical to that defined on the GaugeBar control. It gets and sets the maximum value the control can represent. In addition to the properties listed here, the control will use ForeColor and BackColor as well as Width, Height, and Font.
Binding Data
To draw a chart, in whatever form or shape, you need data. A chart control lends itself very well to be data-bound. However, in the implementation discussed here, we will not use a data-bound control—at least not in the traditional sense of the word. You won't find a public DataSource property accompanied with ancillary properties such DataMember, DataTextField, and DataValueField. We'll discuss data-bound controls in Chapter 20, "Data-Bound and Templated Controls." For now, let's limit ourselves to passing data through a method, named Add, that acts as a data accumulator.
public void Add(string theLabel, float theValue);
The Add method has three overloads and allows you to define information about a single bar. The minimum amount of information you must specify includes the text to draw below the bar and the value to represent. Additional parameters are the bar's ToolTip and color. The data you specify is gathered in a custom data structure and copied into an ArrayList.
The ArrayList is the internal data repository used by the BarChart control and is cached in the control's view state. Because of this behavior, you don't need to rebind the control to its data any time the page posts back. Keeping the data source out of the view state is the usual behavior for data-bound controls. The BarChart control, at least in this implementation, is built in a different way. The difference shouldn't affect performance too much, as the quantity of data you cache in the view state is limited to very few columns.
The Bar Chart Item
Each bar of the chart is logically represented by a custom data structure known as BarChartItem. A new instance of this class is created whenever you call any overload of the Add method.
[Serializable] protected class BarChartItem { public string Text; public float Value; public string ToolTip; public Color BackColor; }
Note that the class must be marked as serializable if you want to cache instances of it in the ASP.NET control's view state. The following code snippet shows what happens when a new bar chart item is added to the control:
public void Add(string theLabel, float theValue, string toolTip, Color barColor) { // Initialize the bar chart object BarChartItem bci = new BarChartItem(); bci.Text = theLabel; bci.Value = theValue; bci.ToolTip = toolTip; bci.BackColor = barColor; // Copy data to the view state for persistence if (_barChartData != null) _barChartData.Add(bci); DataSource = _barChartData; }
The source code of the BarChart control is as follows:
using System; using System.Web.UI; using System.Web.UI.WebControls; using System.ComponentModel; using System.Drawing; using System.Collections; namespace ProAspNet.CS.Ch19 { public class BarChart : WebControl { public BarChart() : base() { _barChartData = new ArrayList(); DataSource = _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 ViewState["BackImageUrl"].ToString();} set {ViewState["BackImageUrl"] = value;} } public int BarCount { get { if (DataSource != null) return DataSource.Count; return 0; } } public Color ChartColor { get {return (Color) ViewState["ChartColor"];} set {ViewState["ChartColor"] = value;} } public string Caption { get {return (ViewState["Caption"].ToString();} set {ViewState["Caption"] = value;} } public string SubTitle { get {return ViewState["SubTitle"].ToString();} set {ViewState["SubTitle"] = value;} } public float Maximum { get {return Convert.ToSingle(ViewState["Maximum"]);} set {ViewState["Maximum"] = value;} } private ArrayList _barChartData; public void Add(string theLabel, float theValue) { Add(theLabel, theValue, "", ChartColor); } public void Add(string theLabel, float theValue, string toolTip) { Add(theLabel, theValue, toolTip, ChartColor); } public void Add(string theLabel, float theValue, string toolTip, Color barColor) { BarChartItem bci = new BarChartItem(); bci.Text = theLabel; bci.Value = theValue; bci.ToolTip = toolTip; bci.BackColor = barColor; // Copy data if (_barChartData != null) _barChartData.Add(bci); DataSource = _barChartData; } protected virtual ArrayList DataSource { get {return (ArrayList) ViewState["DataSource"];} set {ViewState["DataSource"] = value;} } } }
The preceding listing doesn't include the rendering code, which we'll discuss in a moment. The DataSource property is marked as protected and used to cache in the view state an ArrayList built with the data passed by the client application. In particular, the DataSource property is a wrapper for an entry in the ViewState collection. All the client data is temporarily parked in an ArrayList and then added to the view state. Note that the ArrayList must be explicitly associated with the DataSource property after each insertion or deletion. Simply obtaining a reference and then adding data to DataSource just won't make data persistent across postbacks.
The View State from the Control's Perspective
ViewState, which is a collection of name/value pairs, is serialized to a string at the end of the page pipeline. The final string, properly hashed and encoded as Base64, is sent to the client as a hidden variable. As we've seen in Chapter 14, "ASP.NET State Management," the view state of the page is the summation of the view state collections of all constituent controls. Each control governs the serialization and deserialization process for its own data.
Associating the HTML source of the page with the view state is a univocal way of binding together the page and its state. Other techniques can be employed, such as storing the view state on the server in disk files, databases, or memory, but each has pros and cons. As a matter of fact, the overall view state can grow up to several KB of data that is completely useless on the client but terribly helpful once the page posts back. It should be made clear, though, that the usefulness of the view state is primarily for the developer; there's no added value for users in the view state. But without view state, managing state would be significantly more complex. Well, just as complex as in old ASP.
Upon postback, the ASP.NET runtime reads and parses the content of the view state and gives any controls a chance to repopulate their ViewState bag. Thanks to this mechanism, controls that use ViewState to store property data (instead of a private member) have their state automatically persisted across round-trips. The following code fragment shows a property that is saved in ViewState:
public string Caption { get {return Convert.ToString(ViewState["Caption"]);} set {ViewState["Caption"] = value;} }
Note that properties not persisted in the view state should be initialized and set to a default value in the class constructor, the get accessor, or in the handler for the page's Init event.
Note |
As a general guideline, you should persist in the view state properties that cannot be easily restored or recalculated. Properties whose overall size can measure a few KBs (for example, the data source) should be kept off the view state too. Finally, bear in mind that if you persist internal properties (protected or private members) that would make your control strictly dependent on the value of the control's EnableViewState property, your control won't work as expected if the view state is disabled. |
To be persisted in the view state, a type must either be serializable or have a TypeConverter object defined. Of the two possibilities, having a type converter is the most efficient from the view state perspective. You should look at type converters as sort of lightweight and highly specialized type serializers. Storing in the view state a serializable type that doesn't provide a converter results in slower code and generates a much larger output. Type converters are also useful to give a design-time behavior to controls; we'll look at type converters in Chapter 21.
The view state is serialized using a custom ASP.NET object serialization format. It's optimized for a few common types, including primitive types, strings, arrays, and HashTable types. As a result, either you write a type converter or use those simple types.
Tip |
A control can customize how property data is stored in the ViewState. To accomplish this, the control must override a couple of methods on the Control class. The methods are SaveViewState and LoadViewState and have the following signatures: protected virtual object SaveViewState(); protected virtual void LoadViewState(object savedState); |
The HTML structure of the BarChart control is a bit complex and is composed of a few nested tables. The outline is depicted in Figure 19-4.
Figure 19-4: The outline of the BarChart control.
The outermost table counts three rows—title, subtitle, and chart table. The chart table comprises three rows—the chart, the separator, and the labels. Each bar in the chart table is rendered as a stand-alone table with three cells. The bottom-most cell represents the value of the bar; the middle cell is the value; the topmost cell is empty to denote what is left.
The BarChart control renders its output by building a graph of Table objects and rendering the output to HTML using the RenderControl method. As mentioned earlier, this is not necessarily the most efficient way in terms of raw performance but results in more manageable code. We're using this approach mostly for completeness, as we think that in real-world cases, and especially for controls, performance is a critical factor.
The Title Area
The following code demonstrates the main stream of the Render method that the control overrides to produce its output. Note that in building the main and outermost table we mirror in the Table object many of the base properties of the WebControl, including colors, borders, and font.
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 (DataSource != 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 title row uses a larger font and bold typeface, while the subtitle resorts to a smaller font. Note that FontUnit.Larger and FontUnit.Smaller return a font size one unit larger or smaller than the base font size, which is hard-coded to Verdana 8 but can be changed through the Font property.
Building the Chart
An internal method actually generates the chart—the CreateBarChart protected method. Also the child table mirrors many of the base properties of the Web control. The WebControl class just defines properties as a common interface; the developer, though, is responsible for mapping the values of those properties to the constituent elements of the control you're building. In particular, the BackColor property of the BarChart control is mapped to the BackColor property of the table being used to draw the chart. The same occurs with the font, width, height, and CSS style class.
For each element in the internal DataSource array, the CreateBarChart method generates a vertical table with one row and three cells. The height of the cells is determined by looking at the value and the scale to represent.
protected virtual void CreateBarChart(TableCell parent) { // Create the child table Table chart = new Table(); chart.ForeColor = ForeColor; chart.CssClass = CssClass; chart.GridLines = GridLines.None; chart.BackColor = BackColor; chart.Font.Name = Font.Name; chart.Font.Size = Font.Size; chart.CellPadding = 2; chart.CellSpacing = 0; chart.BackImageUrl = BackImageUrl; chart.Style["background-position"] = "bottom"; chart.Width = Width; chart.Height = Height; // Create the chart row TableRow rowChart = new TableRow(); rowChart.VerticalAlign = VerticalAlign.Bottom; foreach(BarChartItem bci in DataSource) { TableCell barCell = new TableCell(); CreateSingleChart(barCell, bci); rowChart.Cells.Add(barCell); } if (DataSource.Count == 0) rowChart.Cells.Add(new TableCell()); chart.Rows.Add(rowChart); // Create the separator row TableRow rowSeparator = new TableRow(); rowSeparator.VerticalAlign = VerticalAlign.Bottom; rowSeparator.Height = 2; TableCell separatorCell = new TableCell(); separatorCell.BackColor = Color.Black; separatorCell.ColumnSpan = BarCount; rowSeparator.Cells.Add(separatorCell); chart.Rows.Add(rowSeparator); // Create the footer row TableRow rowFooter = new TableRow(); rowFooter.Height = 10; rowFooter.VerticalAlign = VerticalAlign.Bottom; rowFooter.BackColor = Color.LightYellow; // Draw the label foreach(BarChartItem bci in DataSource) { TableCell footerCell = new TableCell(); footerCell.HorizontalAlign = HorizontalAlign.Center; footerCell.Text = bci.Text; rowFooter.Cells.Add(footerCell); } if (DataSource.Count == 0) rowFooter.Cells.Add(new TableCell()); chart.Rows.Add(rowFooter); // Connect the chart to the parent parent.Controls.Add(chart); }
The values inherent in the individual bar are packed in the BarChartItem structure and are used to draw and configure the chart table. The following listing shows how the single bar is built. The ratio between the value and the Maximum is protracted in a 0 to 100 scale so that it can be conveniently rendered with percentages.
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); }
When finished, the new table is connected to the parent table's cell that will actually contain it. Figure 19-5 shows how the control looks at the end of game.
Figure 19-5: The various tables that form the BarChart control work together to generate an effective bar chart.
The CreateSingleChart method is marked as protected and overridable to allow potential derived classes to modify the way in which the control is rendered. Declaring protected and virtual methods is a good technique to allow customization from derived classes. Alternatively, you can consider firing ad hoc events in much the same way as the DataGrid, and other iterative controls, do. For example, you could fire a ChartTableCreated event when the chart table is created and BarChartItemCreated when the individual bar has been created.
public event BarChartEventHandler ChartTableCreated; public event BarChartEventHandler BarChartItemCreated;
Both events will pass references to tables and rows allowing users to achieve more specific customization, such as specifying more attractive styles or deciding colors according to run-time conditions.
The Background of the Table
Because the BarChart control is made of HTML tables, and because HTML tables can contain a background image, we can transitively make our control support the same feature. The BackImageUrl property serves this purpose. In addition to improving the graphical quality of the control, the background image represents a powerful way to add a ruler to the chart.
The background image of a table is usually tiled to cover the entire area, but can be controlled in the number and direction of the repetition. As Figure 19-6 proves, a small and simple GIF can be iterated to cover the background of the table emulating a ruler.
Figure 19-6: A BarChart control with a background image that provides a sort of ruler.
The image can have a transparent background, which will be reflected. By default, the control draws the background image from the bottom.
To use the BarChart control, it must be bound to some data. Before Chapter 21 (in which we'll review techniques to enable this feature from the Properties window), let's assume data binding must be performed from code.
private void Chart_Click(object sender, System.EventArgs e) { // // Other properties set through the Properties window // BarChart1.Add("Rome", 3.5f, "Italy"); BarChart1.Add("New York", 12, "USA"); BarChart1.Add("London", 7, "UK"); BarChart1.Add("Mexico City", 16, "Mexico"); BarChart1.Add("Amsterdam", 0.7f, "The Netherlands"); BarChart1.Maximum = 20; }
The Add method has three overloads to indicate the label, value, ToolTip, and background color of each bar. In addition, the BarChartItemCreated event gives you a hook to intervene and modify the chart dynamically. As mentioned, the BarChartItemCreated event fires whenever the individual bar chart is created. For example, the following code shows how to render in red only bars whose value is greater than 10:
void BarChart1_ChartTableCreated(object sender, BarChartEventArgs e) { // Renders a blue-to-white gradient using DHTML (IE50+) string style = ""; style += "progid:DXImageTransform.Microsoft.Gradient("; style += "startColorstr='deepskyblue',endColorstr='white');" // ChartTableCreated event if (!e.IsBarChartCreated) e.ChartTable.Style["filter"] = style; else // BarChartItemCreated event { if (e.Item.Value > 10) e.BarChart.BackColor = Color.Red; } }
This code is used to handle both the ChartTableCreated and BarChartItemCreated events. It intercepts the ChartTableCreated event and renders the background of the main table with a gradient. Both events require a custom delegate—the BarChartEventHandler type. The event takes a custom event data structure—the BarChartEventArgs class—described as follows:
public delegate void BarChartEventHandler(object sender, BarChartEventArgs e); public class BarChartEventArgs : EventArgs { // Whether the bar or the table is being created public bool IsBarChartCreated; // The outermost table public Table ChartTable; // The bar being created public TableRow BarChart; // The data structure with item information public BarChartItem Item; }
The same data structure is used for the ChartTableCreated and the BarChartItemCreated events. The IsBarChartCreated property indicates whether a bar chart or the underlying table is being created. The properties that do not apply to whichever event is fired are simply set to null. As usual, you can define event handlers either programmatically or declaratively by using the OnBarChartItemCreated and OnChartTableCreated attributes. The following code demonstrates the programmatic event binding:
BarChart1.BarChartItemCreated += new BarChartEventHandler(BarChart1_ChartTableCreated); BarChart1.ChartTableCreated += new BarChartEventHandler(BarChart1_ChartTableCreated);
Figure 19-7 shows the BarChart control in action.
Figure 19-7: A dithered background, ToolTips, and bars of different colors adorn the BarChart control.
As we briefly mentioned in Chapter 18, in addition to creating new and derived controls, you can also author Web server controls by combining existing controls into a new monolithic component. The set of methods and properties of a composite control is frequently, but not necessarily, composed from the methods and properties of constituent controls plus new members. A composite control can fire custom events, and it can also handle and bubble up events raised by child controls.
The .NET Framework provides some facilities for the development of composite controls. This special support comes in the form of the Controls property and the protected overridable member named CreateChildControls. The CreateChildControls method is inherited from Control and is called when server controls have to create child controls after a postback.
protected virtual void CreateChildControls();
Another interesting method is EnsureChildControls. This method first checks the current value of the internal, protected ChildControlsCreated property. If this value is false, the CreateChildControls method is called to create all needed child controls. The ChildControlsCreated property is set to true only once all child controls have been successfully created.
ASP.NET calls EnsureChildControls when it needs to make sure that child controls have been created. In most cases, custom server control developers do not need to override this method. If you do override this method, you should use it in a similar fashion as its default behavior. If you provide different behavior, you risk violating the internal logic of standard controls, which can result in buggy applications.
In addition to creating its child controls, a composite component should also implement the INamingContainer interface so that the ASP.NET runtime can create a new naming scope for it. This ensures that all controls in the composite control have a unique name. This will also ensure that child controls' postback data is handled automatically.
Another important aspect of composite controls is that they normally don't require custom logic for rendering, unless the author has to include literal text or dynamically generated elements to enrich the overall user interface. The decision to optimize the performance of a control by using direct rendering instead of composition works for composite controls too. However, for composite controls, using direct rendering can be problematic if embedded controls are custom components whose logic is not completely clear or documented. For example, imagine you build a custom control by aggregating a third-party control. If the internal logic of the control is not well-documented, chances are that you will experience unexpected control behavior.
Let's have a closer look at composite controls. We'll build a simple component by assembling a label and a text box. Incorporating a text box in a composite control also poses additional questions. Would postback data be handled automatically for all child controls? And what about bubbling up events raised by child controls?
We'll build a LabelTextBox control made of a Label control and a TextBox control. The label displays left of the text box in bold. The text of the label is controlled by the Caption property; the Text property of the new composite control maps to the Text property of the embedded (and externally invisible) TextBox control. The LabelTextBox control derives from Control and implements INamingContainer.
Creating Child Controls
The structure of the control is defined in the CreateChildControls method, as shown in the following listing:
// Can inherit from WebControl too namespace ProAspNet.CS.Ch19 { public class LabelTextBox : Control, INamingContainer { public LabelTextBox() : base() { } // Internal members protected TextBox __theTextBox; protected Label __theLabel; // Gets and sets the caption of the label public string Caption { get {return Convert.ToString(ViewState["Caption"]);} set {ViewState["Caption"] = value;} } // Gets and sets the text of the textbox public string Text { get {return Convert.ToString(ViewState["Text"]);} set {ViewState["Text"] = value;} } // Create the child controls of the control protected override void CreateChildControls() { // First clear the child controls Controls.Clear(); // Add the label __theLabel = new Label(); __theLabel.EnableViewState = false; __theLabel.Text = Caption; __theLabel.Font.Name = "verdana"; __theLabel.Font.Bold = true; __theLabel.Font.Size = FontUnit.Point(8); Controls.Add(__theLabel); // Add a blank literal control for spacing Controls.Add(new LiteralControl(" ")); // Add the textbox __theTextBox = new TextBox(); __theTextBox.EnableViewState = false; __theTextBox.Text = Text; __theTextBox.BorderStyle = BorderStyle.Solid; __theTextBox.BorderWidth = 1; __theTextBox.BorderColor = Color.Black; Controls.Add(__theTextBox); } } }
After creation, the Label control is given a default font and style and is then added to the Controls collection of the parent. The same treatment occurs for the TextBox control, which displays with a solid thin black border. The two controls are separated with a literal control containing a couple of blanks.
The Caption and Text properties cache their values in the control's view state. By default, Label and TextBox also cache some of their properties in their own view state. To avoid useless redundancy of data, we explicitly disable the view state for the embedded controls.
__theLabel.EnableViewState = false; __theTextBox.EnableViewState = false;
You should note that CreateChildControls is invoked by the ASP.NET control infrastructure at the last minute during the prerendering stage in the control's lifecycle. You can easily verify this by adding a trace statement in the body of the method and running the sample page with tracing on. CreateChildControls is invoked from within EnsureChildControls whenever the internal flag—the ChildControlsCreated property—indicates the control is not completely set up. In general, CreateChildControls is not called until absolutely necessary. Figure 19-8 shows the trace output.
Figure 19-8: The CreateChildControls method is called during the prerendering stage.
The Importance of Being a Naming Container
If we omit marking the LabelTextBox control with the INamingContainer interface, the child controls will be given a simple, unqualified control ID or the ID that you could assign programmatically. If the composite control doesn't work as a naming container, the child ID will be identical for all child controls, in case you host multiple LabelTextBox controls within the same page. As a result, you experience a run-time error because ASP.NET requires that control IDs be unique. Using a naming container, you have to ensure only that composite controls have a unique ID. (And to say the least, Visual Studio .NET can guarantee this.)
But there's another, subtler reason to mark a composite control as a naming container. The implementation of the interface guarantees that the postback state restoration for the text box (and other controls that expose IPostBackDataHandler) works automatically and transparently with your code. Figure 19-9 illustrates the hierarchy of controls generated for a page that hosts the LabelTextBox control compiled with (the top box) and without (the bottom box) the INamingContainer interface.
Figure 19-9: The top box shows the control names when the INamingContainer interface is implemented for the LabelTextBox control. The bottom box shows the control names when INamingContainer is not implemented.
During the post-back data-handling step (before the Page_Load event arrives), the ASP.NET runtime attempts to find a match between names in the Request.Form collection and controls in the Controls collection of the page. The ID of the HTML input control is LabelTextBox:_ctl1 or _ctl1, depending on whether you implemented the INamingContainer interface or not. So far so good. The rub, though, is in the search algorithm that the ASP.NET runtime employs to find a control in a page. The algorithm is hard-coded in the FindControl method of the Control class. If you specify a control name with one or more occurrences of the colon symbol (:), the search is based in the Controls collection of the control whose name matches the prefix before the symbol. In practice, if the interface is implemented, the ASP.NET runtime searches for a _ctl1 control within the Controls collection of a control named LabelTextBox1. In this case, the control is successfully located and the Text property of the child text box gets properly updated. If the composite control is not a naming container, then the run time assumes it has to find a _ctl1 control within the outermost naming container—the page itself or any other container control. Because no such control exists, the Text property of the text box is not updated, and any other postback state is ignored. However, in this case you can perform the update yourself by implementing the IPostBackDataHandler interface and reading the client value directly from Request.Form using the _ctl1 ID as the key. The value, in fact, is still there, but ASP.NET can't automatically catch its server-side counterpart.
Handling PostBacks
By storing Text and Caption in the local ViewState collection, we can successfully track the previous text value of the Label and TextBox child controls. By making the control a naming container, we also ensure that postback data is correctly detected and assigned to our child controls. Are we completely set up? Well, not completely.
So far we've ensured only that the Text property of the LabelTextBox control is restored from the view state. In addition, we've prepared things so that ASP.NET updates the Text property of the internal TextBox. The two values, though, are not in sync. We must copy the value of the TextBox in the Text property of the composite control.
The TextBox control fires an appropriate event—TextChanged. The idea, therefore, is handling the event internally, set the TextBox and then bubble the event up to the client of the LabelTextBox control.
// Register an internal handler for TextChanged __theTextBox.TextChanged += new EventHandler(InternalTextChanged);
The InternalTextChanged method looks like the following code snippet:
protected void InternalTextChanged(object sender, EventArgs e) { // Update the Text property with the value of the control Text = __theTextBox.Text; // Bubble up the TextChanged event (optional) if (TextChanged != null) TextChanged(this, e); }
The method first synchronizes the internal text box with the Text property of the composite control and then, optionally, fires the same TextChanged event up to the rest of the hierarchy. For the event to reach the clients of the LabelTextBox controls, we need to publicly declare the event.
public event EventHandler TextChanged;
Notice that we can't use the predefined bubble-up interface because the TextChanged event doesn't link to the default mechanism. To do so, a control must call the method RaiseBubbleEvent after firing the event. The TextBox control just doesn't do this. The following pseudo-code shows how a control can bind one of its events to the default bubbling mechanism.
protected virtual void OnTextChanged(EventArgs e) { if (TextChanged != null) TextChanged(this, e); // Needed to link to the default bubble-up mechanism RaiseBubbleEvent(this, e); }
Using a LabelTextBox control in a client page is in no way different than using any other control. The control can be registered with the Visual Studio .NET toolbox and dragged onto the form being developed.
The following code shows a sample page that makes use of the LabelTextBox control.
private void InitializeComponent() { this.Button1.Click += new System.EventHandler(this.Button1_Click); this.Load += new System.EventHandler(this.Page_Load); this.LabelTextBox1.TextChanged += new System.EventHandler(this.LabelTextBox1_TextChanged); } private void Page_Load(object sender, System.EventArgs e) { LabelTextBox1.Caption = "Name"; LabelTextBox1.Text = "Dino"; } private void LabelTextBox1_TextChanged(object sender, System.EventArgs e) { Response.Write("Text changed to: " + LabelTextBox1.Text); }
The output is shown in Figure 19-10.
Figure 19-10: The LabelTextBox control in action.
Literally translated, an old Italian adage states that if you want a thing done, and you do it yourself, you can do as much as three people can. The motto applies to new ASP.NET controls as we described in this chapter.
The .NET Framework provides a wealth of server controls from which you can likely choose exactly the control you are looking for. If this is not the case, and the control simply doesn't exist, you can create your own control from the ground up and obtain incredibly powerful results. Writing a control from scratch is a matter of defining an appropriate object model and providing an effective rendering algorithm. Aside from these two points, other equally important aspects of control development are containment, naming, and integration with the engine that supplies state management.
In this chapter, we've built three controls and learned how to design a rendering engine by choosing between the pure HTML and the control-based approach. The HTML approach provides speed and sacrifices a bit of readability and ease of code maintenance. The control-based approach applies different priorities and chooses code modularity over raw speed of rendering. To say it all, there would be a third case in which you use existing controls for their rendering but not their functionality. In other words, you use controls internally to generate complex HTML markup but don't employ them as child controls. We covered this aspect of programming earlier in the "Using Child Controls For Rendering" section.
Composite controls are a special case of new controls. They exploit the principle of aggregation versus inheritance that we largely explored in Chapter 18. You create a composite control by assembling multiple, distinct controls and synthesizing a new object model on top of them. The resulting object model can be the summation of all elements or, more simply, can be built by taking what is really needed out of constituent elements. For composite controls, being a naming container is a critical point. The behavior of new components can be kept under strict control while resorting to the methods of a couple of interfaces—IPostBackDataHandler and IPostBackEventHandler. The IPostBackDataHandler interface is more useful if the control you're building behaves like an HTML input field (for example, the tag). The IPostBackEventHandler interface, on the other hand, is the tool that allows ASP.NET controls to fire server-side events to notify clients of state changes.
In the next chapter, we'll take a look at even more complex types of controls—data-bound and templated controls. In particular, we'll explore ways to make the BarChart control we created here data-bindable and extensible through templates.
Part I - Building an ASP.NET Page
Part II - Adding Data in an ASP.NET Site
Part III - ASP.NET Controls
Part IV - ASP.NET Application Essentials
Part V - Custom ASP.NET Controls
Part VI - Advanced Topics
Index