Cutting Edge: Reusability in ASP .NET, Part 2

We were unable to locate this content in de-de.

Here is the same content in en-us.

MSDN Magazine
Reusability in ASP .NET, Part 2
Dino Esposito
Download the code for this article: Cutting0109.exe (41KB)
Browse the code for this article at Code Center: Custom Controls

O
ne of the key improvements in ASP .NET over classic ASP is the introduction of server controls. When you want to use a control from the Microsoft® .NET Framework as a server control, you must mark it with the runat=server attribute to make it visible toâ€"and programmable onâ€"the server.
      Web controls in ASP .NET are abstracted from the set of ordinary HTML tags, with some notable additions. For instance, the framework provides you with the Table control (which is similar to the HTML table tag), but also the derived Calendar and DataGrid controls. Overall, Web controls offer a more consistent and uniform programming interface than the basic HTML controls from which they're derived.
      Most Web controls in the .NET Framework have a Text attribute that makes it easier to work with them. For example, the standard HTML checkbox doesn't have a Text property. To associate text with it, you must wrap the whole thing in a <span> tag.
<span> <input type=checkbox . . .> Text of the checkbox </span>
                                    
However, the corresponding <asp:checkbox> Web control exposes a Text property. When rendered to an HTML stream, this Text property is translated back to the <span> tag.
      While pure HTML controls must expose a programming interface that approximates the features of the corresponding HTML tags, derived Web controls don't have this limitation. As a result, they can have methods, properties, and events defined to mix programming comfort, flexibility, and effectiveness.

A Simple Custom Control

      An ASP .NET custom control is a class that's normally derived either from the Control base class, or from a more specific existing class (like DataGrid). If you're building a control from scratch, you probably want to derive it from the Control class. However, if you want to create a slightly modified version of another control, use an existing control as a base class. This lets you take advantage of object-orientation and inheritance.
      The control in Figure 1 acts like an extended version of the TextBox control. Because it's built atop TextBox, it inherits all the standard features of textboxes plus some CSS style settings. This derived textbox will default to having a thin, flat black border and a beige background; its text will appear in Verdana with an extra-small font size. Figure 2 shows how it looks in a test page.

Figure 2 Flat TextBox
Figure 2 Flat TextBox

      In Figure 1, the control also registers a custom handler for the textbox's OnLoad event. Notice that OnLoad fires after the attributes set in the page have been processed. This means that if you want to show some default text whenever the Text property is empty, you can do so with the following code:
public void OnLoad(Object s, EventArgs e) { if (this.Text == "") this.Text
                                        = "<Enter some text>"; } 
      ASP .NET controls need two types of information to be successfully resolved and processed by the runtime. First, they need to be compiled in a binary module (assembly) available to ASP .NET. Next, a namespace name and prefix are required to uniquely qualify the control. Finally, the name of the assembly (without the .dll extension) that contains the code for the control must be explicitly declared. All this information is gathered by the @ Register directive:
<%@ Register TagPrefix="d" Namespace="BWSLib" Assembly="FlatTextBox" %>
                                    
      When the runtime encounters this directive, it takes note of the TagPrefix. Every tag that's marked with the namespace prefix "d" will be resolved in terms of the content of the specified assembly. You get the assembly by compiling the source code of the control. The C# code in the previous snippet can be compiled like this:
csc /t:library flattextbox.cs 
      When you compile flattextbox.cs, you get a file called flattextbox.dll that should be copied to the Bin directory of your Web server. You can then use code like the following to invoke the control in ASP .NET:
<d:FlatTextBox runat="server" id="myCtl" /> 
      Since the new control is inherited from TextBox, it can handle any property, method, or event that the base class exposes and can be used wherever a TextBox is accepted. For example, you can set the maximum number of acceptable characters using the MaxLength property or make it work in password mode. You can also transform it into a multiline, scrollable text area.
<d:FlatTextBox runat="server" id="myCtl" TextMode="MultiLine" Rows="5"
                                        /> 

Overriding Existing Behaviors

      When you build a new control from an existing one, you can not only add new features but also change existing behaviors and even restrict the functionality the base control provides. For example, you could override the Text property to make it accept strings no longer than MaxLength, automatically cutting off the extra text. The MaxLength property controls the length of the string the user types in, but it has no effect on the programmatic setting of the Text property. The following code shows how to override the Text property to take MaxLength into consideration:
public override String Text { get { return base.Text; } set { if (value.Length
                                        > this.MaxLength) base.Text = value.Substring(1, this.MaxLength); else base.Text
                                        = value; } } 
