InformIT

Client-Side Script Integration in ASP.NET

Date: May 28, 2004

Sample Chapter is provided courtesy of Sams.

Return to the article

This chapter takes a look at the major client-side script issues that affect you when you create ASP.NET pages, as well as when you create reusable content such as user controls and server controls. It reinforces some of the basic techniques and demonstrates useful ways that even very simple script can solve common issues you come up against when building ASP.NET Web applications.

In This Chapter

Client-Side Interaction on the Web

Useful Client-Side Scripting Techniques

Summary

ASP.NET provides plenty of clever server-side controls that ultimately generate HTML elements in the browser. However, with the notable exception of the validation controls and one or two other features, that's really all they do. In fact, when they start to build Web applications, most developers who are used to building Windows applications find that the interface features Web developers have become accustomed to using are quite poor.

We can't do much about the actual client-side HTML elements and controls that are available because that's the whole nature of the Web. Content is supposed to be universally supported in all browsers, and browsers are supposed to follow accepted standards. Therefore, if your application needs some fancy new kind of multistate psychedelic flashing button, you're going to have to find a way to build it yourself. And depending on how you implement it, you might then have to find a way to persuade all the people who use your site to download this great new control and install it on their machine.

Avoiding Meaningless and Annoying Content

In reality, most people have seen enough in the way of annoying Java applets, malicious ActiveX controls, time-wasting Flash animations, and pointless Shockwave effects. They expect an application to do what it says on the box by being intuitive and easy to understand and working seamlessly and as fast as possible, given the nature of Internet connections.

Client-side scripting has been a feature of Web development for almost as long as the Web in its current incarnation has been around. Scripting provides an increasing number of useful features that you can take advantage of to make Web applications appear more seamless, responsive, and interactive, while still running well in almost all popular browsers in use today.

This book is about ASP.NET and not client-side scripting, but, in fact, the two are no longer really divisible. ASP.NET generates client-side script in varying quantities, depending on the server controls you place on a page. Even simple effects such as auto-postback depend on some client-side script.

And you saw client-side script being used in the ComboBox control you created in Chapter 5, "Creating Reusable Content."

This chapter takes a look at the major client-side script issues that affect you when you create ASP.NET pages, as well as when you create reusable content such as user controls and server controls. This is by no means a reference work on client-side scripting, but it reinforces some of the basic techniques and demonstrates useful ways that even very simple script can solve common issues you come up against when building ASP.NET Web applications.

Client-Side Interaction on the Web

Client-side interaction is hard to achieve because of the disconnected nature of HTTP and the way that browsers and Web servers work. Information is passed to and from the client only during distinct phases of the Web-surfing process. The server builds the page and sends it to the browser, and the browser submits the page back to the server when it's ready for another one.

Okay, so there are some well-known ways that you can get around this issue, usually by installing a component in the browser that can send and receive HTTP requests without having to reload the current page. The XMLHTTP component within the MSXML parser in Internet Explorer 5 and above is a good example. You can also use Macromedia Flash and a range of third-party plug-ins or components for other browsers. However, the point is that if you want a page to be interactive to the extent that it "does stuff" while loaded into the browser, you need to find a way to execute code within the confines of the browser.

When you're building items of reusable content, as demonstrated in Chapter 5, client-side scripting allows you to push the envelope beyond the simple flow layout of HTML controls to provide extra features that are often seen in traditional executable applications. The following sections explore the fundamental aspects of where, when, and how—and then move on to look at some useful techniques that integrate client-side and server-side programming and provide examples you can use in your own pages.

Client-Side Scripting in the Browser

Client-side scripting has been supported in the mainline Web browsers since Netscape Navigator 2 and Internet Explorer 3. These browsers, and many others, support the simple HTML Document Object Model (DOM) by exposing specific elements to script that runs within the browser. Such elements include frames, forms, controls (such as <input> and <select>), images, links, and anchors (<a> elements with name="..." rather than href="..."). Script can also access the fundamental objects such as the current window and the document within a frame or a window.

This level of accessibility to the page content allows the traditional effects such as reading and setting the values of controls, submitting a form, or swapping images in an <img> element. It also supports a small set of useful events, such as detecting when a control gets or loses the focus or receives a click (via keyboard or mouse). However, this basic level of support for scripting does not offer the three main features that you often need when building better controls or interactive content:

CSS2 and Dynamic HTML

While much has been made of the "browser wars" over the past few years, the situation today regarding the use of client-side scripting is actually a lot more favorable than it was. Microsoft and Netscape added a feature set they called Dynamic HTML to their version 4 browsers, although the blatant incompatibility between them (and the resulting outcry from Web developers and standards bodies alike) was perhaps one of the key factors in the evolution of more comprehensive client-side standards over the following years.

Today we have Cascading Style Sheets (CSS) at version 2, HTML at version 4, and XHTML at version 1.0; together, they provide not only a comprehensive display model based on the original CSS recommendation but also a standard set of methods for accessing and manipulating document content from script or code running on the client. While these recommendations are fundamentally similar to the original Microsoft implementation in Internet Explorer 4, there are subtle differences. However, the mainline manufacturers all have "version 6" browsers available that generally do meet the basic CSS2, HTML4, and XHTML recommendations. These include the following:

CSS2 Support in Version 6 Browsers

In reality, some of the more esoteric features of CSS2 are not fully supported in all version 6 browsers or are less than totally compatible across the different version 6 browsers. However, the basic techniques that we take advantage of in our examples do work in all the current version 6 browsers.

Selecting Your Target

Are most users out there using a version 6 browser? Admittedly, our own Web site is mainly aimed at developers working with the latest Microsoft technologies, so the results we see are probably not representative of the population, but around 75% of our visitors are using Internet Explorer 5 or higher, Netscape/Mozilla 6 or higher, and Opera 6 or higher. Looking at the stats available on other sites, the percentage of visitors using these newer browsers varies from something over 55% to almost 90%.

Why Use the Latest Browser?

You probably wouldn't want to risk driving on an icy freeway during rush hour in a 1910 Model T Ford. Four-inch-wide tires, vague steering, and a distinct lack of braking performance when compared to those in modern vehicles, would make this a risky undertaking at the best of times. Likewise, using an old and unsupported browser is an equally foolhardy adventure these days, with the proliferation of malicious scripts, annoying Java applets, and downright dangerous ActiveX controls that are out there on the Web and being delivered daily in junk email messages. Most car drivers appreciate the added safety of antilock brakes, airbags, and seatbelts, and the sensible browser user does the same by choosing the latest browser so that he or she can stay secure with the updates and patches provided for it.

It's probably reasonable to assume that you can take advantage of CSS2 and HTML4 features to add client-side interactivity to your pages, without affecting the majority of users. Of course, that doesn't mean you can ignore the rest because there are issues such as providing accessibility to users of text-only browsers, page readers, and other devices aimed at specialist markets or disabled users.

The language of choice for client-side programming is, of course, JavaScript—because only Internet Explorer can natively support VBScript. There are several versions of JavaScript available, but the "vanilla" version 1.x satisfies almost all requirements for the simple client-side interactivity you need when building most user controls and server controls. And because Internet Explorer actually has its own JScript/ECMAScript interpreter rather than a real JavaScript one, staying with the features in JavaScript 1.0 or 1.1 provides the best compatibility option.

Version 6 Browser-Compatible Code Techniques

Given the three tasks listed earlier in this chapter that you most commonly need to accomplish in client-side script—access to all elements, access to keypress information, and dynamic positioning of elements—the following sections look at how these can be achieved in modern browsers using script.

Accessing Elements Within a Page

Internet Explorer 4 was the first mainstream browser to provide full access to all the elements in a page by exposing them from the document object as a collection called all. It also allowed selection of a set of elements by type, via the use of the getElementsByTagname method. While CSS2 provides the same getElementsByTagname method, it replaces the document.all collection with two methods named getElementById and getElementByName. Because ASP.NET sets the id and name attributes of an element that is created by a server control to the same value (with the exception of the <input type="radio"> element), the getElementById and getElementByName methods generally provide the same result.

Therefore, the technique for getting a reference to an element within client-side script depends on whether you are only going to send the page to a CSS2-compliant client or whether you want the code to adapt to different client types automatically. The accepted technique for providing adaptive script in a page is to test for specific features that identify the browser type or the support it provides for CSS2. These features are summarized in Table 6.1.

Table 6.1 Features You Can Use to Detect the Browser Type or Its Feature Support

Feature

Description

document.all collection

Supported by Internet Explorer 4.0 and above

document.layers collection

Supported by Netscape Navigator 4.x only

getElementById method

Supported by CSS2-compliant browsers


Using the ASP.NET BrowserCapabilities Object

You can use the ASP.NET BrowserCapabilities object to sniff the browser type and deliver the appropriate page or include the appropriate script or controls. Chapter 7, "Design Issues for User Controls," and Chapter 8, "Building Adaptive Server Controls," demonstrate this approach.

By using the features described in Table 6.1, you can write code such as that shown in Listing 6.1 to execute different sections of script, depending on which browser loads the page. Notice that this causes Internet Explorer 5.x to execute the CSS2-compliant code. If you find that this does not perform correctly with your specific client-side scripts, you can change the tests so as to place Internet Explorer versions 4.x and 5.x into the same section by checking the value of the navigator.appName and navigator.appVersion properties as well.

