Share via


Cutting Edge

DHTML-Enabled ASP.NET Controls

Dino Esposito

Code download available at:CuttingEdge0507.exe(131 KB)

Contents

Anatomy of a Postback
The Client-Side Counterpart
DHTML Behaviors
A DropDownList Example
The Extended Object Model
Summing It Up

In the past, I've covered some core aspects of the interaction between DHTML behaviors, the browser, and ASP.NET runtime (see Cutting Edge: Extend the ASP.NET DataGrid with Client-side Behaviors and Cutting Edge: Moving DataGrid Rows Up and Down). But I haven't covered the intricacies of DHTML behaviors and advanced client-side scripting so I'll do that here. I'll show how to make ASP.NET code and the Internet Explorer DHTML Document Object Model (DOM) work together and discuss how you set up the communication between the ASP.NET runtime and a server-side instance of an ASP.NET control.

Anatomy of a Postback

To design an effective mechanism for cooperation between DHTML and server-side controls, you need a solid understanding of the ASP.NET postback mechanism. Imagine you have a page with a couple of textboxes and a Submit button. When a user clicks the button, the page posts back. The post can be initiated in one of two ways—through a Submit button or through script. A Submit button is represented by the HTML: <INPUT type="submit">. Most browsers also support posting via the submit method in a <form> element. In ASP.NET, the second approach is used for LinkButtons and auto-postbacks.

When a submit operation is initiated, the browser prepares and sends an HTTP request according to the form's contents. In ASP.NET, the "action" attribute of the sending form is set to the URL of the current page; the "method" attribute, on the other hand, can be changed at will and even programmatically. Possible methods include GET and POST.

The postback for an ASP.NET page that contains a couple of textboxes, a dropdown list, and a Submit button looks like this:

_VIEWSTATE=%D...%2D &TextBox1=One &TextBox2=Two &DropDownList1=one &Button1=Submit

The contents of all input fields, including hidden fields, are sent as part of the payload. In addition, the value of the currently selected item in all list controls is added, as is the name of the Submit button that triggered the post. If there are one or more LinkButtons on the page, two extra hidden fields called __EVENTTARGET and __EVENTARGUMENT are added to the payload:

__EVENTTARGET= &__EVENTARGUMENT= &_VIEWSTATE=%D...%2D &TextBox1=One &TextBox2=Two &DropDownList1=one &Button1=Submit

Both of these hidden fields are empty if the page posts back through a Submit button. If you post back through LinkButton in the page, the payload changes as follows:

__EVENTTARGET=LinkButton1 &__EVENTARGUMENT= &__VIEWSTATE=%D ... %2D &TextBox1=One &TextBox2=Two &DropDownList1=one

In this case, the __EVENTTARGET field contains the name of the LinkButton that initiated the post. ASP.NET uses this information when constructing the server-side representation of the requested page and in determining what caused the postback.

On the server, IIS picks up the request and forwards it on to the ASP.NET runtime. A pipeline of internal modules processes the request and instantiates a Page-derived class. The page class is an HTTP handler and, as such, implements the IHttpHandler interface. The runtime calls the Page's ProcessRequest method through the IHttpHandler interface and the server-side processing starts. Figure 1 provides an overall view of the request process.

Figure 1 ASP.NET Postback Process

Figure 1** ASP.NET Postback Process  **

Once the server-side processing of the page has begun, the Page object goes through a sequence of steps, as outlined in Figure 2. As its first step, the Page object creates an instance of all server controls that have a runat="server" attribute set in the requested ASPX source file. At this time, each control is created from scratch and has exactly the same attributes and values outlined in the ASPX source. The Page_Init event is fired when all controls have been initialized. Next, the page gives all of its controls a chance to restore the state they had last time the posting instance of that page was created. During this step, each control accesses the posted view state and restores its state as appropriate.

Figure 2 Page Lifecycle Events

Step Point of Interception
1. Page initialization Page_Init
2. Viewstate restoration LoadViewState overridable method on the Control class
3. Process posted data IPostBackDataHandler.LoadPostData on input controls
4. Page loading Page_Load
5. Data changed event IPostBackDataHandler.RaisePostDataChangedEvent on controls
6. Postback event IPostBackEventHandler.RaisePostBackEvent on controls
7. Prerendering phase Page_PreRender
8. Viewstate creation SaveViewState overridable method on the Control class
9. Rendering Render overridable method on the Control class
10. Page unloading Page_Unload

