Design ASP.NET Pages and Controls That Take Advantage of the DHTML Object Model

 

Dino Esposito
Wintellect

November 2003

Summary: Learn the basics of DHTML and ASP.NET integration, which can allow for changes of contents and layout in an ASP.NET page to be resolved on the client without roundtrips and server involvement. The resulting page is more responsive and gives users a better experience. (11 printed pages)

Contents

Introduction
Browser-Specific Code
Design a Browser-Sensitive Page
Summary

Download the source code for this article.

Introduction

Most Microsoft® ASP.NET controls are designed to be browser independent; they generate plain HTML markup that the majority of browsers easily understand and process. As a page developer, you can control to some extent the version of the HTML language used to generate the browser response. Some of the ASP.NET controls (for example, validation server controls) detect the capabilities of the underlying browser and adapt their response accordingly. In many cases, this means that the markup is enriched with script code that enhances the control's overall functionality.

Adding script code to the output of server controls is a excellent way to move on the client some of the burden associated with certain functions. A wise use of client-side script code can reduce the number of roundtrips without sacrificing functionality. Fewer roundtrips mean more responsive pages and a better workload balance between the server and the clients.

As the same name suggests, ASP.NET server controls are designed and programmed as server-side components. ASP.NET server controls are managed classes, belong to the Microsoft® .NET Framework, and follow the object-oriented paradigm. Their output is markup code that a browser can consume.

A browser is a client-side environment that processes markup code (HTML) and can optionally run script code. In many cases, the script code is a Javascript function that works on top of the object-based representation of the page content. This object model is browser-specific and is known as the HTML object model. Newest versions of Microsoft® Internet Explorer support a richer object model known as the Dynamic HTML (DHTML) object model.

Each ASP.NET server control generates a response that comprises HTML markup plus some script. With very few exceptions (the aforementioned validation controls), the script code embedded with ASP.NET controls is limited to the implementation of the form's postback mechanism, and doesn't enhance the client-side capabilities of the original control.

In this two-part article, we'll see how to design ASP.NET controls that embed script code with the goal of minimizing roundtrips, thus making the page more responsive and effective.

Browser-Specific Code

The option of using client-side script code in ASP.NET pages is strictly dependent on the browser's capabilities. If the browser is a recent one (more on this in a moment), you can feel free to use client-side script to improve the implementation of a functionality. Note that I've said functionality and not control, and with good reason. Some ASP.NET controls are the server-side counterpart of HTML tags. Some have a more abstract interface and implement a functionality that can be further enriched using script code. The equation "more script = more power" doesn't necessarily work for a single ASP.NET control; rather, it is more apt if applied to a Web functionality (whose implementation in turn involves individual controls).

Let's consider a first example (should I say experiment?) of successful DHTML integration within the implementation of an ASP.NET control. Suppose you have a page like that shown in Figure 1.

Aa479326.dhtmlobjectmodel_01(en-us,MSDN.10).gif

Figure 1. An optional form shows up if the user clicks on the hyperlink. The form can either be present in the page but hidden or be generated with a roundtrip.

Unregistered users would click the hyperlink and be redirected to a form where they would type their credentials in. Figure 2 demonstrates the new form.

Aa479326.dhtmlobjectmodel_02(en-us,MSDN.10).gif

Figure 2. A new form appears in the page giving the user a chance to register

The question is, where does the form come from? Which module generated it, and, more importantly, was it a client-side or server-side module?

A page that implements this feature would typically post back when the user clicks the hyperlink and return a modified markup to include the new group of controls. The following code shows how to do this.

<%@ Page Language="C#" %>

<script runat="server">
void ShowRegForm(object sender, EventArgs e) {
   RegPanel.Visible = true;
}
void OnLogIn(object sender, EventArgs e) {
   // Do something   
}
void OnRegister(object sender, EventArgs e) {
   // Do something   
}
</script>

<html>
<body>
<form runat="server">