Listing 6.1—Detecting the Client's Feature Support in Script Code

if (document.getElementById) {
  ... code for CSS2-compliant browsers here ...
} 
else if (document.all) {
 ... code for IE 4.x here ...
}
else if (document.layers) {
 ... code for Netscape Navigator 4.x here ...
}
else {
 ... code for older browsers here ...
}

However, as discussed earlier, the number of users still running Navigator 4.x and Internet Explorer 4.x is extremely low, so you generally need to test only for CSS2 support and provide fallback for all other browsers. There's not a lot of point in spending long development times on supporting browsers that only 1% of users may still be running.

Accessing Keypress Information

Microsoft's early implementation of Dynamic HTML exposed three keypress events for all the interactive elements on a page and for the document object itself. These are the keydown, keypress, and keyup events, and they occur in that order. The keypress event exposes the ANSI code of the key that was pressed, and the other two events expose a value that identifies the key itself (as located within the internal keyboard mappings) rather than the actual character.

Listing 6.2 shows the generally accepted technique for detecting a keypress that works in Internet Explorer version 4.x and higher and in CSS2-enabled browsers. If the event is exposed by the window object, as in Internet Explorer 4 and above, it is extracted from the keyCode property of the event object. In CSS2-compliant browsers, the event is passed to the function by the control to which the function is attached as a parameter, and it can be extracted from the which property.

Listing 6.2—Detecting a Keypress Event and the Code of the Key That Was Pressed

<element onkeypress="showKey(event);">
...
<script language="javascript">
<!--
 var iKeyCode = 0;
 if (window.event) 
  iKeyCode = window.event.keyCode
 else 
  if (e) 
   iKeyCode = e.which;
 window.status = iKeyCode.toString();
//-->
</script>

Dynamic and Absolute Element Positioning

The final feature set that you often need a browser to support when creating user controls and server controls is a way of positioning elements within and outside the usual flow of the page, changing that setting dynamically, and specifying the size of elements. Again, the original Microsoft Dynamic HTML approach has survived almost intact in CSS2, so these features are available in Internet Explorer 4.x and above, as well as in CSS2-compliant browsers. In more strict terms, the features that you are most likely to take advantage of are summarized in Table 6.2.

Table 6.2 Dynamic and Absolute Element Positioning Features

Feature

Description

Showing and hiding elements

Set the display selector of the style attribute to block, inline, or hidden. Other values can be used, but these three are most useful. The value block forces this element to start on a new line and following content to wrap to a new line. The value inline means that preceding and following content will be on the same line, unless that content forces a new line. The value hidden removes the element and all child elements from the page.

Absolute positioning

Set the position selector of the style attribute to absolute to fix an element using the top and left coordinates provided as the top and left style selectors. This removes the element from the flow layout of the page. The alternative is position:relative, which forces the element to follow the flow layout of the page but also allows it to act as a container within which child elements can be absolutely positioned. If no parent element contains position:absolute or position:relative, the current element is positioned with respect to the top left of the browser window.

Specifying the actual size of elements

Set the width and height selectors of the style attribute to fixed values. These values can be specified with units px (pixels), pt (points), in (inches), cm (centimeters), mm (millimeters), or pc (picas) or the typographical units em, en, and ex. The default is px.

Positioning and moving elements dynamically

The values for the display, position, top, left, width, and height selectors can be changed while the page is loaded, and the page will immediately reflect these changes by showing, hiding, or moving the element.


The Client-Side Code in the ComboBox User Control

To demonstrate the feature sets described so far in this chapter, let's briefly review some of the code from Chapter 5, "Creating Reusable Content." That chapter shows how easy it is to build a ComboBox user control for use in browsers that support CSS2 (see Figure 6.1).

Figure 1Figure 6.1 The customer ComboBox user control created in Chapter 5.

This control includes client-side code that manipulates the control elements and their values while the page is loaded into the browser, using most of the features just discussed. Listing 6.3 shows the complete client-side code section. In each of the three functions in Listing 6.3, you can see that you get a reference to the controls you want to manipulate by using the getElementById function that is exposed by the document object.

Listing 6.3—The Client-Side Script for the ComboBox User Control

<script language='javascript'>
function selectList(sCtrlID, sListID, sTextID) {
 var list = document.getElementById(sCtrlID + sListID);
 var text = document.getElementById(sCtrlID + sTextID);
 text.value = list.options[list.selectedIndex].text;
 if (sListID == 'dropbox') openList(sCtrlID);
}

function scrollList(sCtrlID, sListID, sTextID) {
 var list = document.getElementById(sCtrlID + sListID);
 var text = document.getElementById(sCtrlID + sTextID);
 var search = new String(text.value).toLowerCase();
 list.selectedIndex = -1;
 var items = list.options;
 var option = new String();
 for (i = 0; i < items.length; i++) {
  option = items[i].text.toLowerCase();
  if (option.substring(0, search.length) == search ) {
   list.selectedIndex = i;
   break;
  }
 }
}

function openList(sCtrlID) {
 var list = document.getElementById(sCtrlID + 'dropbox');
 var btnimg = document.getElementById(sCtrlID + 'dropbtn');
 if(list.style.display == 'none') {
  list.style.display = 'block';
  btnimg.src = document.getElementById(sCtrlID + 'imageup').src;
 }
 else {
  list.style.display = 'none';
  btnimg.src = document.getElementById(sCtrlID + 'imagedown').src;
 }
 return false;
}
</script>

Alternative Client Support Options

The code in Listing 6.3 doesn't provide support for non-CSS2 browsers. This is because the only ones that support another feature needed for this control (absolute positioning) are Internet Explorer 4.x and Netscape 4.x. Because the number of hits likely to be encountered from these two browsers is negligible, it doesn't seem worth supporting them.

However, extending support to Internet Explorer 4 isn't hard; you would just need to add the test for the document.all collection, as shown in Listing 6.4, and then access the elements by using this collection. The remaining code will work fine as it is.

Listing 6.4—Adapting the selectList Function to Work in Internet Explorer 4.x

function selectList(sCtrlID, sListID, sTextID) {
 var list;
 var text;
 if (document.all) {
  list = document.all[sCtrlID + sListID];
  text = document.all[sCtrlID + sTextID];
 }
 else {
  list = document.getElementById(sCtrlID + sListID);
  text = document.getElementById(sCtrlID + sTextID);
 }
 text.value = list.options[list.selectedIndex].text;
 if (sListID == 'dropbox') openList(sCtrlID);
}

Accessing the document.all Collection and the getElementID Method in JavaScript

Remember that document.all is a collection (array) of elements, so in JavaScript, you must use square brackets ([]) to access the members. On the other hand, getElementId uses ordinary parentheses (()) because it's a method, and you are providing the element ID as a parameter.

Keypress Events in the ComboBox Control

The scrollList function shown in Listing 6.3 continually selects the first matching value in the list while the user is typing in the text box section of the ComboBox. To work, it must be called every time a key is pressed so that it can search the list for the appropriate value (if one exists). To achieve this, you handle the onkeyup event, which runs when the user releases a key.

You attach the scrollList function to the input element that implements the text box by using server-side code (as shown in Chapter 5). When the page gets to the client, the HTML declaration of the text box (with the nonrelevant style information omitted) looks like this:

<input name="cboTest2:textbox2" type="text" id="cboTest2_textbox2"
    onkeyup="scrollList('cboTest2_', 'dropbox', 'textbox2')" />

You can see that a keyup event will pass the three required parameters to the scrollList function. However, you aren't actually interested in detecting which key was pressed because the function just compares the values within the text box and the list to figure out which entry to select. This means that you don't have to pass the event object (required to detect which key was pressed in Netscape and Mozilla browsers) as a parameter. In later examples, you'll see occasions where you do need to detect the actual key value.

Element Positioning in the ComboBox Control

The version of the ComboBox control that provides a drop-down list uses absolute positioning to fix the width of the enclosing <div> element, the width of the text box within it, and the position and size of the <select> list that implements the drop-down list part of the control. You can see in Listing 6.5 that the top of the list is positioned 25 pixels below the top of the text box and 20 pixels to the left of the text box. The widths of the text box and list are adjusted accordingly, depending on the width of the enclosing <div> element. All these values are calculated on the server and are used to create the style selectors shown in Listing 6.5.

Listing 6.5—The Style Selectors for Positioning the Text Box and List in the ComboBox User Control

<div id="cboTest1_dropdiv" Style="position:relative;width:150;">
 <input type="text" id="cboTest1_textbox2" ...
 style="vertical-align:middle;width:133;" />
 <input type="image" id="cboTest1_dropbtn" ... />
 <select size="5" id="cboTest1_dropbox" ... 
 style="display:none;position:absolute;left:20;top:25;width:130;">
  <option value="aardvark">aardvark</option>
  ...
  <option value="lynx">lynx</option>
 </select>
 <img id="cboTest1_imageup" style="display:none" ... />
 <img id="cboTest1_imagedown" style="display:none" ... />
</div>

Notice that the list has the selector display:none so that it's not visible in the page when it loads. Likewise, the two <img> elements that hold the up and down button images are not visible either. They are simply there to preload the images so that they can be instantly switched when the user opens and closes the list.

Showing and Hiding the List Control

