Server Controls and Client-Side Scripting

You can do a lot with custom server controls by returning ordinary HTML to the client. But some of the most exotic (and potentially useful) server controls in the world return client-side script as well. The ASP.NET validation controls are a great example. They couldn’t validate user input on the client side without some help from the browser, and one way to get the browser involved in the execution of a control is to return script that the browser understands.

The chief benefit of writing controls that return client-side script is performance. The more you can do on the client, the better a page will perform. Why? Because executing code in a browser is far faster than sending HTTP requests to a server. Years ago I took part in an online discussion forum that diagrammed message threads with a browser-based tree control. Using the forum was a nightmare because each time you clicked a plus sign to expand a branch of the tree, a request went back to the server, which generated a new GIF depicting the tree with the branch expanded and streamed the GIF back to the client. With a dial-up connection, a branch took a minute or more to expand! That was a classic example of how not to design Web pages. Now that most browsers support client-side scripting, the same tree control can be implemented in script and branches can be expanded and collapsed locally, without incurring round trips to the server.

High-performance tree controls are just one example of the wonders you can accomplish with client-side script. Server controls and scripting are a match made in heaven because a control can hide the script that it relies on under the hood where it belongs. Few veteran Windows developers are experts in client-side scripting too, but they don’t have to be to use controls that generate script. That, after all, is what encapsulation is all about: hiding difficult implementation details behind the veil of reusable components so that anyone can write sophisticated applications, regardless of their background or experience level.

Most controls that emit client-side script return JavaScript, also known as JScript and ECMAScript, rather than VBScript. VBScript works fine in Internet Explorer, but it’s unsupported in other browsers. Returning JavaScript provides a measure of browser independence. In reality, that independence is more measured than we would like because it’s difficult to write scripts that work equally well in Internet Explorer, Netscape Navigator, and other browsers. Still, I’ll use JavaScript for all the sample controls in this chapter that return client-side script and, when necessary, customize the script on the fly to match the browser type, enabling it to work in Internet Explorer and Navigator.

Returning JavaScript to the Client

The simplest way to return JavaScript from a custom control is to output it from the control’s Render method. The control in Figure 8-24 does just that. It’s a simple control called MessageButton that renders out an <input type=“submit”> tag. Inside the tag is an OnClick attribute that calls JavaScript’s alert function when the button is clicked. If you declare a control instance like this:

<win:MessageButton?Text="Click?Me" Message="Hello,?world"?RunAt="server" />

the control renders itself this way:

<input?type="submit" name="_ctl1" value="Click?Me"
??onclick="javascript:alert?('Hello,?world')">

As a result, clicking the button pops up “Hello, world!” in a message box.

MessageButton1.cs
using?System;
using?System.Web.UI;

namespace?Wintellect
{
????public?class?MessageButton?:?Control
????{
????????string?MyText?= "";
????????string?MyMessage?= "";

????????public?string?Text
????????{
????????????get?{?return?MyText;?}
????????????set?{?MyText?=?value;?}
????????}

????????public?string?Message
????????{
????????????get?{?return?MyMessage;?}
????????????set?{?MyMessage?=?value;?}
????????}
????????protected?override?void?Render?(HtmlTextWriter?writer)
????????{
????????????writer.WriteBeginTag?("input");
????????????writer.WriteAttribute?("type", "submit");
????????????writer.WriteAttribute?("name",?UniqueID);

????????????if?(ID?!=?null)
????????????????writer.WriteAttribute?("id",?ClientID);

????????????if?(Text.Length?>?0)
????????????????writer.WriteAttribute?("value",?Text);

????????????if?(Message.Length?>?0)
????????????????writer.WriteAttribute?("onclick",
??????????????????? "javascript:alert?(\'" +?Message?+ "\')");

????????????writer.Write?(HtmlTextWriter.TagRightChar);
????????}
????}
}
Figure 8-24
MessageButton control, version 1.
The RegisterClientScriptBlock Method