At this point, each control's state must be updated with any data posted by the browser. For this to happen, a special conversation is set up between individual controls and the ASP.NET runtime. This is an important point to consider in light of client-side interaction.

The ASP.NET Page class looks up the Form or QueryString collection, depending on the HTTP verb that was used to submit the request. The collection is scanned to find a match between a posted name and the ID property of a server-side control created to serve the request. For example, if the HTTP payload contains TextBox1=One, the Page class expects to find a server-side control named TextBox1. Each ASP.NET control lives on the server but retains a counterpart on the client. The link between them is the string containing the control's ID.

While the ASP.NET Page class can successfully locate a server control with a given name, it has no idea of the type of that control. In other words, from the page perspective, TextBox1 can be either a TextBox, a DropDownList, a DataGrid, or a custom control. For this reason, the Page class processes the control only if it adheres to an expected contract—the IPostBackDataHandler interface. If the control implements that interface, the page invokes its LoadPostData method. The method receives the name of the control (TextBox1, in the example) plus the collection of posted values—that is, Form or QueryString. As an example, a TextBox control will extract the corresponding value ("One", in the example) and compare it to its internal state. This behavior is common to all input controls and to all controls that expect to receive input data from the browser. For example, a DataGrid control that allows users to change the order of columns using drag and drop will, at this point, receive the modified order of columns.

The LoadPostData implementation depends on the characteristics and expected behavior of the particular control. The TextBox control compares the posted string to the value of its Text property. The DropDownList control compares the incoming data to the value of the currently selected item. If the compared values coincide, the method returns false. If the values differ, then the relevant control properties are updated and the method returns true. Figure 3 shows an implementation for a TextBox control.

Figure 3 IPostBackDataHandler Implementation

bool IPostBackDataHandler.LoadPostData( string name, NameValueCollection postedValues) { string oldValue = this.Text; string newValue = postedValues[name]; if (!oldValue.Equals(newValue)) { this.Text = newValue; return true; } return false; } void IPostBackDataHandler.RaisePostDataChangedEvent() { this.OnTextChanged(EventArgs.Empty); }

LoadPostData for a TextBox control compares the value posted for a given control with the current value of the Text property. Note that at the time this comparison is made, the Text property contains the value just restored from the view state. From now on, the state of the control is up to date and reflects the old state and the input coming from the client. The Boolean value that LoadPostData returns indicates whether or not the second method on the interface—RaisePostDataChangedEvent—must be invoked later. A return value of true means that the value of Text (or the property or properties a control updates with posted values) has been refreshed and subsequently the TextBox raises a server-side data-changed event. For a TextBox control, this event is TextChanged.

Once this step has been accomplished, the Page_Load event is fired and a second check is made on the control that appears to be responsible for the postback (based on the information sent from the browser). If this control implements IPostBackEventHandler, the RaisePostBackEvent method is invoked to give the control a chance to perform the postback action. The following pseudocode illustrates the implementation of this method for the Button class:

void IPostBackEventHandler.RaisePostBackEvent(string eventArgument) { if (CausesValidation) Page.Validate(); OnClick(new EventArgs()); OnCommand(new CommandEventArgs( CommandName, CommandArgument)); }

As you can see, when a button is clicked and the host page has completed its restoration process, the OnClick event is invoked, followed by OnCommand. A similar piece of code serves the LinkButton class. Code like this is used for any custom controls that require the post action to be started on the client.

The Client-Side Counterpart

Each server control outputs some markup that is sent down to the client. The browser then uses that information to build a DOM rooted in the outermost tag of the control's markup. Simple server controls such as TextBox map directly to HTML elements; more complex controls like the DataGrid map to a subtree of HTML elements, in many cases rooted in an HTML table tag. The root tag, or the most significant tag in the HTML, is given a name (the name HTML attribute) that matches the ID of the server control. This guarantees that the ASP.NET runtime can correctly match up client HTML elements with instances of server-side controls.