The code in the openList function shown in Listing 6.3 has the job of showing and hiding the drop-down list when the user clicks the up/down button or makes a selection from the list. It's simply a matter of switching the display selector for the list between none and block, depending on whether the list is already open or closed. At the same time, you switch the button image. The relevant code section is shown in Listing 6.6.

Listing 6.6—Showing and Hiding the Drop-Down List Part of the ComboBox Control

if(list.style.display == 'none') {
 list.style.display = 'block';
 btnimg.src = document.getElementById(sCtrlID + 'imageup').src;
}
else {
 list.style.display = 'none';
 btnimg.src = document.getElementById(sCtrlID + 'imagedown').src;
}

Useful Client-Side Scripting Techniques

The following sections demonstrate some useful client-side scripting techniques. These techniques are some of the several that regularly crop up as questions on ASP.NET mailing lists and discussion forums:

The following sections start by examining the ways you can inject client-side confirmation dialogs into ASP.NET code and then look at how to trap keypresses and prevent a form from being submitted.

Buttons, Grids, and Client-Side Script

A common scenario with the excellent ASP.NET grid and list controls is to allow users to edit and delete rows inline—while they are displayed within a DataGrid or DataList control. The DataGrid control can provide attractive and interactive pages, with minimum code requirement from the developer. However, one feature that many people ask for is to be able to prompt users before carrying out some action such as deleting a row.

One way would be to trap the delete event on the server and generate a confirmation page to send back to the user. However, this is counterintuitive, inefficient, and breaks the flow of the application. The user will probably expect something like what is shown in Figure 6.2.

Figure 2Figure 6.2 Confirming a button click before allowing a row to be deleted.

In fact, this kind of feature is extremely easy to add to the DataGrid control and other controls. All you need is a simple client-side script function that pops up a JavaScript confirm dialog and returns true or false, depending on which button the user clicked. You then return that value to the control that raised the event—in this example, the Delete button. If you return true, the event is processed and the row is deleted. If you return false, the event is canceled and the row is not deleted.

Listing 6.7 shows the client-side function, named ConfirmDelete, that is used in this example. You can see that this function is extremely simple, taking just the product name as a parameter. It displays the confirmation message in a confirm dialog and returns the value from the dialog (which will, of course, be true if the user clicked OK or false if the user clicked Cancel). You declare this function in the <head> section of the page, although you could inject it into the page by using the RegisterClientScriptBlock method (as described in Chapter 5) if you prefer.

Listing 6.7—The Client-Side ConfirmDelete Function

<script language='javascript'>
<!--
function ConfirmDelete(sName) {
 var sMsg = 'Are you sure you want to delete "' + sName + '"?';
 return (confirm(sMsg));}
//-->
</script>

The Declaration of the DataGrid Control

The visible part of the sample page is made up of the <form> section shown in Listing 6.8, which contains the declaration of the DataGrid control. A lot of this code sets the appearance of the DataGrid, but the important point to note is that you assign server-side event handlers to the OnItemCommand and OnItemDataBound attributes.

You also add a TemplateColumn element to the grid and declare a Button control within it, giving it the CommandName value "Delete". This will appear as the first column of the grid, and because you haven't changed the AutoGenerateColumns property from its default of True, the DataGrid control will automatically generate bound display columns for all the columns in the source rows as well.

The ItemCommand event will normally be raised when the user clicks the Delete button (which submits the form), but the client-side function will prevent the form from being submitted by canceling the event on the client if the user clicks Cancel in the confirmation dialog.

Listing 6.8—The Declaration of the Form and DataGrid Control

<form id="frmMain" runat="server">

 <asp:DataGrid id="dgr1" runat="server"
    Font-Size="10" Font-Name="Tahoma,Arial,Helvetica,sans-serif"
    BorderStyle="None" BorderWidth="1px" BorderColor="#deba84"
    BackColor="#DEBA84" CellPadding="5" CellSpacing="1"
    DataKeyField="ProductID"
    OnItemCommand="DoItemCommand"
    OnItemDataBound="WireUpDeleteButton">
  <HeaderStyle Font-Bold="True" ForeColor="#ffffff" 
         BackColor="#b50055" />
  <ItemStyle BackColor="#FFF7E7" VerticalAlign="Top" />
  <AlternatingItemStyle backcolor="#ffffc0" />
  <Columns>
   <asp:TemplateColumn>
    <ItemTemplate>
     <asp:Button id="blnDelete" Text="Delete" 
        CommandName="Delete" runat="server" />
    </ItemTemplate>
   </asp:TemplateColumn>
  </Columns>
 </asp:DataGrid>

</form>

So how do you attach the client-side ConfirmDelete function to the Delete buttons in each row? You use the ItemDatabound event of the DataGrid control. This occurs for each row as it is bound to the data source to create the output shown in the page, and the code in Listing 6.8 specifies that the event handler named WireUpDeleteButton will be called each time this event is raised by the DataGrid control.

The WireUpDeleteButton Event Handler

Listing 6.9 shows the WireUpDeleteButton event handler, and you can see that the first task (as is usual when handling this event) is to make sure that you only process the correct type of row. You want to access the Delete button in every row where it occurs, so you must handle the event for both Item and AlternatingItem rows.

Item and AlternatingItem Rows

The ItemDataBound event is called for every row in the DataGrid control, including header, footer, separator, selected, and edit rows, as well as the item and alternating item rows that you want to process. Also bear in mind that, even if you don't specify an AlternatingItem template (or any styling information for the alternating rows), the event handler will identify alternate rows as being of AlternatingItem type, so you need to test for both Item and AlternatingItem row types.

Listing 6.9—The Code for the WireUpDeleteButton Event Handler

Sub WireUpDeleteButton(source As Object, e As DataGridItemEventArgs)

 ' make sure this is an Item or AlternatingItem row
 Dim oType As ListItemType = CType(e.Item.ItemType, ListItemType)
 If oType = ListItemType.Item _
 Or oType = ListItemType.AlternatingItem Then

  ' get ProductName value from this row
  Dim sName As String = e.Item.Cells(3).Text

  ' escape any single quotes
  sName = sName.Replace("'", "\'")

  ' get a reference to the Delete Button in this row
  Dim oCtrl As Button _
   = CType(e.Item.FindControl("blnDelete"), Button)

  ' attach the client-side onclick event handler
  oCtrl.Attributes.Add("onclick", _
   "return ConfirmDelete('" & sName & "');")

 End If

End Sub

When you find a suitable type of row, you get the text from the third cell in that row (the product name). The DataGrid control knows what the values that will be used to populate this row are when the ItemDataBound event occurs, even though it has not yet created the final markup that will appear in the page. Although in this case you extract the value from the output row, you could equally well query the row in the data source to which it is bound by using the following:

Dim sName As String = e.Item.DataItem("ProductID")

After you've extracted the product name, you must escape any single quotes it might contain. Otherwise, you'll get an error when you try to use the value in your client-side JavaScript function because a single quote will be treated as a string-termination character.

Then you can get a reference to the Delete button by using the FindControl method and attach the client-side function to the client-side click event by specifying it as the onclick attribute. Notice that you insert the product name from this row into the attribute to create the function parameter, and you include the return keyword so that the value of the function will be returned to the button control in the browser.

If you view the source of the page in the browser, you'll see the output that ASP.NET actually creates for each row, as in this example:

<input type="submit" value="Delete" ... 
    onclick="return ConfirmDelete('Vegie-spread');" />

An Easy Way to Use Client-Side Dialogs

Chapter 7, "Design Issues for User Controls," describes some useful techniques for including client-side dialogs in applications. The chapter describes a user control that makes it easy to attach client-side script dialogs to elements in your ASP.NET pages.

Now, if the user clicks the Delete button and then clicks Cancel in the confirmation dialog, the function returns false and the click event is not processed. The result is that the page is not submitted, so the row is not deleted.

This chapter doesn't list the code that creates the DataReader instance, performs the data binding to the DataGrid control, or deletes the row when the Delete button is clicked. All this is conventional and is just the same as you would normally use to fill a DataGrid control and process user interaction. You can view all the code for this example by using the [view source] link at the bottom of the example page.

Detecting and Trapping Keypress Events

Web browsers, by default, allow the user to submit a form by pressing the Return key—even when the input focus is on another control. If there is more than one <form> section on a page, the browser should submit the one containing the element that currently has the focus. In fact, each browser behaves slightly differently:

Generally, the default behavior of all the browsers is fine. However, ASP.NET imposes a limitation on Web page structure in that there can be only one server-side <form> section. In other words, only one <form runat="server"> control can be placed on a page.

Multiple Forms on Pages That Use the ASP.NET Mobile Control

The limitation of a single form doesn't apply to pages that inherit from MobilePage and that are designed for use in small-screen devices such as cellular phones. These devices usually require pages that contain more than one <form> section to create the individual screens (called cards) that the device will display. (The set of cards is, not surprisingly, called a deck.)

So there are really two issues here. You might want to trap the Return key so that it doesn't submit the form (or trap some other key so that it does not produce a character or carry out some other action). Alternatively, you might have more than one submit button on a form, perhaps because you want to offer the user more than one option when submitting the form. If you allow the Return key to be processed, the effect will always be that of the user clicking the first submit button on the form.