Returning client-side script from a control’s Render method is fine when the script is simple enough to be embedded in control tags, but what about more complex scripts? Many controls return client-side script blocks containing functions that are called from control tags. Here’s how the OnClick attribute in the previous section would look if instead of calling alert directly, it called a local function named doAlert:

<script?language="javascript">
<!--
function?doAlert?(message)
{
????alert?(message);
}
-->
</script>
??.
??.
??.
<input?type="submit" name="_ctl1" value="Click?Me"
??onclick="javascript:doAlert?('Hello,?world')">

Rather than return blocks of client-side script by manually writing them out with HtmlTextWriter, controls should return them with Page.RegisterClientScriptBlock. RegisterClientScriptBlock returns a block of client-side script and ensures that it’s returned only once, no matter how many controls on the page use it. RegisterClientScriptBlock accepts two string parameters: one that assigns the script a name, and another that contains the script itself. If a control outputs a block of script using HtmlTextWriter, a page containing 10 instances of the control will see the same script block replicated 10 times. If the control uses RegisterClientScriptBlock, however, the block is returned only one time because each control instance registers the script block using the same name.

The version of MessageButton in Figure 8-25 displays message boxes by calling a local function in a client-side script block. The block is returned by RegisterClientScriptBlock. Note where RegisterClientScriptBlock is called: in the OnPreRender override. ASP.NET calls OnPreRender on every control on a page before calling any of the controls’ Render methods. Calling RegisterClientScriptBlock from OnPreRender ensures that the script block is registered early enough in the page rendering process to allow ASP.NET to control the position of the script block in the output. As you’ve probably already surmised, OnPreRender is another of the virtual methods that a control inherits from Control.

MessageButton2.cs
using?System;
using?System.Web.UI;

namespace?Wintellect
{
????public?class?MessageButton?:?Control
????{
????????string?MyText?= "";
????????string?MyMessage?= "";

????????public?string?Text
????????{
????????????get?{?return?MyText;?}
????????????set?{?MyText?=?value;?}
????????}

????????public?string?Message
????????{
????????????get?{?return?MyMessage;?}
????????????set?{?MyMessage?=?value;?}
????????}

????????protected?override?void?OnPreRender?(EventArgs?e)
????????{
????????????Page.RegisterClientScriptBlock?(
??????????????? "MessageButtonScript",
??????????????? "<script?language=\"javascript\">\n" +
??????????????? "<!--\n" ????????????????????????????+
??????????????? "function?doAlert?(message)\n" ??????+
??????????????? "{\n" ???????????????????????????????+
??????????????? " ???alert?(message);\n" ????????????+
??????????????? "}\n" ???????????????????????????????+
??????????????? "-->\n" ?????????????????????????????+
??????????????? "</script>"
????????????);
????????}

????????protected?override?void?Render?(HtmlTextWriter?writer)
????????{
????????????writer.WriteBeginTag?("input");
????????????writer.WriteAttribute?("type", "submit");
????????????writer.WriteAttribute?("name",?UniqueID);

????????????if?(ID?!=?null)
????????????????writer.WriteAttribute?("id",?ClientID);

????????????if?(Text.Length?>?0)
????????????????writer.WriteAttribute?("value",?Text);

????????????if?(Message.Length?>?0)
????????????????writer.WriteAttribute?("onclick",
??????????????????? "javascript:doAlert?(\'" +?Message?+ "\')");

????????????writer.Write?(HtmlTextWriter.TagRightChar);
????????}
????}
}
Figure 8-25
MessageButton control, version 2.