When you use or build an ASP.NET control with rich client-side functionalities you end up with at least two related problems. First, you have to figure out how to transfer to the server any input generated on the client. Second, you must make sure that the server control retrieves and properly handles that chunk of information. A third issue revolves around the format you use to send data across the wire.

There might be many ways to solve these issues and, frankly, any approach that works is valid. But when writing code for an ASP.NET control, why not do as the ASP.NET team did. That's where that anatomy of a postback fits in.

In the two articles that I mentioned at the beginning of this piece, I create a custom DataGrid control and collect some user input through drag and drop and other client-side operations. The input is then serialized to a string and packed into a hidden field. The hidden field is like any other <INPUT> tag except that it doesn't show up in the user interface. The hidden field is part of the form and its contents are picked up and used to prepare the HTTP payload when a postback is made. The hidden field is created by the server-side control and given the same ID of the control.

For example, a custom DataGrid named DragDropGrid1 will create its own personal hidden field with the same name. Any client-side action that is relevant to the behavior of the grid is persisted to the hidden field. When the page posts back, that information is carried to the server and consumed by ASP.NET in the manner described earlier. The matching ID determines the link between the contents of the input field and a server-side control. The control-specific implementation of the IPostBackDataHandler interface does the rest, giving the control a chance to modify its server state in light of client-side user actions.

If you get to consider simple and basic controls such as TextBox and DropDownList, then the <INPUT> element and the displayed user interface are the same thing. Any user interface-related operation automatically modifies the contents associated with the input element. This is much less automatic with more complex and advanced controls. Again, think of a DataGrid control that allows row movements or columns by drag and drop. The user interface of a DataGrid is a mere HTML table padded with plain text. The hidden field to carry data is silently created as part of the markup and injected in the page. Some additional code is needed to capture UI events and persist results to the hidden field.

What do you think this additional code should look like? Can it really be different from mere script code? It has to be pure JavaScript code at its core, but if the browser supports it, you can wrap it up in a more elegant and neater object model—that's mostly what a DHTML behavior is all about.

DHTML Behaviors

DHTML behaviors are a feature of Internet Explorer 5.0 and later. They're not supported by any other browser. A DHTML behavior component can be written in any Internet Explorer-compatible scripting language (usually JavaScript) and supplies dynamic functionality that can be applied to any element in an HTML document through CSS style sheets. DHTML behaviors use CSS to separate script and content in a document using an .htc that incorporates all the DHTML-based functionality needed to describe and implement a given behavior. This behavior, in turn, can be attached to a variety of HTML elements via a new CSS style. Put another way, DHTML behaviors bring the benefits of reuse to the world of scripting.

What's in a DHTML behavior? First, a behavior component can define an object model—a collection of methods, properties, and events that describe the provided behavior and supply tools to control it programmatically. In addition, a DHTML behavior needs to capture some page- and element-level events and handle them. You code this through classic HTML event handlers. You have access to the whole page DOM and can read and write attributes throughout the page. Figure 4 shows an HTC component that allows expanding and collapsing the children of the element to which it is applied.

Figure 4 DHTML Behavior Component

<PROPERTY NAME="Expanded" /> <ATTACH EVENT="onreadystatechange" HANDLER="Init" /> <ATTACH EVENT="onclick" HANDLER="HandleClick" /> <script language="javascript"> // Handles the initialization phase function Init() { if (Expanded == null) Expanded = true; // Toggle visibility for all children of THIS element for (i=0; i<children.length; i++) { if (Expanded == true) children[i].style.display = ""; else children[i].style.display = "none"; } } // Handles the OnClick event on the current element function HandleClick() { var i; var style; // Make sure the sender of the event is THIS element if (event.srcElement != element) return; // Toggle visibility for all children of THIS element for (i=0; i<children.length; i++) { style = children[i].style; if (style.display == "none") { style.display = ""; } else { style.display = "none"; } } } </script>

In DHTML, the expand/collapse functionality is achieved by toggling the value of the display attribute in the style object. A value of "none" keeps the element hidden; a value of "" (empty string) makes the element visible.

The core functionality is found in a <script> tag that collects public event handlers as well as internal functions and classes. Outside of the <script> tag, you define the object model of the behavior and the internal events it wants to handle:

<PROPERTY NAME="Expanded" /> <ATTACH EVENT="onreadystatechange" HANDLER="Init" /> <ATTACH EVENT="onclick" HANDLER="HandleClick" />