Listing 6.10 shows the code to detect a keypress event and discover which key was pressed. Notice that you enclose the key detection code in a function that accepts both a reference to the event and a key code value. If the user presses a key that generates a key code equal to the specified code, you return the value false from the function. Otherwise, you return the value true.

Listing 6.10—A Function to Detect the Keypress Code and Return true or false

function trapKeypress(e, theKey) {
 var iKeyCode = 0;
 if (window.event) iKeyCode = window.event.keyCode
 else if (e) iKeyCode = e.which;
 return (iKeyCode != theKey);
}

You can attach the trapKeypress function to any control that exposes keypress events (keydown, keypress, or keyup). The important point is that you must return the value from the function to the element that raised the event, as in this example:

<element onkeypress="return trapKeypress(event, 13);">

Now the browser will ignore the keypress (in this example, the Return key with ANSI code 13) if the trapKeypress function returns false or process it as usual if the function returns true. Therefore, you can prevent the Return key from being processed by a control by attaching the trapKeypress function to that control (or to more than one control). To trap a different key, or more than one key, you would just have to pass the appropriate key code(s) to the function.

Some Keypress Events Cannot Be Canceled

For security reasons, you cannot trap and cancel keypresses that initiate system events. Although you can detect the keypress event and extract the key code, you cannot prevent key combinations that open menus or close the browser.

It's also possible to detect the state of the Ctrl, Shift, and Alt keys within a keypress event. The event object passed to the event handler for CSS2-compliant browsers exposes three Boolean properties named altKey, ctrlKey, and shiftKey that are true if the corresponding key was pressed when the event occurred. Internet Explorer 6.0 extends this by adding three more properties that allow you to tell if it was the Alt, Ctrl, or Shift key on the left side of the keyboard: altLeft, ctrlLeft, and shiftLeft. You'll see these properties in use in the next example.

Discovering the Key Codes You Need

As mentioned earlier, the key code returned from the keypress event is different from the key code returned from the keydown and keyup events for non-alphanumeric keys. To help you discover the key code you want, we've included a simple page within the examples for this book that displays the key codes for each event and the states of the Ctrl, Shift, and Alt keys.

Figure 6.3 shows that the keydown and keyup events always return the key code 65 for the A key, regardless of whether the Shift key is pressed as well; the keypress event returns the correct ANSI codes for both uppercase and lowercase letters.

Figure 3Figure 6.3 A sample page that displays key mappings and keypress information.

The relevant sections of the code in this page are shown in Listing 6.11, which demonstrates how you can collect the states of the Ctrl, Shift, and Alt keys as well as the actual key code.

Listing 6.11—The Code for the Key Mappings Sample Page

<form>
 <input type="text" size="40" id="txtTest"
 value="Put cursor here and press a key"
 onkeydown="showKeycode(event, 'keydown');"
 onkeypress="showKeycode(event, 'keypress');"
 onkeyup="showKeycode(event, 'keyup');" />
 <p />
 <div id="divResult"></div>
</form>
...
...
function showKeycode(e, sEvent) {
 var iKeyCode = 0;
 if (window.event) iKeyCode = window.event.keyCode
 else if (e) iKeyCode = e.which;
 var theDiv = document.getElementById('divResult');
 var theTextbox = document.getElementById('txtTest');
 if (sEvent == 'keydown') {
  theDiv.innerHTML = '';
  theTextbox.value = '';
 }
 theDiv.innerHTML += sEvent + ' event - key code is: '
          + iKeyCode.toString()
 if (e.altKey == true)
  if (e.altLeft == true) 
   theDiv.innerHTML += ', the Left ALT key was pressed'
  else theDiv.innerHTML += ', the ALT key was pressed';
 if (e.ctrlKey == true)
  if (e.ctrlLeft == true) 
   theDiv.innerHTML += ', the Left CTRL key was pressed'
  else theDiv.innerHTML += ', the CTRL key was pressed';
 if (e.shiftKey == true)
  if (e.shiftLeft == true) 
   theDiv.innerHTML += ', the Left SHIFT key was pressed'
  else theDiv.innerHTML += ', the SHIFT key was pressed';
 theDiv.innerHTML += '<br />';
}

The <form> section of Listing 6.11 contains just the text box and the <div> element that displays the results. All three keypress events are wired up to a function named showKeycode, and they pass to this function a reference to the event object, together with the event name as a string to use to create the output seen in the page. Because you don't intend to cancel any keypresses, you don't return the value of the function to the control.

The next section of code in Listing 6.11 shows the function (showKeycode) that handles the three keypress events and displays the values you see in the page. If the event name is keydown, code in the showKeycode function first removes any existing content (generated by previous keypresses) from the text box and from the <div> element that displays the results. Then the output is generated, using the key code detected at the start of the function, and by appending the settings of the Ctrl, Shift and Alt keys.

Notice how the code sets the Internet Explorer extension properties for the left-hand keys to true, as well as the CSS2 standard properties, so you have to do a quick check to see what output to generate for each one. Browsers other than Internet Explorer will return false for the left-hand key properties that don't exist, so the else part of the if construct is executed in that case.

Trapping the Return Key in a Form

We need to look at one more detail of trapping keypress events. You've seen how to trap a keypress for a control, but often you'll have several controls on a form, so you'll need to attach the function to each one. However, by default, events bubble up through the control hierarchy of the page. This means that you can handle a keypress event for the containing element as well as at the individual control level. Obviously, the form itself is a container, so it's a good place to trap keypress events. You could also trap them at the page level by attaching the function directly to the opening <body> tag.

The sample page, shown in Figure 6.4, contains a single server-side <form> element that contains a selection of controls, including two submit buttons. The effect of the <hr /> element used to separate the two sets of controls makes it look like there are two separate forms, but of course this isn't possible in an ASP.NET page. If you experiment with this page, you'll discover that you cannot submit the form by pressing the Return key.

Figure 4Figure 6.4 The sample page that traps the Return key.

Setting the Tab Order of Controls

You should consider including the TabIndex attribute on form elements, especially where you want to trap keypress events. This allows you to control the order in which the input focus moves from one control to another when the Tab key is pressed, and providing a logical sequence makes it easier to work with complex forms. You set the TabIndex attribute when declaring server controls in ASP.NET, or you can set the TabIndex property dynamically at runtime, to an Integer value that denotes the index of the control within the tab order of the page:

<asp:TextBox runat="server" TabIndex="3" />

For non-server controls, you just add a TabIndex attribute in the usual way:

<input type="text" tabindex="3" />

The client-side code used in this page (see Listing 6.12) is basically the same as the code in Listing 6.10. However, notice that this time you declare a page-level variable named blnReturn, and you use the keypress event to set its value to false if the user pressed the Return key or true otherwise.

Listing 6.12—The Client-Side Code to Trap and Store the Keypress Information

<script language="javascript">
<!--

var blnReturn = true;

function trapReturn(e) {
 var iKeyCode = 0;
 if (window.event) iKeyCode = window.event.keyCode
 else if (e) iKeyCode = e.which;
 blnReturn = (iKeyCode != 13);
}

//-->
</script>

Storing the Key Code Test Result

The reason for using a separate variable to store the result of the key code test is that you need to handle the submit event of the form and return the value false from the onsubmit event handler if you want to prevent the form from being submitted. You can't perform the key code test in the onsubmit event handler because the keypress information is not available when this event is raised. Instead, you capture the result of the test in the keypress event and store it in a variable, as shown in Listing 6.12, when the keypress event occurs.

Effectively, the blnReturn variable reflects the validity of the last keypress, and you can then use the value of this variable in the submit event of the form. If the last keypress was the Return key, blnReturn is false and the form is not submitted. Listing 6.13 shows the HTML declarations for the page in Figure 6.4, and you can see the two event handler attributes attached to the opening <form> tag.

Listing 6.13—The Declaration of the HTML <form> Section Within the Sample Page

<form id="frmMain" runat="server"
   onkeydown="trapReturn(event);" 
   onsubmit="return blnReturn;">

 <asp:TextBox id="txtTest1" Text="Some value" runat="server" />
 <p />
 <asp:CheckBox id="chkTest1" AutoPostback="True"
    Text="AutoPostback Checkbox" OnCheckedChanged="ButtonClick"
    runat="server" /><br />
 <asp:CheckBox id="chkTest2" Text="Normal CheckBox" runat="server" />
 <p />
 <asp:Button id="btnOne" CommandName="Button 1"
    Text="Submit Button 1" runat="server" OnClick="ButtonClick" />

<hr />

 <asp:TextBox id="txtTest2" Text="Another value" runat="server" />
 <p />
 <asp:RadioButton id="optTest1" GroupName="grp1"
    AutoPostback="True" Text="AutoPostback Radio Button"
    OnCheckedChanged="ButtonClick" runat="server" /><br />
 <asp:RadioButton id="optTest2" GroupName="grp1" Checked="True"
    Text="Normal RadioButton" runat="server" />
 <p />
 <asp:Button id="btnTwo" CommandName="Button 2"
    Text="Submit Button 2" OnClick="ButtonClick" runat="server" />
 <p />
 <asp:Label id="lblMsg" EnableViewState="False" runat="server" />

</form>

Listing 6.13 also shows the declarations of the other controls placed on the form, as well as the two submit buttons. None of these controls require any client-side event handlers because the keypress events will bubble up to the <form> element and be trapped there. However, you need server-side event handler declarations for some of the controls so that they call the ASP.NET routine named ButtonClick if they are used to initiate a postback.