RegisterClientScriptBlock prevents a function from being downloaded to a page multiple times. ASP.NET supports a similar method named RegisterStartupScript whose purpose isn’t to return client-side JavaScript functions but to return ordinary script—code that’s not contained in functions—that executes when the page loads. The difference between RegisterClientScriptBlock and RegisterStartupScript is the location at which they position the scripts registered with them. RegisterClientScriptBlock puts the scripts near the top of the document, shortly after the <form> tag. RegisterStartupScript puts them just before the </form> tag. The placement of the startup script is important because if the script interacts with other elements on the page, those elements must be loaded before the script executes. Placing startup script near the end of the document ensures that other elements on the page are present and accounted for when the script runs.

Keeping Your Code Off the Client

The <script> tag supports a Src attribute that permits scripts to be referenced remotely. The following code creates a message button that calls a function named doAlert from a JS file on the server:

<script?language="javascript" src="/jscript/messagebutton.js">
</script>
??.
??.
??.
<input?type="submit" value="Click?Me"
??onclick="javascript:doAlert?('Hello,?world')">

Messagebutton.js contains doAlert’s implementation. Keeping the code on the server serves two purposes:

This is precisely how ASP.NET’s validation controls work. They emit a modest amount of client-side script to the browser, but most of their work is done by functions in a script file named WebUIValidation.js that’s stored on the server.

Figure 8-26 contains the final version of MessageButton—one that emits a <script> tag with a Src attribute pointing to a JavaScript file on the server. Functionally, this version is identical to the previous two. But view the source code returned to the browser, and you’ll see that the doAlert function is no longer visible.

MessageButton3.cs
using?System;
using?System.Web.UI;

namespace?Wintellect
{
????public?class?MessageButton?:?Control
????{
????????string?MyText?= "";
????????string?MyMessage?= "";

????????public?string?Text
????????{
????????????get?{?return?MyText;?}
????????????set?{?MyText?=?value;?}
????????}

????????public?string?Message
????????{
????????????get?{?return?MyMessage;?}
????????????set?{?MyMessage?=?value;?}
????????}

????????protected?override?void?OnPreRender?(EventArgs?e)
????????{
????????????Page.RegisterClientScriptBlock?(
??????????????? "MessageButtonRemoteScript",
??????????????? "<script?language=\"javascript\" " +
??????????????????? "src=\"/JScript/MessageButton.js\"></script>");
????????}

????????protected?override?void?Render?(HtmlTextWriter?writer)
????????{
????????????writer.WriteBeginTag?("input");
????????????writer.WriteAttribute?("type", "submit");
????????????writer.WriteAttribute?("name",?UniqueID);

????????????if?(ID?!=?null)
????????????????writer.WriteAttribute?("id",?ClientID);

????????????if?(Text.Length?>?0)
????????????????writer.WriteAttribute?("value",?Text);

????????????if?(Message.Length?>?0)
????????????????writer.WriteAttribute?("onclick",
??????????????????? "javascript:doAlert?(\'" +?Message?+ "\')");

????????????writer.Write?(HtmlTextWriter.TagRightChar);
????????}
????}
}
Figure 8-26
MessageButton control, version 3, and the associated JavaScript file.
MessageButton.js
function?doAlert?(message)
{
????alert?(message);
}

Now that you’ve seen the mechanics of how controls return client-side script, let’s put this newfound knowledge to work building some intelligent controls that do their work without incurring round trips to the server.

The RolloverImageLink Control

One of the most common scripted elements found on Web sites is the “rollover image.” When the cursor goes over the top of it, the image changes; when the cursor moves away, the image reverts to its original form. Usually, the image is combined with a hyperlink so that clicking it jumps to another URL. The rollover effect adds visual flair that draws the user’s attention and says “click here.”

Rollover images are relatively simple to implement with JavaScript and DHTML. The following HTML file demonstrates how image rollovers work. An <img> tag is enclosed in an anchor element. The anchor element includes OnMouseOver and OnMouseOut attributes that dynamically change the image’s Src attribute when the mouse enters and leaves the image:

<html>
??<body>
????<a?href="next.html"
??????onmouseover="javascript:document.myimage.src='logo2.jpg'"
??????onmouseout="javascript:document.myimage.src='logo1.jpg'">
??????<img?name="myimage" src="logo1.jpg">
????</a>
??</body>
</html>