<b>Login</b><br>
<asp:textbox runat="server" id="LogUserName" /><br>
<asp:textbox runat="server" id="LogPassword" textmode="password" /><br>
<asp:button runat="server" text="Log in" onclick="OnLogIn" />
<hr>
If not registered, click 
<asp:linkbutton runat="server" id="_clickHere"
text="here" onclick="ShowRegForm" />.
<hr>

<asp:panel runat="server" id="RegPanel" visible="false">
<b>Register</b><br>
<asp:textbox runat="server" id="RegUserName" /><br>
<asp:textbox runat="server" id="RegPassword" textmode="password" /><br>
<asp:button runat="server" text="Register" onclick="OnRegister" />
</asp:panel>

</form>
</body>
</html>

The registration form is fully declared in the body of the ASP.NET page but wrapped in an invisible Panel control. When the Visible attribute of a control is set to false, the control doesn't generate markup code. An instance of that control (and all of its children) is still created during the processing of the request. When the user clicks the hyperlink to register, the ShowRegForm event handler runs. It simply turns on the Visible attribute of the panel. As a result, the response for the browser now includes the previously invisible panel.

Is this implementation good enough? In terms of raw performance, this implementation is not very good. It forces the ASP.NET runtime to instantiate a handful (which can easily be dozens) of extra and likely unused controls. An alternate approach entails the dynamic creation of controls—perhaps an all-encompassing user control to minimize the need to write code. If the registration panel is going to be displayed, you create any needed controls and bind them to the server-side form. The approach is clearly lazier and as such faster. However, it requires you to write some extra code to manage the ViewState for the dynamically created controls. (Dynamic controls and ViewState management are off topic here, but you can learn more at http://www.aspnetpro.com/features/2003/06/asp200306de_f/asp200306de_f.asp.)

Having all controls statically declared makes the overall code of the page easier to read, understand, and modify. Overall, the impact on the request of the instantiation of possibly unused controls is negligible. It is worth noting that not only do invisible controls have no affect on the HTML markup, but there's no clue of them in the ViewState as well.

If you ever coded DHTML, you should know that one of the most common patterns in DHTML consists of hiding and displaying blocks of elements. You normally wrap HTML elements in a <div> tag and make them appear and disappear using the display style attribute. If you consider that the ASP.NET Panel control is nothing more than a client-side <div> tag, wouldn't it be worth trying a completely different implementation of the registration panel involving some client-side code?

The following HTML markup differs from the above in that it is wrapped by a static client-side <div> tag instead of a Panel. The display attribute set to none makes the <div> tag and its contents initially invisible.

<div id="RegPanel" style="display:none;">
<b>Register</b><br>
<asp:textbox runat="server" id="RegUserName" /><br>
<asp:textbox runat="server" id="RegPassword" textmode="password" /><br>
<asp:button runat="server" text="Register" onclick="OnRegister" />
<hr>
</div>

The value of the display attribute can be changed programmatically using the DHTML object model. In this way, the page will be updated to show the registration panel without posting back to the server. The key point is, how do you fire the event that modifies the value of the display attribute? In the previous, pure ASP.NET example, the "here" hyperlink was coded using a LinkButton. A LinkButton generates the following markup for a link button named _clickHere.

<a href="javascript:__doPostBack('_clickHere','')">here</a>

The __doPostBack function is a built-in Javascript function that the ASP.NET runtime automatically embeds in the page response. The function programmatically submits the HTML form, adding the information needed to let the ASP.NET runtime identify the server-side code to execute. In the example considered so far, the ShowRegForm server-side event handler turns the visibility of the Panel on. The same effect can be obtained with client-side code replacing the __doPostBack function with a custom Javascript function like the following.

<script language="javascript">
function ShowRegisterDlg() {
   RegPanel.style["display"] = "";
}
</script>

When users click on the hyperlink, the above script code changes the display status of the registration panel making it visible. The operation takes place entirely on the client and no roundtrip is required. The page is certainly more responsive, but the necessary markup is initially sent along with the rest of the page whether you click the link or not.

The following listing shows a DHTML version of the sample page. The lines in bold mark the difference from the previous, pure ASP.NET solution.

   <%@ Page Language="C#" %>

   <script runat="server">
   void OnLogIn(object sender, EventArgs e) {
      // Do something   
   }
   void OnRegister(object sender, EventArgs e) {
      // Do something   
   }
   </script>

   <script language="javascript">
   function ShowRegisterDlg() {
      RegPanel.style["display"] = "";
   }
   </script>

   <html>
   <body>
   <form runat="server">

   <b>Login</b><br>
   <asp:textbox runat="server" id="LogUserName" /><br>
   <asp:textbox runat="server" id="LogPassword" textmode="password" /><br>
   <asp:button runat="server" text="Log in" onclick="OnLogIn" />
   <hr>
   If not registered, click 
   <a href="javascript:ShowRegisterDlg()">here</a>

   <hr>

   <div id="RegPanel" style="display:none;">

   <b>Register</b><br>
   <asp:textbox runat="server" id="RegUserName" /><br>
   <asp:textbox runat="server" id="RegPassword" textmode="password" /><br>
   <asp:button runat="server" text="Register" onclick="OnRegister" />
   </div>


   </form>
   </body>
   </html>

The key difference between the two versions is in the implementation of the hyperlink that displays the registration panel. How can you design an ASP.NET control that uses server controls or DHTML script according to the capabilities of the underlying browser?

Design a Browser-Sensitive Page

Browser information is packed in the HttpBrowserCapabilities object returned by the Request.Browser property. Among the other information, the property returns the full string contained in the User-Agent header. This is probably the most flexible way to make sure the browser has just the characteristics you want to check. The following code shows how to make sure that the browser is Microsoft® Internet Explorer version 4.0 or newer:

bool upLevelBrowser = false;
HttpBrowserCapabilities caps = Request.Browser;
if (caps.Browser.ToUpper().IndexOf("IE") > -1) {
    // This is IE. Is version >3? 
    upLevelBrowser = (caps.MajorVersion >3);
}

By putting this code in the Page_Load event handler, you enable yourself to modify the structure of the page according to the capabilities of the browser. For example, when the user clicks on the hyperlink, what happens depends on the type of the browser. Although clear in the overall design, the solution is trickier to implement than one might at first think. The hidden difficulty revolves around the type of object the user really clicks on. As discussed earlier, whatever the browser, the user always clicks a hyperlink with some client script code attached. Since we're talking about ASP.NET pages, any script code is associated with the hyperlink on the server.

If the browser is up-level, you render the clickable hyperlink by using a client-side HTML element—that is, an anchor <a> element without the runat="server" attribute.

Click <a href="javascript:YourFunc()">here</a>

If the browser is down-level, you use the ASP.NET LinkButton control.

Click <asp:linkbutton runat="server" text="here" onclick="Clicked" /> 

How should you design the page layout to accommodate this requirement? You can use a classic ASP approach resulting in the following code snippet.

   If not registered, click 
   <% if (upLevelBrowser) {%>

   <a href="javascript:ShowRegisterDlg">here</a>
   <% } else {%>

   <asp:linkbutton runat="server" text="here" onclick="ShowRegisterDlg" />
   <% } %>

You need a global variable—upLevelBrowser—indicating the type of the browser, and you need to embed some script code in the resulting page. Although based on ASP code blocks—a feature that in some way violates the object-oriented approach of ASP.NET—this solution works great, but it is more appropriate for page-wide implementations and is not particularly reusable. Let's examine an alternative approach based on ASP.NET placeholders.

You use a PlaceHolder control to mark the place in which the hyperlink should appear in the page no matter what the form, postback link button or client-side script. Next, in the Page_Load event handler, you populate the Controls collection of the placeholder with a dynamically created instance of the LiteralControl or the LinkButton control. The following code snippet shows the page layout:

If not registered, click <asp:placeholder runat="server" id="theLink" />

In the Page_Load event handler, you first learn about the browser's capabilities and then configure the placeholder to host an HTML hyperlink or a server-side link button.

void Page_Load() {
    // Check browser capabilities
    bool upLevelBrowser = false;
    HttpBrowserCapabilities caps = Request.Browser;
    if (caps.Browser.ToUpper().IndexOf("IE") > -1) {
        // This is IE. Is version >3? 
        upLevelBrowser = (caps.MajorVersion >3);
    }

    // if downlevel (considering only IE4+ uplevel)
    if (upLevelBrowser) {
        AddDhtmlScriptCode(theLink);
        RegPanel.Visible = true; 
        RegPanel.Style["display"] = "none";
    }
    else
        AddPostBackCode(theLink);
}

If the browser is up-level, the registration panel should be included as HTML but not displayed to the user. For this reason, you must set the Visible property to true—ensuring that the HTML code for the panel will be generated—andat the same time, you need to hide the controls from view by resorting to the display style properties.

If the browser proves to be down-level (whatever this means to your application) you simply create and configure the link button control to receive the user's clicking and post back.

void AddPostBackCode(PlaceHolder ctl) {
    LinkButton link = new LinkButton();
    link.ID = "showRegPanel";
    link.Text = "here";
    link.Click += new EventHandler(this.ShowRegisterDlg);
    ctl.Controls.Add(link);
}

In case of an up-level browser, you need to embed some client script code in the page.

void AddDhtmlScriptCode(PlaceHolder ctl) {
    // Name of the Javascript function
    string scriptFuncName = "ShowRegisterDlg";

    // Token used to register the Javascript procedure  
    // with the instance of the Page object
    string scriptName = "__ShowRegisterDlg";

    // Create the hyperlink HTML code and add it to the placeholder
    string html = "<a href='javascript:{0}()'>{1}</a>";
    html = String.Format(html, scriptFuncName, "here");
    LiteralControl lit = new LiteralControl(html);
    ctl.Controls.Add(lit);

    // Create the Javascript function (must include <script>)
    StringBuilder sb = new StringBuilder("<script language=Jscript>\n");
    sb.Append("function ");
    sb.Append(scriptFuncName);
    sb.Append("() {\n");
    sb.Append("RegPanel.style['display'] = '';");
    sb.Append("\n}\n<");
    sb.Append("/");
    sb.Append("script>");

    // Register the Javascript function with the page so that it can be 
    // flushed when the page is rendered 
    if (!IsClientScriptBlockRegistered(scriptName))
             this.RegisterClientScriptBlock(scriptName, sb.ToString());
}

The AddDhtmlScriptCode function first replaces the placeholder with a literal text representing the <a> anchor that points to a custom Javascript function. Next, it inserts in the response text the source of this Javascript function. The RegisterClientScriptBlock method on the Page class keeps track that the developer requested that the specified script code must be inserted in a client-side <script> tag in the final HTML page. Figure 3 below shows the page in action on a down-level browser. Notice the target of the hyperlink on the status bar: if clicked, the page will post back.

Aa479326.dhtmlobjectmodel_03(en-us,MSDN.10).gif

Figure 3. A browser-sensitive page in action on a down-level browser

  1. A good question is, can I wrap this code in a browser-sensitive control? Of course you can. You can inherit your custom control from PlaceHolder and import the code that detects the browser. Next, override the Render method and output a LiteralControl or a LinkButton control. A handful of public properties will give your users a way to connect the server-side control to custom client-side script.

Summary

Integrating DHTML and ASP.NET server controls is not as easy and straightforward as it may at first seem. However, in many cases it means for the most part adding some Javascript code to a server-side control, for which the Page class provides several methods. In the second part of this article, I'll demonstrate a server-side control that make intensive use of DHTML client code to enhance its functionality. In particular, I'll discuss a collapsible panel a la Microsoft® Windows® XP. Stay tuned!

About the Author

Dino Esposito is a trainer and consultant based in Rome, Italy. Member of the Wintellect team, Dino specializes in ASP.NET and ADO.NET and spends most of his time teaching and consulting across Europe and the United States. Dino manages the ADO.NET courseware for Wintellect and writes the "Cutting Edge" column for MSDN Magazine.

© Microsoft Corporation. All rights reserved.