The ButtonClick event handler is shown in Listing 6.14. You can see that all it does is display the current time (so that you can easily tell whether the form was submitted) and the text of the control that caused the postback.

Listing 6.14—The Server-Side Code That Displays Information when the Page Is Submitted

<script runat="server">

Sub ButtonClick(sender As Object, e As EventArgs)

 ' display time page was last submitted
 lblMsg.Text = "Page submitted at " _
       & DateTime.Now.ToString("hh:mm:ss") _
       & " by " & sender.Text

End Sub

</script>

Creating a MaskedEdit Control

As well as the ComboBox control described in Chapter 5, "Creating Reusable Content," there is at least one other control missing from the standard set provided by Web browsers—a MaskedEdit control. This is really just a text box that allows only specific characters to be entered, depending on the mask (that is, the character-by-character definition of the string that is acceptable).

Let's look at a simple example of a MaskedEdit control that demonstrates some useful techniques you can adapt to your own applications. You'll convert it into a user control and a custom server control in later chapters, but for now, you should just look at the actual control implementation.

One other feature of the sample control is interesting. You can see in Figure 6.5 that the text box displays the mask as a series of light gray underscores and literal characters (such as the hyphens between the number groups in this case). You'll see how this is achieved after you look at the rest of the code in the page.

Figure 5Figure 6.5 The MaskedEdit control sample page in action.

Trapping and Handling the Keypress Events

You've seen techniques for trapping keypress events and extracting the key code information in previous examples in this chapter. The MaskedEdit control obviously uses much the same techniques to catch each keypress and figure out whether the character the user typed is valid for the current location in the text box (in other words, whether it matches the mask).

The client-side code section of the sample page comprises four functions and some page-level variable declarations. These are the functions:

The mask can contain only the characters shown in Table 6.3.

Table 6.3 The Characters That Can Be Used to Define the Mask for the MaskedEdit Control

Character

Allows Only...

a

Lowercase or uppercase letters, or the numbers 0 to 9.

A

Uppercase letters or the numbers 0 to 9.

l

Lowercase or uppercase letters, but not numbers.

L

Uppercase letters, but not numbers.

n

Only the numbers 0 to 9.

?

Any printable character.


Listing 6.15 shows the page-level variables. You can see the string that contains the mask characters and a string you use to define alphabetic characters. This second string is also used to extract the ANSI/Unicode character code and to convert letters to uppercase. The bStarting variable is used by the doKeyUp function to force it to check whether there are any literal characters at the start of the mask, which it must insert into the text box when it first gets the focus.

Listing 6.15—The Page-Level Variables and the Handler for the keydown Event

var sMaskSet = 'aAlLn?'
var sUAscii = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
var bStarting = true;

function doKeyDown(e, textbox, sMask) {
 // trap and cancel keys that are not appropriate
 var iKeyCode = 0;  // collect key code
 if (window.event) iKeyCode = window.event.keyCode;
 else if (e) iKeyCode = e.which;
 if (iKeyCode == 32 || iKeyCode == 39 || iKeyCode == 35
 || iKeyCode == 8 || iKeyCode == 9)
  return true;    // space left end backspace tab
 if (iKeyCode < 47)  // non-printable character
  return false;
}

Handling the keydown Event

Listing 6.15 shows the doKeyDown function, which returns false if the key code represents one of the nonprintable values that cannot be accepted. This forces the text box to ignore that keypress event.

Handling the keypress Event

Listing 6.16 shows the doKeyPress event handler, which is executed next if the doKeyDown function returned true. After clearing the status bar, you extract the key code and then see whether the end of the mask has already been reached. If it has, the only keypress you can accept is the Backspace key (code 8). Otherwise, you return false to cancel the keypress and leave the text box value as it already stands.

Listing 6.16—The Client-Side Handler for the keypress Event

function doKeyPress(e, textbox, sMask) {

 window.status = '';
 var iKeyCode = 0;  // collect key code
 if (window.event) iKeyCode = window.event.keyCode;
 else if (e) iKeyCode = e.which;

 // check if mask already filled, and not backspace
 var iLength = textbox.value.length;
 if ((iLength == sMask.length) && (iKeyCode != 8))
  return false;

 // get mask character for this position in textbox
 var sMaskChar = sMask.charAt(iLength);

 // see if it's a special character
 if (sMaskSet.indexOf(sMaskChar) > -1) {

  // masked character required
  switch (sMaskChar) {

   case 'a':  // any alphanumeric character
    if ((iKeyCode > 47 && iKeyCode < 58)
    || (iKeyCode > 64 && iKeyCode < 91)
    || (iKeyCode > 96 && iKeyCode < 123))
     return true
    else return false;

   case 'A':  // uppercase alphanumeric character
    if ((iKeyCode > 47 && iKeyCode < 58)
    || (iKeyCode > 64 && iKeyCode < 91))
     return true
    else if (iKeyCode > 96 && iKeyCode < 123) {
     textbox.value += sUAscii.charAt(iKeyCode - 97);
     return false;
    }
    else return false;

   case 'l':  // any letter
    if ((iKeyCode > 64 && iKeyCode < 91)
    || (iKeyCode > 96 && iKeyCode < 123))
     return true
    else
     return false;

   case 'L':  // uppercase letter
    if (iKeyCode > 64 && iKeyCode < 91)
     return true
    else if (iKeyCode > 96 && iKeyCode < 123) {
     textbox.value += sUAscii.charAt(iKeyCode - 97);
     return false;
    }
    else return false;

   case 'n':  // any numeric character
    if (iKeyCode > 47 && iKeyCode < 58)
     return true
    else return false;
   case '?':  // any character
    return true;

   default: return false;
  }
 }
 else
  return true;
}

If the mask is not yet filled, you then examine it to see whether the character the user typed matches the mask. This code uses a rather long switch statement, mainly to make it easy to see how it works. You might prefer to create smaller functions or more compact code for your own implementation. You return true if the key matches the mask or false to cancel the event if the key does not match the mask. Notice that lowercase letters are automatically converted to uppercase where the mask value is 'A' or 'L'.

Handling the keyup Event

After the doKeyPress event has been processed, the keyup event is raised, and you handle it with the doKeyUp function shown in Listing 6.17. If the bStarting variable is false, you know that the user has typed something into the text box, so you again extract the key code from the event passed to the function.

Listing 6.17—The Client-Side Handler for the keyup Event

function doKeyUp(e, textbox, sMask) {
 if (bStarting != true) {
   var iKeyCode = 0;  // collect key code
   if (window.event) iKeyCode = window.event.keyCode;
   else if (e) iKeyCode = e.which;
   if (iKeyCode < 47 && iKeyCode != 32) return;
 }
 // check if next mask characters are literals
 // and add to text box if they are
 while ((textbox.value.length < sMask.length) &&
 (sMaskSet.indexOf(sMask.charAt(textbox.value.length)) == -1)) {
  textbox.value += sMask.charAt(textbox.value.length);
 }
 var sNext;
 if (textbox.value.length == sMask.length)
  sNext = 'Complete'
 else
  switch (sMask.charAt(textbox.value.length)) {
   case 'a':
    sNext = 'Expecting any alphanumeric character (0-9,A-Z,a-z)';
    break;
   case 'A':
    sNext = 'Expecting an uppercase alphanumeric char (0-9,A-Z)';
    break;
   case 'l':
    sNext = 'Expecting any letter (A-Z, a-z)';
    break;
   case 'L':
    sNext = 'Expecting an uppercase letter (A-Z)';
    break;
   case 'n':
    sNext = 'Expecting any numeric character (0-9)';
    break;
   case '?':
    sNext = 'Expecting any character';
    break;
   default: sNext = '';
  }
 window.status = sNext;
}

Then, using a while construct, you add to the text box any literal characters that appear at the start of the mask. The two conditions that must be met for the while loop to execute are that the length of the mask must be greater than the length of the text in the text box and the current character must not be one of the special mask characters which indicate that the user must enter a value.

So the code first compares the length of the mask with the length of the text in the text box to make sure that execution of the while loop stops at the end of the mask. Then it checks whether the current character in the value in the text box is also present in the string that contains the valid mask characters (sMask). If it is, this means that it is one of the special placeholders that indicate the kind of value that the user must enter, so the while loop just moves to the next character. If it is not a valid mask character, then it must be a literal character, so it is added to the string value in the text box.

After this, you can create the prompt indicating the next character type that is expected and display that in the browser's status bar.

Meanwhile, the variable bStarting will be true if the user hasn't entered anything into the text box yet (in other words, if this is the first keypress event). At this point, you want to insert any literal characters that appear at the start of the mask string, and you achieve this by handling the focus event for the text box, as shown in the next section.

Handling the focus Event

Listing 6.18 shows the function that is executed when the text box gets the focus. You simply set the bStarting value to true, call the onKeyUp function, and then set bStarting back to false again. This causes the doKeyUp function to add any literal characters and display the prompt in the status bar, but without attempting to extract the key code first.

Listing 6.18—The Client-Side Event Handler for the focus Event

function doFocus(e, textbox, sMask) {
 bStarting = true;
 doKeyUp(e, textbox, sMask);
 bStarting = false;
}