The RolloverImageLink control in Figure 8-27 encapsulates this behavior in a custom control. Assuming RolloverImageLink is compiled into a DLL named RolloverImageLinkControl.dll, creating a rollover image hyperlink is as simple as this:

<%@?Register?TagPrefix="win" Namespace="Wintellect"
??Assembly="RolloverImageLinkControl" %>
????.
????.
????.
<win:RolloverImageLink?NavigateUrl="next.html" RunAt="server"
??OnImageUrl="image1.jpg" OffImageUrl="image2.jpg" />

The control has three public properties: NavigateUrl, OnImageUrl, and OffImageUrl. NavigateUrl identifies the target of the hyperlink. OnImageUrl and OffImageUrl identify the images shown when the cursor is over the image and when it’s not. The source code should be easy to understand given that the control’s output looks very much like the HTML shown previously.

RolloverImageLink.cs
using?System;
using?System.Web.UI;

namespace?Wintellect
{
????public?class?RolloverImageLink?:?Control
????{
????????string?MyNavigateUrl?= "";
????????string?MyOnImageUrl?= "";
????????string?MyOffImageUrl?= "";

????????public?string?NavigateUrl
????????{
????????????get?{?return?MyNavigateUrl;?}
????????????set?{?MyNavigateUrl?=?value;?}
????????}

????????public?string?OnImageUrl
????????{
????????????get?{?return?MyOnImageUrl;?}
????????????set?{?MyOnImageUrl?=?value;?}
????????}

????????public?string?OffImageUrl
????????{
????????????get?{?return?MyOffImageUrl;?}
????????????set?{?MyOffImageUrl?=?value;?}
????????}

????????protected?override?void?Render?(HtmlTextWriter?writer)
????????{
????????????//?Output?an?<a>?tag
????????????writer.WriteBeginTag?("a");

????????????if?(NavigateUrl.Length?>?0)
????????????????writer.WriteAttribute?("href",?NavigateUrl);

????????????if?(OnImageUrl.Length?>?0?&&?OffImageUrl.Length?>?0)?{
????????????????writer.WriteAttribute?("onmouseover",
??????????????????? "javascript:document." +?ClientID?+ ".src=\'" +
?????????????????????OnImageUrl?+ "\'");
????????????????writer.WriteAttribute?("onmouseout",
??????????????????? "javascript:document." +?ClientID?+ ".src=\'" +
?????????????????????OffImageUrl?+ "\'");
????????????}
????????????writer.Write?(HtmlTextWriter.TagRightChar);

????????????//?Output?an?<img>?tag
????????????writer.WriteBeginTag?("img");
????????????writer.WriteAttribute?("name",?ClientID);
????????????if?(OffImageUrl.Length?>?0)
????????????????writer.WriteAttribute?("src",?OffImageUrl);
????????????writer.Write?(HtmlTextWriter.TagRightChar);

????????????//?Output?a?</a>?tag
????????????writer.WriteEndTag?("a");
????????}
????}
}
Figure 8-27
RolloverImageLink control.
The NumTextBox Control

For years, Windows developers have customized edit controls by processing the EN_CHANGE notifications that fire as individual characters are typed and filtering out unwanted characters. A common application for this technique is to build numeric edit controls that accept numbers but reject letters and other symbols.

You can achieve the same effect in Web applications with JavaScript and DHTML. Modern browsers fire OnKeyDown events as characters are typed into a text box. A Web page can register a handler for OnKeyDown events and filter out unwanted characters by returning false from the event handler. The following Web page demonstrates how it works. The OnKeyDown attribute in the <input type =“text”> tag activates the JavaScript isKeyValid function on each keystroke. The isKeyValid function examines the key code and returns true if the key represents a numeric character or any of a number of auxiliary keys, such as Tab, Backspace, and Delete. Try it out and you’ll find that the input field won’t accept any characters other than the numbers 0 through 9:

<html>
??<head>
????<script?language="javascript">
????<!--
??????var?keys?=?new?Array?(8,?9,?13,?33,?34,?35,?36,?37,?39,?45,?46);

??????function?isKeyValid?(keyCode)
??????{
??????????return?((keyCode?>=?48?&&?keyCode?<=?57)?││
??????????????isAuxKey?(keyCode));
??????}

??????function?isAuxKey?(keyCode)
??????{
??????????for?(i=0;?i<keys.length;?i++)
??????????????if?(keyCode?==?keys[i])
??????????????????return?true;
??????????return?false;
??????}
????-->
????</script>
??</head>
??<body>
????<form>
??????<input?type="text" name="quantity"
??????onkeydown="javascript:return?isKeyValid?(window.event.keyCode)">
????</form>
??</body>
</html>

This example works in Internet Explorer 4 and later. To work in Netscape Navigator 4 and later, the script must be modified:

<html>
??<head>
????<script?language="javascript">
????<!--
??????function?isKeyValid?(keyCode)
??????{
??????????return?((keyCode?>=?48?&&?keyCode?<=?57)?││
??????????????keyCode?==?8?││?keyCode==?13);
??????}
????-->
????</script>
??</head>
??<body>
????<form>
??????<input?type="text" name="quantity"
????????onkeydown="javascript:return?isKeyValid?(event.which)">
????</form>
??</body>
</html>

Once more, this behavior is begging to be encapsulated in a custom control to shield the developer from the nuances of DHTML and client-side script. It also presents the perfect opportunity to demonstrate adaptive rendering—varying the script that a control outputs based on the browser it renders to.

Figure 8-28 contains the source code for a NumTextBox control that trivializes the task of creating numbers-only text boxes:

<%@?Register?TagPrefix="win" Namespace="Wintellect"
??Assembly="NumTextBoxControl" %>
????.
????.
????.
<win:NumTextBox?ID="Quantity" RunAt="server" />

NumTextBox exposes its content through a Text property that throws an exception if a noninteger value is assigned. It uses RegisterClientScriptBlock to register the <script> block containing isKeyValid and its helpers. And it works with both Internet Explorer and Netscape Navigator, thanks to adaptive rendering logic that returns one set of client-side script to Internet Explorer and another to Navigator. If the requestor is neither Internet Explorer nor Navigator (or is a down-level version of either), the control returns no client-side script because chances are it won’t work anyway.

How does NumTextBox adapt its output to the browser type? By using the Request object’s Browser property, which contains a variety of information about the browser that made the request, including its make and version number. As described in Chapter 7, ASP.NET reads the User-Agent headers accompanying HTTP requests and populates the Browser property with information inferred from those headers. NumTextBox checks the browser’s type and version number by reading Browser’s Type and MajorVersion properties:

string?browser?=?Context.Request.Browser.Type.ToUpper?();
int?version?=?Context.Request.Browser.MajorVersion;
??.
??.
??.
if?(browser.IndexOf?("IE")?>?-1?&&?version?>=?4)?{
????//?Internet?Explorer?4?or?later
}
else?if?(browser.IndexOf?("NETSCAPE")?>?-1?&&?version?>=?4)?{
????//?Netscape?Navigator?4?or?later
}

For Internet Explorer, Browser.Type returns a string of the form “IE4,” while for Navigator it returns a string such as “Netscape4.” Using String.IndexOf to check for the substrings “IE” and “Netscape” detects requests emanating from these browsers.

NumTextBox.cs
using?System;
using?System.Web.UI;