The preceding code snippet declares a variable named Expanded and a couple of handlers for the onclick and onreadystatechange DOM events. Properties can be assigned a value in the HTML source through the mechanism of attributes. Event handlers must be defined in the <script> tag. The onreadystatechange event is a common presence in many DHTML behaviors because it represents the initialization phase of the component. In Figure 4, you check the value of Expanded in the initializer and, based on that, you toggle the visibility value of child elements.

To attach a behavior, you use CSS notation (behaviors are ignored in browsers that do not support CSS):

<style> .LIST {behavior:url(expand.htc);} </style>

The CSS attribute is named "behavior". It is assigned a URL that ultimately points to the HTC file. Once you have defined a LIST class, you assign it to any HTML element that requires it:

<ul class="LIST" style="cursor: hand;" expanded="false">

As you see, any public properties defined on the behavior can be initialized as an attribute in any tags that contain style attributes.

Other than the public object model, DHTML behavior offers nothing that you can't get through plain scripting. But with a single attribute you can attach a certain behavior to a given HTML element or to the root of a HTML element subtree, as is the case with ASP.NET controls. A DHTML behavior can encapsulate a lot of details regarding the internal implementation of the behavior and it has full access to the page's DOM.

A DropDownList Example

In both aforementioned articles, I glossed over the code that specifically handles browser/server communication. Now it's time to focus on what the control needs to do in order to receive and properly process on the server any client-side input. The sample ExtendedDropDownList control is a custom control that is derived from the basic DropDownList control:

public class ExtendedDropDownList : System.Web.UI.WebControls.DropDownList { ... }

The most important difference between the basic and extended dropdown control is that the extended one exposes a client-side object model to let script code add elements dynamically. Newly added elements are added to the items collection and sent to the server out-of-band, that is outside the classic format that the HTTP payload takes when a dropdown control is involved.

In ASP.NET, the DropDownList control is designed to be read-only across postbacks. In other words, any list items dynamically added through DHTML code are lost once the page posts back to the server. The canonical HTTP payload doesn't include the items in the dropdown list. It only mentions the ID of the currently selected item. Control-specific information generated on the client can get to the server only in a hidden field. The hidden field can be given an arbitrary name, but in general you give the worker hidden field the same ID as the server control. As explained in the earlier "Anatomy of a Postback" section, this guarantees that the ASP.NET runtime invokes the methods of the IPostbackDataHandler interface on the control to post incoming data. For a custom DropDownList control, though, things are a little bit different. In fact, the base DropDownList control already requires an input element with the same name as the control's ID. This is a <SELECT> element:

<SELECT name="DropDownList1"> <OPTION value="one">One <OPTION value="two">Two <OPTION value="three">Three </SELECT>

A custom and enhanced dropdown list control requires an extra hidden field to carry the text and IDs of the additional items appended at the client. To avoid naming conflicts, this hidden field must have a different name. In my code, I use "_My" to postfix the ID. This is arbitrary, but once you choose a naming convention you must stick with it. In the source code of the customized ExtendedDropDownList control (available in the code download), the control defines a Boolean property to enable client-side insertion and overrides the OnPreRender method. The OnPreRender method simply registers the additional hidden field with the predetermined name. The hidden field is empty when the page is rendered to the client and will be filled as the user works with the control adding items dynamically to the list.

What about the contents of the hidden field? Should you use any specific format or convention? The data being passed to the server should be laid out according to a format that is known to both the server control and the client-side DHTML behavior. The format you choose is arbitrary as long as it achieves the expected goals. In this example, I'll use a pipe-separated pair of strings for each dynamically added item (note that without proper escaping, this prohibits the use of the pipe character in the actual values). The left part of the pair represents the text; the right part is for the ID. Here's an example:

One|id1,Two|id2

The custom ExtendedDropDownList control must implement the IPostBackDataHandler interface from scratch. The implementation on the base class is marked as private and can't be invoked from within a derived class.