Together, the four functions doKeyDown, doKeyPress, doKeyUp, and doFocus implement the complete MaskedEdit control feature. There are some limitations, due mainly to the fact that the browser security model prevents canceling of some keypress and other events, and the text box control in the browser does not offer all the features of controls you might be used to in, for example, a Windows Forms or executable application. One particular issue is that users can click on the text box to reposition the input cursor, thereby breaking the mask code.

Validating the Value the User Enters

Although the MaskedEdit control works reasonably well, you might decide to add an ASP.NET RegularExpressionValidator control to the page as well to ensure that the input actually does match the mask when submitted. This would also have the advantage of validating the value on the server side after the page is submitted—something you should always do to prevent the server from being spoofed by the user creating a dummy page that contains invalid values.

Using the MaskedEdit Control

Listing 6.19 shows the HTML declarations of the controls in the page shown in Figure 6.5. The two drop-down lists are populated with the four sample mask strings and three text sizes, from which you can select to experiment with the control. The MaskedEdit control is declared as an ordinary ASP.NET TextBox control, and you'll add the event handlers that perform the magic to it in the server-side Page_Load event later in this chapter.

Listing 6.19—The HTML Declarations in the MaskedEdit Control Sample Page

<form id="frmMain" runat="server">

 <asp:DropDownList id="selMask" AutoPostback="True" runat="server">
  <asp:ListItem Value="nnnn-nn-nnTnn:nn:nn" Text="UTC Date and Time" />
  <asp:ListItem Value="Qnnnnn-LLnn" Text="Part Number" />
  <asp:ListItem Value="(nnn)-nnn-nnnn" Text="US Phone Number" />
  <asp:ListItem Value="LLn? nLL" Text="UK Postal Code" />
 </asp:DropDownList>
 <asp:DropDownList id="selSize" AutoPostback="True" runat="server">
  <asp:ListItem Value="10" Text="10 pt" />
  <asp:ListItem Value="12" Text="12 pt" />
  <asp:ListItem Value="16" Text="16 pt" />
 </asp:DropDownList><p />

 <asp:TextBox id="txtMaskEdit" Columns="25" runat="server" /> <p />

</form>

The Server-Side Page_Load Event Handler

The Page_Load event handler is shown in Listing 6.20. In it you collect the mask string and font size from the drop-down lists in the page, and you specify the font name. Then you apply these font details to the text box. Here you're using the Courier New font. You need a monospaced (fixed-pitch) font so that the characters typed into the text box will line up correctly with the light-gray placeholders.

Listing 6.20—The Page_Load Event Handler for the MaskedEdit Control Demonstration Page

Sub Page_Load()

 Dim sMask As String = selMask.SelectedValue
 Dim sFont As String = "Courier New"
 Dim sSize As String = selSize.SelectedValue

 txtMaskEdit.Text = ""
 txtMaskEdit.Style("font-family") = sFont
 txtMaskEdit.Style("font-size") = sSize & "pt"

 Dim sQuery As String = sMask
 sQuery = sQuery.Replace("a", "_")
 sQuery = sQuery.Replace("A", "_")
 sQuery = sQuery.Replace("l", "_")
 sQuery = sQuery.Replace("L", "_")
 sQuery = sQuery.Replace("n", "_")
 sQuery = sQuery.Replace("?", "_")
 sQuery = Server.UrlEncode(sQuery)
 sFont = Server.UrlEncode(sFont)

 txtMaskEdit.Style("background-image") _
  = "url(mask-image.aspx?mask=" _
  & sQuery & "&font=" & sFont & "&size=" & sSize & "&cols=" _
  & txtMaskEdit.Columns.ToString() & ")"

 Dim sTip As String = sMask
 sTip = sTip.Replace("a", "[a]")
 sTip = sTip.Replace("A", "[A]")
 sTip = sTip.Replace("l", "[l]")
 sTip = sTip.Replace("L", "[L]")
 sTip = sTip.Replace("n", "[n]")
 sTip = sTip.Replace("?", "[?]")
 txtMaskEdit.ToolTip = "Mask: " & sTip & vbCrlf & " where:" _
  & vbCrlf & "[a] = any alphanumeric character (0-9, A-Z, a-z)" _
  & vbCrlf & "[A] = an uppercase alphanumeric char (0-9, A-Z)" _
  & vbCrlf & "[l] = any letter character (A-Z, a-z)" _
  & vbCrlf & "[L] = an uppercase letter character (A-Z)" _
  & vbCrlf & "[n] = any numeric character (0-9)" _
  & vbCrlf & "[?] = any character"

 txtMaskEdit.Attributes.Add("onkeydown", _
  "return doKeyDown(event, this, '" & sMask & "')")
 txtMaskEdit.Attributes.Add("onkeypress", _
  "return doKeyPress(event, this, '" & sMask & "')")
 txtMaskEdit.Attributes.Add("onkeyup", _
  "return doKeyUp(event, this, '" & sMask & "')")
 txtMaskEdit.Attributes.Add("onfocus", _
  "return doFocus(event, this, '" & sMask & "')")

End Sub

Where do the light-gray placeholders come from, and how do you get them into the text box? In this example you're taking advantage of the fact that you can specify an image for the background of most controls—including a text box—under the CSS2 recommendations. So all you have to do is create a suitable image that contains the placeholder characters and assign it to the text box's background-image style selector. The text that the user types into the text box will then overlay the image, giving the effect shown in Figure 6.5.

The sample page uses a separate ASP.NET page named mask-image.aspx to generate the required image dynamically at runtime. The code in the Page_Load event creates the URL that will load this page. It also appends as the query string the mask string as it will appear in the text box (all the special characters that denote values the user must type are replaced with underscores), the font name, the font size, and the value of the Columns property of the text box. All this information is required to be able to create the appropriate image.

You also want to provide a pop-up ToolTip for the text box that makes it easy for the user to understand what input is required. So the next stage in the Page_Load event handler is to build a suitable string and assign it to the ToolTip property of the text box. If you embed carriage returns into the ToolTip string, Internet Explorer will break up the string to give a neater display (although unfortunately other browsers ignore the carriage returns). Figure 6.6 shows the ToolTip as it appears in Internet Explorer 6.

Figure 6Figure 6.6 The MaskedEdit control page, showing a mask and the corresponding ToolTip.

The final stage in the Page_Load event is to attach the client-side functions to the text box to turn the text box into a MaskedEdit control. As demonstrated several times already in this chapter, you specify the values required for the client-side function parameters. For each function, you must pass in a reference to the client-side event (using the event keyword), a reference to the current control (using the this keyword), and the mask to use. Here's an example:

txtMaskEdit.Attributes.Add("onkeydown", _
 "return doKeyDown(event, this, '" & sMask & "')")

The result is that the Page_Load event now creates an <input type="text"> control with a range of attributes. Listing 6.21 shows the output that is generated when this page is viewed in a browser. This code shows the ToolTip with embedded carriage returns, the four event handler attributes, and the style declarations that specify the font and the background image.

Listing 6.21—The Output Generated for the MaskedEdit Control when the Page Is Viewed in a Browser

<input name="txtMaskEdit" type="text" size="25" id="txtMaskEdit"
 title="Mask: [n][n][n][n]-[n][n]-[n][n]T[n][n]:[n][n]:[n][n]
     where:
     [a] = any alphanumeric character (0-9, A-Z, a-z)
     [A] = an uppercase alphanumeric character (0-9, A-Z)
     [l] = any letter character (A-Z, a-z)
     [L] = an uppercase letter character (A-Z)
     [n] = any numeric character (0-9)
     [?] = any character" 
 onkeydown="return doKeyDown(event, this, 'nnnn-nn-nnTnn:nn:nn')"
 onkeypress="return doKeyPress(event, this, 'nnnn-nn-nnTnn:nn:nn')"
 onkeyup="return doKeyUp(event, this, 'nnnn-nn-nnTnn:nn:nn')" 
 onfocus="return doFocus(event, this, 'nnnn-nn-nnTnn:nn:nn')" 
 style="font-family:Courier New;font-size:10pt;background-image
    :url(mask-image.aspx?mask=____-__-__T__%3a__%3a__&
    font=Courier+New&size=10&cols=25);" />

Generating the Background Mask Image

The only remaining code you need to examine now is that which generates the image for the background of the text box. Generating images dynamically before the .NET Framework came along was hard, and most Web developers relied on custom COM components created by third-party suppliers. However, the .NET Framework removes much of the complexity from creating images dynamically. This is not to say that you still won't find many great image components and server controls around—and for complex tasks, these components and controls can save a huge amount of development time.

Nevertheless, the requirements for this example are simple. You just need to create an image that contains a text string. Listing 6.22 shows the complete code for the page mask-image.aspx, which generates the image and returns it as a stream that represents a GIF file.

Listing 6.22—The ASP.NET Page That Generates the Mask Image for the Text Box

<%@Page Language="VB" %>
<%@Import Namespace="System.Drawing" %>
<%@Import Namespace="System.Drawing.Imaging" %>

<script runat="server">