The override keyword informs the compiler that you're trying to replace the base property. If you remove either the get or set accessor, the visibility of the property changes to write-only or read-only. In the case I just demonstrated, it remains a read/write property with a slightly different behavior when it comes to writing.
      Whenever a new value is assigned to the Text property of the FlatTextBox control, the code specified in the set accessor executes. It compares the length of the new value with MaxLength. Notice that the keyword value is a generic placeholder for the actual value you assign to the property. If the new value is longer than allowed by the MaxLength setting, the Text property will throw out any characters that don't fit.
      When overriding a control's property, you must use the base keyword to prefix the property in the accessor's code. (The keyword base is specific to C#. In Visual Basic, you must use MyBase instead.)
get { return base.Text; } 
This snippet maintains the base control's get behavior, which simply returns the current value of the control's buffer.
      Bear in mind that when you define the get accessor you're actually defining the way in which the control itself will work on that property. So if you write the get code like the following snippet, it will keep calling itself for values until it generates a stack overflow.
get { return Text; } 
This happens because reading the value of any property is resolved in terms of its get accessor. So a statement like "return Text" in the get accessor will call itself recursively until you blow the stack. The same considerations apply to set accessors.
      You may also define a brand new Text property that is managed independently from the Text property of the base class. In this case, though, you must explicitly prefix the Text property with a keyword called new (as opposed to override).

Figure 3 Input Maximum Length = 4
Figure 3 Input Maximum Length = 4

      In light of this, running the following code snippet in an ASP .NET page will generate the page shown in Figure 3, where the string "Enter some text" is cut down to the first four characters (Ente).
<d:FlatTextBox runat="server" id="myCtl" maxlength="4" /> 
      Custom controls in .NET are full-fledged, compiled classes that lend themselves to building hierarchies of custom components. The code in Figure 4 demonstrates how to build another control on top of the FlatTextBox.
      The new ReadonlyFlatTextBox control is a multiline text area with five rows by default. It does not support changes, and in addition it disables the default state maintenance. ViewState is a Control class property that refers to a StateBag collection object for storing information that's maintained across multiple requests of the same page. ASP .NET controls use the ViewState property to append state information to the bag of properties that ASP .NET pages carry from server to browser and back. Since this ReadonlyFlatTextBox control is a sort of label, there's no need to remain so interactive during page requests. Disabling view state management will help the control's performance.
      A textbox becomes multiline as soon as you set the TextMode property with any of the proper values in the TextBoxMode enum. The ReadonlyFlatTextBox control defaults to MultiLine, but also prevents you from changing this setting programmatically. The control overrides the TextBoxMode property and hides the set accessor.
public override TextBoxMode TextMode { get { return base.TextMode; } } 
      So now there's no way for client applications to change the value stored in the TextBoxMode property of this derived control. If you attempt to do that anyway, you get the compiler error shown in Figure 5.

Figure 5 Compiler Error
Figure 5 Compiler Error

As you can see, the error message tells you that the error is not due to a missing property, but to the attempt to use a property that has been declared read-only. Thanks to the power of object-oriented programming, this happens only in the derived class. From the ASP .NET perspective, this is a way to build specialized components that can be programmed as native elements of the .NET Framework.

A Labeled TextBox

      When you use a textbox in a Web application, you usually associate it with a descriptive label. Is there a way to automate this process, saving yourself the burden of repeatedly declaring an <asp:label> control each time? Yes, of course.
      Figure 6 shows the code necessary to build the LabeledTextBox class. It derives from TextBox and defines a few extra properties such as Label, LabelCssClass, and LabelOnTop. Label contains the text to be used for the description. LabelCssClass is a string that contains the name of the CSS class to associate with the label. LabelOnTop is a Boolean value that, when set to true, causes the control to draw the label atop the textbox instead of to the left of it.
      When writing custom controls, you should pay attention to the CreateChildControls method. You might want to override it whenever your control contains children. In this case, you override this method, build instances of children, and add them to the Controls collection of the parent class. You can append it to the bottom of the collection through the Add method. Alternatively, you can insert the new child control at any valid position using AddAt.
      The following code shows an alternative way to build a control that combines one label and one textbox.
protected override void CreateChildControls() { m_textBox = new TextBox();
                                        m_textBox.Text = m_Text; m_label = new Label(); m_label.Text = m_Label + "&nbsp;&nbsp;";
                                        this.Controls.Add(m_label); this.Controls.Add(m_textBox); } 
      All in all, I would create this control by starting from Control and adding both a label and a textbox to the Controls collection of the base class. Since the label and the textbox are at the same logical level, using a third object as the parent is probably the best available design approach.
      Once you have overridden CreateChildControls, you don't have to worry too much about the rendering of the control. CreateChildControls fills up the control's Controls collection. By default, the Render method just traverses this collection and asks each control to draw itself. Render is the method responsible for generating HTML code. The code in Figure 6 offers an alternative approach. The control derives from TextBox, overrides the Render method, and makes itself responsible for generating the output of the various child controls.
      Render takes an HtmlTextWriter objectâ€"basically a streamâ€"and dumps plain HTML into it.
protected override void Render(HtmlTextWriter output) 
You can call base.Render to force the base class to generate its output. By contrast, RenderControl is the method to call to ask a control to render itself recursively.
m_label = new Label(); m_label.Text = m_labelText; m_label.RenderControl(output);
                                    
      When generating the HTML code, you would normally take advantage of the various properties that the control features. In this case the LabelOnTop property decides about the position of the label with respect to the textbox. If LabelOnTop is set to true, the label goes above the textbox; otherwise the label and textbox will be rendered side by side. The final physical structure of the LabeledTextBox control can be divined by examining this code:
m_label.RenderControl(output); if (m_LabelOnTop) output.WriteLine("<br>");
                                        else output.WriteLine("&nbsp;&nbsp;"); base.Render(output); 
Figure 8 LabeledTextBoxes in Action
Figure 8 LabeledTextBoxes in Action

The code renders the label first, followed by a literal control that acts as the separator, and finally the textbox. Figure 7 shows the source code of an ASP .NET page that uses the control. Figure 8 shows the output of the page. Figure 9 shows the generated HTML code for the two LabeledTextBox controls in Figure 7.

Figure 9 HTML Generated for LabeledTextBoxes
Figure 9 HTML Generated for LabeledTextBoxes

Maintaining State in Controls

      Web pages, including ASP .NET pages, get invoked multiple times in the same session. You cannot save any control information from call to call because the control's lifetime is tied to the page. When the page is dismissed on the server and sent as HTML to the browser, all the controls that were instantiated on the page are marked as ready for the garbage collector.
      When the server loads an ASP .NET page, all the controls it contains go through a sequential process entailing several steps. Although the communication between client and server is stateless, the user's experience must be seamless. ASP .NET, in conjunction with the page and the controls, manages to provide this apparent continuity.
      ASP .NET controls have a ViewState property for storing information that must be persisted across multiple page invocations. ViewState plays a key role in allowing controls to start working upon postback events in the same state they were in at the end of the previous request.

Figure 10 Control's Lifecycle
Figure 10 Control's Lifecycle

      In Figure 10 you can see the various steps that form the control's lifecycle once the host page has been requested again. First, the control is initialized and its previous state is restored with a call to its LoadViewState method. LoadViewState populates the control's ViewState collection with all the control-specific information that was saved during the previous invocation. A control can save information for type and size, and the process for restoring state can be customized by overriding the LoadViewState method. At this stage, ViewState holds an exact copy of the data that the control stored at the end of the previous request. This information comes across as form data and basically is the same data you could access through the Request.Form collection. This information includes the state of the client-side HTML elements like the status of checkboxes and dropdown lists and the contents of one textbox.
      Some of the data, of course, may have changed during client-side user interaction. The posted information is merged with the freshly restored ViewState data and only at this point can the state of the control be considered consistent and ready to use.
      As the next step, the control's OnLoad event is fired, giving it a chance to apply additional changes to its state, distinguishing between the first and successive accesses. Any change detected between the current and previous postbacks will generate a RaisePostBackDataEvent event for the control. After that, the runtime executes the server-side code associated with the client-side event and prepares the rendering of the control.
      Controls can persist custom data by storing it in ViewState. ViewState is an instance of the StateBag class, which is basically a collection. To store something in ViewState, do the following:
ViewState["MyData"] = . . . 
      A control can make some of its properties automatically persistent by using a ViewState entry as the storage medium for one property. Instead of a local data member, the following code uses ViewState in the get and set accessors.
public String Text { get { return (String) ViewState["Text"]; } set { ViewState["Text"]
                                        = value; } } 
This behavior is built into a lot of the standard controls, including TextBox.
      If you derive a control from TextBox (or from any other control that has automatic state maintenance features), you don't need to take any particular measures unless you want to add special properties to the bag. If you derive your custom element from Control, be ready to use ViewState to preserve part of your state from one request to the next.

A Table-based Input Form

      The LabeledTextBox is useful in that it glues together two elements that you often use in the same context. However, if you place several LabeledTextBox controls on a form, there's no way to automatically align textboxes and labels unless you use a fixed-size font and labels of the same length. In other circumstances you need to build a table for this. Let's see how to build a more complex component. The final goal of the next <InputForm> control is to allow you to write the following ASP .NET code:
<d:InputForm runat="server" > <FormField id="ffFirstName" Label="First
                                        Name" Text="" /> <FormField id="ffLastName" Label="Last Name" Text="Esposito"
                                        /> <FormField id="ffAddress" Label="Address" /> <FormField id="ffCity"
                                        Label="City" /> </d:InputForm> 
InputForm is the primary control that creates the surrounding table and governs the behavior, the position, and the content of the child labels and textboxes.

Figure 11 InputForm Output
Figure 11 InputForm Output

In Figure 11 you can see the expected output of such an InputForm control: a table with two columns, one for labels and one for textboxes. In terms of raw HTML, the control is merely a table with two columns of cells: one for labels and one for textboxes.
<table> <tr> <td><span>Label</span></td>
                                        <td><input type=text></td> </tr> ••• </table> 
      Assuming that you hold an InputForm class, you can create its child controls by overriding CreateChildControls. You probably want to stick to the familiar and statically declare your child controls in server pages instead of instantiating them dynamically in compiled code. The use of CreateChildControls is particularly suitable when you have a fixed structure and a nonmodifiable number of controls to create. With a variable length series of grouped textboxes, you need a flexible and HTML-like declarative syntax to exploit.
      InputForm is the control that will provide the surrounding environment. It outputs the HTML code to group the controls and configures them graphically by setting style properties like background color, font, and border style.
      You cannot reuse the LabeledTextBox control for InputForm's children because it was not designed to be used in a table. You need a control that manages a label and a textbox, but will allow them to be rendered in different cells of the same table row.
      Figure 12 shows the source code of the FormField. It doesn't hold Label and TextBox controls as data members, but keeps track of the text and CSS stylesheet to be used for both. It also has rendering capabilities hardcoded in the Render override.
      When you develop controls like this, you have to make sure that the child controls are of the specified and requested type. In this case, what could happen if other types of controls appeared in the body of the <InputForm> tag? The output of the following code is unpredictable and depends on InputForm.
<expo:InputForm runat="server" id="iform"> <asp:label runat="server"
                                        id="x" Text="???" /> <FormField id="ffFirstName" Label="First Name" />
                                        </expo:InputForm> 
What this snippet does depends on how you write the source code of the InputForm control as well as the assumptions that you make in doing so.
      The key question to ask is if there is any way to govern the type of controls declared as children of InputForm (and any other similarly grouped control). You can't always depend on other programmers to inherit from InputForm properly. Neither the compilers nor the runtime check the schema of your composite control. Is there a way to accept some child controls and discard others that you don't know about?

Control Builders

      Across the .NET Framework, certain words are used to express frequently used concepts. One word is "reader," whose meaning you find implemented in classes such as SqlDataReader, XmlTextReader, and StreamReader. Another one is "info," describing complex data structures such as FontInfo, DirectoryInfo, and FileInfo. Yet another is "builder." In addition to StringBuilder and SQLCommandBuilder, there's a ControlBuilder class that is of interest here.
      A control builder works with the parser to build a control, and any child controls it contains whenever a request is made for an ASP .NET page. It has a method called GetChildControlType that returns the .NET type object for the current control's children. In the body of this method you can check the tag name of the processed control and skip those that you know or don't want to handle.
      The association between controls and respective builders (if any) is realized through the ControlBuilderAttribute attribute:
[ ControlBuilderAttribute(typeof(MyControlBuilder)) ] public class InputForm
                                        : TextBox {...} 
In Figure 13 you can see the source code for the InputForm control and its specific control builder. In GetChildControlType you define the useable types based on the tag name. However, another important task is accomplished in AddParsedSubObject:
protected override void AddParsedSubObject(Object obj) { if (obj is BWSLib.FormField)
                                        m_formFields.Add(obj); } 
This method, which you should override in cases like this, passes on to your application an object instance that is relative to a parsed subobject. The argument obj is a .NET class instance that you can process at your leisure. Normally, if you have an array of controls to handle at rendering time, you might want to consider adding any of these instances to an internal collection for further processing.
      In particular, InputForm makes sure that the child control is a FormField object, and, if successful, adds the control to an array. The subobjects then be responsible for producing the interface of the control.
      When it comes to creating custom controls it's important to consider a couple of issues to obtain reusable code. First, design the component properly. If the component has to be inherited from another component, make sure your choice of the parent class is the best possible. I created InputForm from a Control object. Actually, since the InputForm is a table, inheriting from the Table object wouldn't have been a far-fetched idea. The second thing to consider is that there might be a few different ways to do the same thing, and you should be able to choose one of the best.

About DataGrids

      As with any other .NET control, you can build new controls starting from the DataGrid class. This way, you could try to package some of the vanilla code that I developed over the few past installments of Cutting Edge where I progressively explained the ASP .NET grid control. Next month, I'll start over again with the DataGrid and discuss the serious design issues you face when attempting to derive new grids. In addition, I'll answer one of the hottest grid-related questions: how and where do you add a new item?

Send questions and comments for Dino to cutting@microsoft.com.
Dino Esposito is a trainer and consultant based in Rome, Italy. Author of several books for Wrox Press, he now spends most of his time teaching classes on ASP .NET and ADO .NET for Wintellect (http://www.wintellect.com). Get in touch with Dino at dinoe@wintellect.com.

From the September 2001 issue of MSDN Magazine.


Page view tracker