Home > Articles > Programming > ASP .NET

📄 Contents

  1. Client-Side Interaction on the Web
  2. Useful Client-Side Scripting Techniques
  3. Summary
  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

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:

  • Trapping an event that occurs on the client and popping up a confirmation dialog before carrying out the action on the server (for example, getting the user to confirm that he or she wants to delete a row in a DataGrid control).

  • Trapping a Return keypress to prevent a form from being submitted or trapping any other keypress that might not be suitable for a control or an application you are building.

  • Handling individual keypress events (for example, implementing a MaskedEdit control).

  • Creating a button that the user can click only once—effectively creating a form that can only be submitted once. This prevents the user from causing a second postback, which might interrupt server-side processing, when nothing seems to be happening at the client.

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:

  • Internet Explorer switches the focus to the form's submit button and activates (that is, clicks) it. Even if there is more than one submit button on the current form, Internet Explorer always moves to and activates the first one.

  • Netscape and Mozilla don't move the focus, but they always activate the first submit button.

  • Opera is more intelligent than Internet Explorer, Netscape, and Mozilla. As you move the focus to and between elements on a form, Opera automatically sets the submit button as the default button, which is then activated when Return is pressed. However, when there is more than one submit button on a form, Opera changes the one that is the default (the one with the darker gray outline) as you move between the controls on the form. When you press Return, the submit button that follows the current control (within the buttons' declaration order in the page source) is activated.

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:

  • doKeyDown—This function is executed when the user presses a key. Its task is to cancel any keypresses that the control cannot support. With a few exceptions, it cannot handle nonprintable characters.

  • doKeyPress—This function is executed when the user releases a key. It checks the key code against the mask and cancels it if it is not valid. In cases where an uppercase letter is expected, the code automatically converts lowercase letters to uppercase and accepts them.

  • doKeyUp—This function is executed when the user releases a key. Its task is to add to the text box any literal characters that follow the current character so that the user does not have to enter them manually. It also creates a message in the status bar that indicates the next character that is expected.

  • doFocus—This function is executed when the control first receives the focus. It just has to make sure that any literal characters at the start of the mask are inserted into the text box. It does this by calling the doKeyUp function.

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:

  • Disable the button as soon as it is clicked, by handling the onclick event with client-side code, and setting the disabled property to true. If the form has more than one submit button or uses AutoPostback on other controls, you also have to disable those controls at the same time. Remember that some browsers (especially older ones) do not allow controls to be disabled.

  • Trap the client-side onclick event of the button and set a client-side variable to true; then prevent the button from being clicked again while this value is true by returning false from the event handler.

  • Set a client-side variable to true as soon as the form is submitted and prevent it from being submitted again while this value is true. This is useful if the form has more than one submit button or uses AutoPostback on other controls because you don't need to change the properties of these controls (as you would with the first method). However, this approach does not give the user visual feedback that the button is disabled.

  • Allow the user to submit the form multiple times but detect this on the server and carry out the required processing only the first time the form is submitted. Again, there is no visual feedback for the user with this method, but this approach works when the client's browser does not support client-side scripting (or the user has disabled it).

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.

  • + Share This
  • 🔖 Save To Your Account