Sub Page_Load()

 ' set content-type of response so client knows it is a GIF image
 Response.ContentType="image/gif"

 ' get mask and font details from query string and URL-decode
 Dim sText As String = Server.UrlDecode(Request.QueryString("mask"))
 Dim sFont As String = Server.UrlDecode(Request.QueryString("font"))
 Dim sSize As String = Request.QueryString("size")
 Dim sCols As String = Request.QueryString("cols")

 Dim iWidth, iHeight As Integer
 iWidth = Integer.Parse(sSize) * Integer.Parse(sCols)
 iHeight = Integer.Parse(sSize) * 3

 ' create a new bitmap
 Dim oBitMap As New Bitmap(iWidth, iHeight)

 ' create new graphics object to draw on bitmap
 Dim oGraphics As Graphics = Graphics.FromImage(oBitMap)

 ' create the rectangle to hold the text
 Dim oRect As New RectangleF(0, 0, oBitMap.Width, oBitMap.Height)

 'create a solid brush for the background and fill it
 Dim oBrush As New SolidBrush(Color.White)
 oGraphics.FillRectangle(oBrush, oRect)

 ' create a Font object for the text style
 Dim oFont As New Font(sFont, Single.Parse(sSize))

 ' create a brush object and draw the text
 oBrush.Color = Color.FromArgb(153, 153, 153)
 oRect = New RectangleF(-1, 1, oBitMap.Width, oBitMap.Height)
 oGraphics.DrawString(sText, oFont, oBrush, oRect)

 ' write bitmap to response
 oBitmap.Save(Response.OutputStream, ImageFormat.Gif)

 ' dispose of objects
 oBrush.Dispose()
 oGraphics.Dispose()
 oBitmap.Dispose()

End Sub

</script>

Notice that you have to import the Drawing and Imaging namespaces to be able to use the classes they contain. You also have to set the ContentType value for the page to "image/gif" so that the browser will treat it as an image. After this, you extract the values from the query string that you need to build the image. You URL-encoded them in the Page_Load event handler when you created the query string (because some of them contain spaces or other non-URL-legal characters), so you have to decode them first.

Calculating the Size of the Image and the Bitmap

You need to make sure your image is large enough to fill the text box, or you'll get multiple copies tiled over the background. However, you don't want to make it any bigger than necessary because you want to minimize download times to achieve the fastest possible rendering. You use the number of columns and the font size to give an image of sufficient width and height.

The code then creates a new Bitmap instance of that size and from it a Graphics object that you can use to draw and write on the image. By default the image is black (with a pixel value zero), so you create a rectangle the same size as the image and a new white SolidBrush object. The Fill method of the brush then paints the image white.

Drawing the Text

To draw the text, you need to create an instance of a Font object that represents the font and size specified by the values in the query string, and then you need to change the color of the SolidBrush object to light gray. Then it's simply a matter of defining a rectangle where you want to draw the text (you have to adjust the top and left values slightly to get the best lineup possible in the text box) and writing the text onto the Bitmap instance. To return the Bitmap instance, you save it directly to the ASP.NET Response object's current OutputStream instance, specifying the image format you want.

Usability Issues with the MaskedEdit Control

Although the MaskedEdit control is neat, easy to use, and works quite well, you'll probably discover a few shortcomings when you start to experiment with it. We mentioned the difficulties in absolutely controlling the user's keypresses and mouse clicks earlier in this chapter, and you might want to extend the code to try to handle these more accurately. There is also the issue that, if you type quickly, the client-side event handler code cannot keep up, and it misses the literal characters in the mask string.

Another issue that you'll come across concerns the background mask image. The actual size and spacing of the characters on the bitmap that the mask-image.aspx page generates depend on the environment of the server (the screen resolution, the installed fonts, and other internal parameters). However, the text that is displayed in the text box as the user types depends on the settings of the user's machine (that is, the client machine). You are likely to find slight misalignment occurring in some cases.

Having said all this, the techniques demonstrated for creating images dynamically and for handling keypress events are still valid—and you will no doubt find many other uses for them in your own applications.

Creating a One-Click Button

Now that you've seen how to use client-side code to detect keypress events, let's move on to talk about how you can use client-side code and/or server-side code to prevent users from clicking a button on a page more than once—or at least detecting if they do so.

There are several ways to approach this problem, and the example used here demonstrates four of the most obvious solutions:

Disabled Buttons in Opera

Opera, even in version 7, does not gray out a button or control that is disabled. However, it does correctly prevent the user from clicking a button or activating a control that has the disabled property set to true. Someone once told me that Opera was so named because it was designed to keep the other browser manufacturers on their toes with regard to performance, usability, and features. But if this were the case, surely it would have been named Ballet.

Figure 6.7 shows the sample page after the button has been clicked. You can see that, under the text box, a message indicating how many times the page has been submitted so far is displayed. By default, all three methods of preventing the page from being submitted more than once are enabled, and they are processed in the same order as the check boxes on the page. You can turn off each one to see the remaining methods in action; we'll look at what effects this has shortly.

Figure 7Figure 6.7 The one-click button demonstration page in action.

Figure 6.8 shows a schematic view of how the controls on the sample page affect the way that it runs and which of the four techniques for preventing multiple button clicks or multiple server-side page processing are employed. The three decision boxes correspond to the three check boxes in the page.

Figure 8Figure 6.8 A schematic of the processes for preventing multiple page submissions in the one-click button example.

The Code to Implement a One-Click Button

The visible part of the sample page is created using the HTML shown in Listing 6.23. None of the controls has a client-side event handler attached in the declaration shown here; you'll be adding them dynamically at runtime.

Listing 6.23—The Form Section of the One-Click Button Sample Page

<form id="frmMain" runat="server">

 <asp:TextBox id="txtTest" Text="Required value" runat="server" />
 <asp:Button id="btnOneClick" Text="Click me" runat="server" />
 <asp:Label id="lblMsg" EnableViewState="False" runat="server" />

 <asp:Checkbox id="chkNoDisable" runat="server"
   Text="Do not disable button after first click." />
 <asp:Checkbox id="chkAllowClick" runat="server"
   Text="Allow multiple button clicks to be processed." />
 <asp:Checkbox id="chkAllowSubmit" runat="server"
   Text="Allow page to be submitted again while processing." />

</form>

Setting the disabled Property of the Button to true

When all three methods for preventing multiple form submissions are enabled, the one that actually prevails is the one that disables the submit button as soon as it's clicked. It's easy enough to do this; you just attach a client-side function that sets the disabled property of the button to true in that button's onclick event.

However, you can't do this directly within the declaration of the submit button in this example because you've used an ASP.NET Button server control, and the OnClick attribute sets the server-side event handler (not the client-side one). If you write this:

<asp:Button text="Submit" runat="server" 
   onclick="MyServerCode" />

you can expect the server-side routine or function named MyServerCode to be executed when the button is clicked, after the page has been submitted to the server. One way you can get around this is to use the ordinary HTML server controls instead of the ASP.NET Web Forms controls. The button control implemented by the HtmlInputButton class exposes the OnServerClick event handler property to define code that runs on the server, allowing you to use the onclick attribute to specify the client-side event handler:

<input type="submit" value="Submit" runat="server"
    onserverclick="MyServerCode"
    onclick="MyClientSideCode();"/>

HtmlControls Versus WebControls Property Names

Remember that you have to use the ordinary HTML attribute names with the standard controls from the System.Web.UI.HtmlControls namespace. For example, the caption of a button is set with the Value property and not with the Text property.

The other approach is to use a Web Forms control but add the client-side attribute dynamically when creating the page on the server. This allows the client-side onclick functionality to coexist with the server-side event handling. When the button is clicked, the client-side code runs first, and then, after the page is posted back to the server, any ASP.NET server-side event handler attached to the control is invoked:

control.Attributes.Add("onclick", "MyClientSideCode()")

The sample page uses this technique. In the server-side Page_Load event handler, you specify that the client-side function named buttonClick will be executed when the button is clicked. You pass to this function a reference to the current control (using the this keyword) and assign the return value to the event:

btnOneClick.Attributes.Add("onclick", "return buttonClick(this);")

The Client-Side buttonClick Event Handler

Listing 6.24 shows the buttonClick client-side event handler that is called when the button on the sample page is clicked. In theory, all you actually need to do to prevent it from being clicked again is to set the disabled property to true, using the following:

buttonOneClick.disabled = true;

However, in Internet Explorer and Opera, this prevents the form from being submitted the first time as well (although it works as expected in Netscape and Mozilla). This means that you have to submit the form programmatically within the code, after setting the disabled property of the button:

buttonOneClick.disabled = true;
document.forms[0].submit();

You could even do this directly in the declaration of the button, rather than writing a function and calling it from the onclick attribute. However, because you are implementing several techniques in the same page, the event handlers are a little more complicated. When the button is clicked, you look to see if the first check box is selected. If it is not, you disable the button to prevent it from being clicked again.

Listing 6.24—The Client-Side buttonClick Event Handler and Timer Routines

var bButtonClicked = false;

function buttonClick(ctrl) {
 // check value of first checkbox
 var theForm = document.forms[0];
 if(theForm.elements['chkNoDisable'].checked == false) {
  // first checkbox is not ticked
  // disable submit button
  ctrl.disabled = true;
  startTimer();
  theForm.submit();
 }
 // check value of second checkbox
 if(theForm.elements['chkAllowClick'].checked == false) {
  // second checkbox is not ticked
  if (bButtonClicked == false) {
   // first time button was clicked
   bButtonClicked = true;
   startTimer();
   return true;
  }
  else {
   // prevent button event from being executed
   return false;
  }
 }
 else {
  // second checkbox is ticked
  // allow button event to continue
  startTimer();
  return true;
 }
}