namespace?Wintellect
{
????public?class?NumTextBox?:?Control
????{
????????string?MyText?= "";

????????string?IEClientScriptBlock?=
??????????? "<script?language=\"javascript\">\n" ??????????????+
??????????? "<!--\n" ??????????????????????????????????????????+
??????????? "var?keys?=?new?Array?(8,?9,?13,?33,?34,?35, " ????+
??????????????? "36,?37,?39,?45,?46);\n" ??????????????????????+
??????????? "function?isKeyValid?(keyCode)\n" ?????????????????+
??????????? "{\n" ?????????????????????????????????????????????+
??????????? " ???return?((keyCode?>=?48?&&?keyCode?<=?57)?││ " +
??????????????? "isAuxKey?(keyCode));\n" ??????????????????????+
??????????? "}\n" ?????????????????????????????????????????????+
??????????? "function?isAuxKey?(keyCode)\n" ???????????????????+
??????????? "{\n" ?????????????????????????????????????????????+
??????????? " ???for?(i=0;?i<keys.length;?i++)\n" ?????????????+
??????????? " ???????if?(keyCode?==?keys[i])\n" ???????????????+
??????????? " ???????????return?true;\n" ??????????????????????+
??????????? " ???return?false;\n" ?????????????????????????????+
??????????? "}\n" ?????????????????????????????????????????????+
??????????? "-->\n" ???????????????????????????????????????????+
??????????? "</script>";

???????string?NetscapeClientScriptBlock?=
??????????? "<script?language=\"javascript\">\n" ??????????????+
??????????? "<!--\n" ??????????????????????????????????????????+
??????????? "function?isKeyValid?(keyCode)\n" ?????????????????+
??????????? "{\n" ?????????????????????????????????????????????+
??????????? " ???return?((keyCode?>=?48?&&?keyCode?<=?57)?││ " +
??????????????? "keyCode?==?8?││?keyCode?==?13);\n" ???????????+
??????????? "}\n" ?????????????????????????????????????????????+
??????????? "-->\n" ???????????????????????????????????????????+
??????????? "</script>";
????????public?string?Text
????????{
????????????get?{?return?MyText;?}
????????????set
????????????{
????????????????//?Make?sure?value?is?numeric?before?storing?it
????????????????Convert.ToInt64?(value);
????????????????MyText?=?value;
????????????}
????????}

????????protected?override?void?OnPreRender?(EventArgs?e)
????????{
????????????string?browser?=?Context.Request.Browser.Type.ToUpper?();
????????????int?version?=?Context.Request.Browser.MajorVersion;

????????????if?(browser.IndexOf?("IE")?>?-1?&&?version?>=?4)
????????????????Page.RegisterClientScriptBlock?("NumTextBoxScript",
????????????????????IEClientScriptBlock);
????????????else?if?(browser.IndexOf?("NETSCAPE")?>?-1?&&?version?>=?4)
????????????????Page.RegisterClientScriptBlock?("NumTextBoxScript",
????????????????????NetscapeClientScriptBlock);
????????}

????????protected?override?void?Render?(HtmlTextWriter?writer)
????????{
????????????string?browser?=?Context.Request.Browser.Type.ToUpper?();
????????????int?version?=?Context.Request.Browser.MajorVersion;

????????????writer.WriteBeginTag?("input");
????????????writer.WriteAttribute?("type", "text");
????????????writer.WriteAttribute?("name",?UniqueID);

????????????if?(ID?!=?null)
????????????????writer.WriteAttribute?("id",?ClientID);

????????????if?(Text.Length?>?0)
????????????????writer.WriteAttribute?("value",?Text);

????????????if?(browser.IndexOf?("IE")?>?-1?&&?version?>=?4)
????????????????writer.WriteAttribute?("onkeydown",
??????????????????? "javascript:return?isKeyValid?(window.event.keyCode)");
????????????else?if?(browser.IndexOf?("NETSCAPE")?>?-1?&&?version?>=?4)
????????????????writer.WriteAttribute?("onkeydown",
??????????????????? "javascript:return?isKeyValid?(event.which)");

????????????writer.Write?(HtmlTextWriter.TagRightChar);
????????}
????}
Figure 8-28
NumTextBox control.