The code in the LoadPostData method serves two main purposes. First, it manages the index of the selected item. The ID of this item comes through the HTTP payload and is matched against the current contents of the dropdown list. The index found (if any) is then used to overwrite the value of the SelectedIndex property. If this results in a change to the existing value, the host page gets a SelectedIndexChanged event. It is worth noting here that the event is not raised by the ASP.NET runtime. To be more precise, when LoadPostData returns true, the ASP.NET runtime invokes the RaisePostDataChangedEvent method on the same IPostBackDataHandler interface. By implementing this method, a control can fire a proper event.

The second goal of LoadPostData for the ExtendedDropDownList control is populating the Items collection with the new elements just added on the client. As mentioned, text and ID of these new elements are stored in the control's hidden field.

You can decide to raise an ad hoc event to signal that new elements have been added on the client. To do so, you define a private Boolean variable (_addedNewElements in the example) that is set during the execution of LoadPostData only if new elements have been added, as shown here:

void IPostBackDataHandler.RaisePostDataChangedEvent() { OnSelectedIndexChanged(EventArgs.Empty); if (_addedNewElements) OnNewItemsAdded(EventArgs.Empty); }

NewItemsAdded is a custom server-side event that is fired right after SelectedIndexChanged and before the postback event caused by the submit control:

public event EventHandler NewItemsAdded;

With the control's implementation discussed so far, when the Page_Load event is fired to the page the dropdown list control has been fully rebuilt to reflect the changes on the client—the new items are now definitely part of the control's state.

The Extended Object Model

When the dropdown list is displayed on the client, it takes the form of a <SELECT> tag. The DHTML object model designs a tree of objects around these elements and gives you the tools to add or remove items programmatically via JavaScript. The following code shows what you really need to execute:

var oOption = document.createElement("OPTION"); element.options.add(oOption); oOption.innerText = text; oOption.value = id;

By employing a DHTML behavior, you can wrap the previous code in a new method that's easier to use. Figure 5 details my dropdownlistex.htc behavior. It contains a Boolean property to enable support for client-side insertions as well as a client-side method named AddItem. The code for this method actually extends the DHTML tree for a regular dropdown element and simplifies the insertion of a new item. When attached to a client-side button, the following code adds a new item to the specified dropdown list entirely on the client. As you can see, it leverages the AddItem method defined on the behavior:

<SCRIPT lang="javascript"> function InsertTheNewItem() { var obj = document.getElementById("NewElement"); var text = obj.value; var id = obj.value; document.getElementById("DropDownList2").AddItem(text, id); } </SCRIPT>

Figure 5 The DropDownListEx.htc Behavior

<PROPERTY NAME="Modifiable" /> <METHOD NAME="AddItem" /> <ATTACH EVENT="onreadystatechange" HANDLER="Init" /> <script language="javascript"> // Handles the initialization phase function Init() { if (Modifiable == null) Modifiable = false; } // Add a new item programmatically function AddItem(text, id) { if (!eval(Modifiable)) return false; var oOption = document.createElement("OPTION"); element.options.add(oOption); oOption.innerText = text; oOption.value = id; var hiddenField = GetHiddenField(element.id + "_My"); // Add a separator var tmp = hiddenField.value; if (tmp != "") hiddenField.value += ","; hiddenField.value += text + "|" + id; } function GetHiddenField(fieldName) { // Go up in the hierarchy until the FORM is found var obj = element.parentElement; while (obj.tagName.toLowerCase() != "form") { obj = obj.parentElement; if (obj == null) return null; } if (fieldName != null) return obj[fieldName]; } </script>

Summing It Up

DHTML behaviors are Internet Explorer client-side components that encapsulate a given behavior and attach it to an HTML element. From the ASP.NET perspective, you can utilize these components to enrich server controls with advanced, browser-specific capabilities. It is important to realize that DHTML behaviors are not strictly necessary to endow server controls with powerful client capabilities; their use, however, makes it possible to better encapsulate all of the required features in a reusable and easily accessible object model.

In addition to learning the internal mechanics of DHTML behaviors, you should become familiar with the postback interfaces of ASP.NET controls—in particular, IPostBackDataHandler. This interface lets developers handle any posted data whose format and layout is entirely up to you. A deep understanding of this interface is key to implementing effective interaction between browser and server environments within the boundaries of ASP.NET controls.

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

Dino Esposito is a Wintellect instructor and consultant based in Italy. Author of Programming ASP.NET and the new book Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching classes in ASP.NET and ADO.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.