function startTimer() {
 // display "Please wait" message
 var label = document.getElementById('lblMsg');
 label.innerHTML = '<b>Please wait.</b>';
 // start interval timer for one second
 window.setTimeout('showProgress()', 1000);
}

function showProgress() {
 // update "Please wait" text
 var label = document.getElementById('lblMsg');
 label.innerHTML += '<b>.</b>';
 // restart interval timer for one second
 window.setTimeout('showProgress()', 1000);
}

The sample page contains a couple routines that start and then reset a timer within the page, to provide a progress indicator showing that the server is processing the page. (You simulate a long process taking place on the server side, as you'll see shortly.) You can see the two timer functions, named startTimer and showProgress, at the end of Listing 6.24. After disabling the button, you call the routine to start the timer and then submit the form by calling its submit method (as discussed earlier). The result is shown in Figure 6.9.

Figure 9Figure 6.9 The progress indicator that runs while the page is being processed.

Trapping the click Event for the Button

The buttonClick event shown in Listing 6.24 continues by looking to see if the second check box is selected. If it isn't, you want to prevent more than one button click from being processed. (Remember that if the first check box is selected, the button will not be disabled after the first click.) In other words, you allow the first button click to be handled normally, but you trap and prevent any subsequent clicks by returning false from the event handler.

This is similar to the techniques used in the previous examples to trap a keypress event. You declare a page-level variable named bButtonClicked that is initially set to false (shown at the start of Listing 6.24). When a click event occurs, code in the buttonClick event handler tests to see if bButtonClicked is false. If it is, bButtonClicked is set to true, and the code starts the progress indicator timer and returns true from the function to allow the click to be processed by the browser.

If the button has already been clicked, bButtonClicked will be true, so the function can return false to prevent this click event from being processed. Finally, if the second check box is not selected, you start the timer and return true to allow the click to be processed.

Trapping the submit Event for the Form

Having seen how you can prevent multiple click events from being processed by using a page-level variable, you won't be surprised to see how the sample page prevents multiple submissions of a form. Listing 6.25 shows the formSubmit function, which is attached to the opening <form> element when the page is created (in the server-side Page_Load event), using the following:

frmMain.Attributes.Add("onsubmit", "return formSubmit(this);")

A page-level variable named bFormSubmitted is initially set to false and then switched to true when the form is first submitted. The progress indicator timer is also started at this point, and the function returns true to allow the form to be submitted. Subsequent attempts to submit the form fail because the function returns false. However, if the third check box is selected, the function always returns true to allow the form to be submitted multiple times—whereupon the final approach to handling multiple form submissions comes into play.

Listing 6.25—The Client-Side formSubmit Function

var bFormSubmitted = false;

function formSubmit(ctrl) {
 // check value of third checkbox
 if(ctrl.elements['chkAllowSubmit'].checked == false) {
  // third checkbox is not ticked
  if (bFormSubmitted == false) {
   // first time form was submitted
   bFormSubmitted = true;
   startTimer();
   return true;
  }
  else {
   // prevent form from being submitted
   return false;
  }
 }
 else {
  // third checkbox is ticked
  // allow form to be submitted
  startTimer();
  return true;
 }
}

Counting Postbacks with Server-Side Code

If all three check boxes are selected in the sample page, the user will be able to submit the form multiple times before the postback has completed and the page is reloaded into the browser. To prevent this from interrupting resource-intensive processing, you can use the final technique demonstrated by this example.

This technique involves counting postbacks. A counter variable is added to both the page and the user's session. When the page is created, the same value is placed into the viewstate of the page and stored in a session variable. Each time the page is posted back, the counter is incremented and the new value is placed in the viewstate and in the session.

However, if the user submits the same page more than once, the value in the viewstate will remain the same, whereas the value in the session variable will have been incremented when the initial postback from this instance of the page occurred. Figure 6.10 shows the process as a schematic diagram to make it easier to see how this works.

Figure 10Figure 6.10 Counting postbacks to prevent multiple processing of the same page.

The Page_Load Event Code for Counting Postbacks

All the processing required to implement counting of postbacks is performed within the Page_Load event of the page, although you could attach it to server-side event handlers instead if required. Listing 6.26 shows the complete Page_Load event handler. After you add the client-side event handlers required for the previous techniques to the button and form elements on the page, you check to see if this is a postback or if the page is being loaded for the first time.

Listing 6.26—The Page_Load Event Handler Code for Counting Postbacks

Sub Page_Load()

 ' add client-side event attributes to button and form here
 ...

 If Page.IsPostBack Then

  ' collect session and viewstate counter values
  Dim sPageLoads As String = Session("PageLoads")
  Dim sPageIndex As String = ViewState("PageIndex")

  If sPageLoads = "" Then
   lblMsg.Text &= "<b>WARNING:</b> Session support " _
    & "is not available."
  Else
   Dim iPageLoads As Integer = Integer.Parse(sPageLoads)
   Dim iPageIndex As Integer = Integer.Parse(sPageIndex)

   ' see if this is the first time the page was submitted
   If iPageLoads = iPageIndex Then
    lblMsg.Text &= "Thank you. Your input [" _
     & iPageLoads.ToString() & "] has been accepted."

    ' *************************************
    ' perform required page processing here
    ' *************************************

    ' delay execution of page before sending response
    ' page is buffered by default so no content is sent
    ' to the client until page is complete
    Dim dNext As DateTime = DateTime.Now
    dNext = dNext.AddSeconds(7)
    While DateTime.Compare(dNext, DateTime.Now) > 0
     ' wait for specified number of seconds
     ' to simulate long/complex page execution
    End While

   Else
    lblMsg.Text &= "<b>WARNING:</b> You clicked the button " _
     & (iPageLoads - iPageIndex + 1).ToString() & " times."
   End If

   ' increment counters for next page submission
   Session("PageLoads") = (iPageLoads + 1).ToString()
   ViewState("PageIndex") = (iPageLoads + 1).ToString()

  End If
 Else

  ' preset counters when page first loads
  Session("PageLoads") = "1"
  ViewState("PageIndex") = "1"
  lblMsg.Text="Click the button to submit your information"

 End If
End Sub

If you look at the code at the end of Listing 6.26, you can see that when it's not a postback, you just set the viewstate and session values to "1" (remember that they are stored as String values). The viewstate of the page is a useful bag for storing small values. These values are encoded into the rest of the viewstate that ASP.NET automatically generates for the page it is creating.

Using a Hidden Control to Store Values

An alternative approach would be to store the value in a hidden-type input control on the page. However, this is less secure than using the viewstate because the value can be viewed by users, who might be tempted to try to spoof the server by changing the value (although this is probably an unlikely scenario).

If this is a postback, the first step is to check whether sessions are supported by looking for the value you stored against the PageLoads key when the page was initially created. The process will not work if there is no value in the session, and at this point, you need to decide what you want to do about it. If you absolutely need to perform the postback counting process, you can warn the user that he or she must enable sessions, or perhaps you would redirect the user to a page that uses ASP.NET cookieless sessions. You might even decide to use cookieless sessions for all clients.

Using Cookieless Sessions in ASP.NET

The ASP.NET cookieless sessions feature provides session support for clients that do not themselves support HTTP cookies. It works by "munging" (that is, inserting) the session ID into the URL of the page and automatically updating all the hyperlinks in the page to reflect the updated URL. All you need to do to enable cookieless sessions is place in the root folder of the application a web.config file that contains the following:

<configuration>
 <system.web>
  <sessionState cookieless="true" />
 </system.web>
</configuration>

Comparing the Postback Counter Values

The next step in the process of checking for multiple postbacks is to compare the values in the viewstate and the session. If they are the same, you can accept the postback and start processing any submitted values. The sample page displays a message to indicate the current postback counter value. The code in the page uses a loop that waits seven seconds to simulate a long process. Afterward, you can increment the counter values in the viewstate and session, and then you can allow the page to be created and sent to the client.

However, if the viewstate and session values are different, you know that the postback has occurred from a page that you are already processing. Rather than try to cancel the existing processes that were started by previous postbacks from this instance of the page, you just ignore the current postback and don't carry out the processing again. Instead, you return a message to the user, indicating how many times he or she clicked the button. You can see the result in Figure 6.11.

Figure 11Figure 6.11 The result in the one-click button example when the form is submitted more than once.

Trigger-Happy Button Clicks

Note that it's possible to click the button so quickly that ASP.NET does not have time to start processing the page and update the session value. In this case, the page reports fewer clicks than actually occurred when the final submit action has been processed.

Summary

This chapter takes a more comprehensive look at how the client-side script used in the ComboBox control, described at the end of Chapter 5, works. It also discusses the three main requirements for producing interactive pages when using client-side script:

Following this discussion, the chapter delves deeper into integrating client-side code with ASP.NET server-side code to produce useful controls and interactive pages. This chapter considers four topics:

So, as you've seen, getting exactly the performance, appearance, or usability you want is not always easy (or even possible!). However, you can create components and build reusable content that far exceeds the standard output that ASP.NET can provide on its own. Chapter 7 continues this theme by looking at some more user controls that combine with the features of ASP.NET to make building interactive pages easier.

800 East 96th Street, Indianapolis, Indiana 46240