InformIT

Loading Progress and Status Displays in ASP.NET 1.1

Date: Jul 16, 2004

Sample Chapter is provided courtesy of Sams.

Return to the article

This chapter shows you two ways to present users with status information while a complex or lengthy process is taking place: displaying a simple "please wait" message or animated GIF image, and implementing the server-side process as a series of staged individual operations.

In This Chapter

ASP.NET is extremely fast when you're creating and delivering Web pages. However, no matter how fast and efficient your Web server and the software it runs (including your Web applications) are, the delay between the user clicking a button and seeing the results can vary tremendously. On a good ADSL or direct Internet connection, it might be a "wow, that was quick" few seconds. On a dial-up connection, especially when the server is on the other side of the world, it's more likely to be the seemingly interminable "did I remember to pay the phone bill?" response.

One feature that most executable applications offer but that is hard to provide in a Web application is accurate status information and feedback on a long-running process. However, this can be achieved in at least two different ways, depending on the process your application is carrying out and the kind of status or feedback information you want to provide.

One technique is a "smoke and mirrors" approach, in that it makes the user feel comfortable that something is happening—while in fact the information the user sees bears no real relationship to the progress of the server-based operation. The other approach, covered toward the end of the chapter, provides accurate status and feedback details but imposes limitations on client device type and the kinds of operation for which it is suitable.

In this chapter you will see what is possible regarding loading progress and status displays. You'll learn how to use and adapt a variety of techniques to suit your own applications and requirements. This chapter starts with a look at the theory of the process and examines the simplest way it can be achieved.

Displaying a "Please Wait" Page

Many ASP.NET developers find that despite their best efforts in producing efficient code that minimizes response times, the vagaries of database response times, the transit time over the Internet, and user input criteria that are not specific enough can result in a lengthy delay before a page appears in the browser. The result is that users often click the submit button several times to try to elicit a response from your server, sometimes causing all kinds of unfortunate side effects.

Chapter 6, "Client-Side Script Integration," looks at some specific solutions for creating a one-click button. However, an alternative approach is to provide a page that loads quickly and that displays a "please wait" message or some suitable graphic feature, while the real target page is being processed and delivered. In ASP 3.0 and other dynamic Web programming environments, it's common to handle this process with separate pages that implement the three execution stages shown in Figure 3.1.

Figure 3.1Figure 3.1 The traditional separate-pages approach to providing a "please wait" message.

ASP.NET engenders the single-page postback architecture approach. However, you can build similar features into ASP.NET applications by implementing the three pages as separate sections of a single page. The server control approach to populating elements and attributes on the page also makes it easier to work with elements such as the <meta> element that you use as part of the process. Figure 3.2 shows the ASP.NET approach, as it is adopted in the example described in the following sections.

Passing Values Between Requests

Of course, what's missing from Figures 3.1 and 3.2 is how any values submitted by the user are passed from the "please wait" page to the code that creates the results. In ASP 3.0 and other dynamic Web page technologies, the usual technique is to include a placeholder within the content attribute of the <meta> element that gets replaced by a query string containing the values sent from the <form> section. You can then extract these from the query string in the page or section of code that generates the results. You'll see this discussed in more detail in the section "Displaying the "Please Wait" Message," later in this chapter.

Figure 3.2Figure 3.2 The ASP.NET single-page approach to providing a "please wait" message.

A Simple "Please Wait" Example

Figure 3.3 shows the initial display of a simple sample page that displays a "please wait" message while the main processing of the user's request is taking place. The page queries the Customers table in the sample Northwind database that is provided with SQL Server. In the text box on the page, the user enters all or part of the ID of the customer he or she is looking for.

Figure 3.3Figure 3.3 The initial page of the simple "please wait" example.

When the user clicks the Go button, the value in the text box is submitted to the server, and the page shown in Figure 3.4 is displayed. No complex processing is required to display this page, and the total size of the content transmitted across the wire is small, so it should appear very quickly. The user knows that his or her request is being handled, and there is no submit button for the user to play with in the meantime.

Obtaining the Sample Files

You can download this example and the other examples for this book from the Sams Web site at http://www.samspublishing.com, or from http://www.daveandal.net/books/6744/. You can also run many of this book's examples online at http://www.daveandal.net/books/6744/.

Figure 3.4Figure 3.4 The "please wait" message that is displayed while processing the main page.

After a short delay (about 3 or 4 seconds, in this example), the main page, which contains the results, is returned to the user and replaces the "please wait" message. You can see in Figure 3.5 that the main page contains a list of customers matching the partial ID value that was provided. At the bottom of the page is a New Customer link that takes the user back to the first page.

Figure 3.5Figure 3.5 The main page, displaying the results of a search for matching customers.

The HTML and Control Declarations

Listing 3.1 shows the relevant parts of the sample page shown in the preceding section. Notice that although you include a <meta> element in the <head> section of the page, you don't specify any attributes for it. Instead, you give it an ID and specify that it is a server control by including the runat="server" attribute. However, this <meta> element will have no effect on the page or the behavior of the browser until you specify the attributes for it in the server-side code.

Listing 3.1 The HTML and Control Declarations for the Simple "Please Wait" Sample Page

<html>
<head>
<!----- dynamically filled META REFRESH element ----->
<meta id="mtaRefresh" runat="server" />
</head>
<body>

<!----- form for selecting customer ----->
<form id="frmMain" Visible="False" runat="server">
 Enter Customer ID:
 <asp:Textbox id="txtCustomer" runat="server" />
 <asp:Button id="btnSubmit" Text="Go" runat="server" />
</form>

<!----- "please wait" display ----->
<div id="divWait" Visible="False" runat="server">
 <center>
 <p>&nbsp;</p>
 <p>&nbsp;</p>
 <b>Searching, please wait...</b><p />
 <p>&nbsp;</p>
 <span id="spnError"></span>
 <p>&nbsp;</p>
 </center>
</div>

<!----- section for displaying results ----->
<div id="divResult" Visible="False" runat="server">
 <b><asp:Label id="lblResult" runat="server" /></b><p />
 <asp:DataGrid id="dgrResult" runat="server" /><p />
 <asp:Hyperlink id="lnkNext" Text="New Customer" runat="server" />
</div>

</body>
</html>

The remainder of the page is made up of the three sections that implement the three pages shown in Figures 3.3 through 3.5. All three pages include a Visible="False" attribute in their container element—either the <form> element itself for the first one or the containing <div> element for the other two pages. So all three sections will be hidden when the page is loaded, and you can display the appropriate one by simply changing its Visible property to True.

Meta Refresh and Postback Issues

As you can see from the figures and code so far in this chapter, this example uses a <meta> element in the "please wait" page to force the browser to load the main page. This much-used technique is a handy way to redirect the browser to a different page, and it is supported in almost every browser currently in use today.

When you use the server-side Response.Redirect method in an ASP.NET (or ASP 3.0) page, the server sends two HTTP headers to the client to indicate that the browser should load a different page from the one that was requested. The 302 Object Moved header indicates that the requested resource is now at a different location, and the Location new-url header specifies that the resource is located at the URL denoted by new-url.

The <meta> element supports the http-equiv attribute, which is used to simulate the effects of sending specific HTTP headers to the browser. To redirect the browser to a different URL, using a <meta> element, you can use this:

<meta http-equiv="refresh" content="[delay];url=[new-url] />

In this syntax, [delay] is the number of seconds to wait before loading the page specified in [new-url]. All browsers will maintain the current page they are displaying until they receive the first HTTP header sent by the server for the new page. So if the processing required for creating the new page takes a while and the server does not send any response until the processing is complete, the user will continue to see the page containing the <meta> element (the "please wait" message). By default, ASP.NET enables response buffering, so it does not generate any output until the new page is complete and ready to send to the browser.

Replacing the Existing Page in the Browser

Web browsers continue to display the existing page when you click a link in that page or enter a new URL in the address bar, while they locate and start to load the new page. However, as soon as the first items of the page that will be rendered are received (as opposed to the HTTP headers), the existing page is removed from the display, to be replaced by the progressive rendering of the new page.

One important point to note, however, is that if you disable output buffering by setting Response.Buffer = False, or if you force intermediate output to be sent to the response by using Response.Flush, the page currently displayed in the browser will be discarded as soon as the partial output of the new page is received.

You can delay the removal of the existing page in some browsers—for example, Internet Explorer supports page translations, which take advantage of the built-in Visual Filters and Transitions feature (see http://msdn.microsoft.com/workshop/author/filter/filters.asp#Interpage_Transition).

However, the issue here is that unlike when you submit an ASP.NET <form> element, the redirection caused by the <meta> element doesn't perform a postback. This means that viewstate for the page will not be maintained, and the values of any controls on the whole page (including the nonvisible sections) will be lost. So any values that you want to pass to the page the next time it loads (that is, when you display the results of processing the main section of the page) must be passed in the query string of the URL specified in the <meta> element.

Of course, this is what you would have to do in the pre-ASP.NET example shown in Figure 3.1 as well. Code in the page must collect the values from all the controls in the <form> section of the page when it is posted to the server, and it must build up a query string containing these within the <meta> element. You'll see how to do this in the following section.

The Page_Load Event Handler

The Page_Load event handler for the sample page first has to determine the current stage of the three-step process:

The following sections describe the code and page content that is used in the example to implement these three stages.

Displaying the "Please Wait" Message

Listing 3.2 shows the first section of the Page_Load event handler for the sample page. The only time a postback will have occurred is at Stage 1 because the other two stages are initiated by a <meta> element or a hyperlink. (Code in a section of the Page_Load event handler makes the <form> element visible when the page first loads, as you'll see shortly.)

Listing 3.2 The First Part of the Page_Load Event Handler

Sub Page_Load()

 If Page.IsPostback Then

  ' user submitted page with customer ID
  ' create URL with query string for customer ID
  ' next page will not be a postback, so viewstate will be lost
  Dim sRefreshURL As String = Request.Url.ToString() _
   & "?custID=" & txtCustomer.Text

  ' use META REFRESH to start loading next page
  mtaRefresh.Attributes.Add("http-equiv", "refresh")
  mtaRefresh.Attributes.Add("content", "0;url=" & sRefreshURL)

  ' hide <form> section and show "wait" section
  frmMain.Visible = False
  divWait.Visible = True

 Else
 ...

The Page.IsPostback property will be True only at Stage 1. At that point, you can extract the value of the text box (and any other control values that you might have in more complex examples) and build up the URL and query string for the <meta> element. You obviously want to reload the same page, so you get the URL from the Url property of the current Request instance. In this example, the only value you need to maintain as the page is reloaded is the value of the text box, and you use the name custID for this as you create the query string.

Then, as shown in Listing 3.2, you add the attributes you need to the <meta> element already declared in the page. You declare the <meta> element as a server control by using the following:

<meta id="mtaRefresh" runat="server" />

ASP.NET will implement this element as an instance of the HtmlGenericControl class because there is no specific control type within the .NET Framework class library for the <meta> element. However, the HtmlGenericControl type has an Attributes collection that you can use to add the attributes you need to it. You add the http-equiv="refresh" attribute and the content attribute, with a value that will cause the browser to immediately reload the page. If you view the source of the page in the browser (by selecting View, Source), you'll see the complete <meta> element:

<meta id="mtaRefresh" http-equiv="refresh" content="0;url=
/daveandal/books/6744/loadwait/simplewait.aspx?custID=a"></meta>

The next line of code hides the <form> section of the page. Because this stage is a postback, the viewstate of the controls on the page is maintained, so the form will remain visible if you don't hide it. The final code line makes the section containing the "please wait" message visible.

The HtmlGenericControl Class

The HtmlGenericControl class is described in more detail in Chapter 1, "Web Forms Tips and Tricks," where it is used for another control type that is not part of the .NET Framework class library.

Displaying the Results

Listing 3.3 shows the second section of the Page_Load event handler. This section is executed only if the Page.IsPostback property is False; however, you have to detect whether the page is being loaded by the <meta> element in the "please wait" page (Stage 2) or the hyperlink in the results page (Stage 3).

Listing 3.3 The Second Part of the Page_Load Event Handler

 ...
 Else

  ' get customer ID from query string
  Dim sCustID As String = Request.QueryString("custID")

  If sCustID > "" Then

   ' page is loading from META REFRESH element and
   ' so currently shows the "please wait" message
   ' a customer ID was provided so display results
   divResult.Visible = True

   ' set URL for "Next Customer" hyperlink
   lnkNext.NavigateUrl = Request.FilePath

   ' get data and bind to DataGrid in the page
   FillDataGrid(sCustID)

  Else

   ' either this is the first time the page has been
   ' loaded, or no customer ID was provided
   ' display controls to select customer
   frmMain.Visible = True

  End If
 End If

End Sub

You've just seen how the code that runs in Stage 1, when the user submits the form, adds the customer ID to the query string as custID=value. (When the user loads the page by clicking the hyperlink in the results page, there will be no query string.) So you test for the presence of a customer ID value and, if there is one, you can make the section of the page that displays the results visible, set the URL of the hyperlink in that section of the page so that it will reload the current page, and then call a separate routine, named FillDataGrid, that calculates the results and fills the ASP.NET DataGrid control in this section of the page.

At the end of Listing 3.3 you can see the code that runs for Stage 3 of the process. In this case, you know that it's not a postback, and there is no customer ID in the query string. So either this is the first time the page has been accessed or the user did not enter a customer ID value in the text box. In either case, you just have to make the <form> section visible, and the user ends up back at Stage 1 of the process.

Viewstate and the Visible Property

Notice that because the page does not maintain viewstate for Stages 2 and 3, you don't need to hide the other sections of the page content. All three carry the Visible="False" attribute, so they will not be displayed unless you specifically change the Visible property to True when the page loads each time.

Populating the DataGrid Control

The only other code in the sample page is responsible for fetching the required data from the database and populating the DataGrid control on the page. The full or partial customer ID, extracted from the query string at Stage 2 of the process, is passed to the FillDataGrid routine, which is shown in full in Listing 3.4.

Listing 3.4 The Final Part of the Page_Load Event Handler

Sub FillDataGrid(sCustID As String)

 Dim sSelect As String _
  = "SELECT CustomerID, CompanyName, City, Country, Phone " _
  & "FROM Customers WHERE CustomerID LIKE @CustomerID"
 Dim sConnect As String _
  = ConfigurationSettings.AppSettings("NorthwindSqlClientConnectString")
 Dim oConnect As New SqlConnection(sConnect)

 Try

  ' get DataReader for rows from Northwind Customers table
  Dim oCommand As New SqlCommand(sSelect, oConnect)
  oCommand.Parameters.Add("@CustomerID", sCustID & "%")
  oConnect.Open()
  dgrResult.DataSource = oCommand.ExecuteReader()
  dgrResult.DataBind()
  oConnect.Close()
  lblResult.Text = "Results of your query for Customer ID '" _
          & sCustID & "'"

  ' force current thread to sleep for 3 seconds
  ' to simulate complex code execution
  Thread.Sleep(3000)

 Catch oErr As Exception

  oConnect.Close()
  lblResult.Text = oErr.Message

 End Try

End Sub

The code here is fairly conventional. It creates a parameterized SQL statement and then executes it with a Command instance to return a DataReader instance that points to the result set generated by the database. You use the customer ID passed to the routine as the value of the single Parameter instance you create, and the resulting DataReader instance is bound to the DataGrid control. See the section "Using Parameters with SQL Statements and Stored Procedures" in Chapter 10, "Relational Data-Handling Techniques," for more details on using parameterized SQL statements.

Simulating a Complex or Lengthy Process

The code used to populate the DataGrid control in this example is unlikely to qualify as a complex or lengthy operation. Unless someone pulls the network cable out, it won't take long enough for the user to see the "please wait" message in the demonstration page. So to simulate a long process, you can insert a call to the Sleep method of the static Thread object, specifying that the current thread should wait 3 seconds before continuing:

Thread.Sleep(3000)

The only point to watch for here is that you have to import the System.Threading namespace into the page to be able to access the Thread object:

<%@Import Namespace="System.Threading" %>

Displaying a Progress Bar Graphic

A static "please wait" message is fine, but it could not be described as eye-catching, and it gives no indication that anything is actually happening. The server could die while the message is being shown, leaving you still staring at the "please wait" message three days later. It's nice to have some kind of indication that the Web site is still alive and really is working furiously to generate the results you asked for.

Unfortunately, with the way that Web browsers and HTTP work, this isn't easy to achieve. Each request/response is treated as a single unit of operation, and there is no persistent connection over which status information can be passed. There are ways around this, of course, but they tend to hit performance and cause undue server loading. You'll see an example of this in the section Implementing a Staged Page Load Process, later in this chapter.

An alternative is to display something that makes it look like the browser is working hard but actually bears no relationship to what's happening on the server. When you do this, you avoid the need for extra connections while the main process is taking place, and yet you still satisfy the user's desire to see something happening. The simplest solution is to use an animated GIF file in the page instead of or in addition to the "please wait" message.

Figure 3.6 shows the "please wait" page for this example. Instead of just a text message, you now also have a progress bar that appears to reflect the state of the long-running process that is generating the results the user is waiting for.

As intimated earlier, however, the progress bar is an illusion in that it will keep moving, regardless of whether the page takes a minute or a month to appear. But by carefully choosing the timing of the animation in the GIF file to match the anticipated average response times for average users, you can get it to look quite realistic.

Figure 3.6Figure 3.6 Displaying a progress bar while loading another page.

Other than the appearance of the progress bar, the remainder of this example looks the same as the previous example, which displays just the "please wait" text message. Therefore, the following sections concentrate on what's different in the declaration of the HTML, the server controls, and the code used to drive this page compared to the previous example.

Achieving True and Accurate Status Displays

To achieve a real page-loading status display, you can arrange for your server-side code to flush chunks of output to the client as it carries out the processing required to generate the results. These chunks of output could be client-side script that writes status details within the current browser page or even just simple <img> elements that load images to indicate progress of the operation. As an example, the MSN Expedia Web site (http://www.expedia.com) flushes partial page output to the browser, as you can see if you view the source of the page while it's searching for that holiday in Florida you keep promising your kids. However, it also uses a "dummy" animated graphic, just as this example does, which effectively indicates nothing about the actual underlying process of the operation.

The Progress Bar Animated Graphic Files

We provide four different versions of the animated progress bar graphic in the images folder of the examples you can download for this book (from http://www.daveandal.net/books/6744/). The only difference between them is the speed at which the progress display moves from left to right. The details of these graphics files are summarized in Table 3.1.

Table 3.1 The Progress Bar Animated GIF Files for This Example

Filename

Description

progressbar10.gif

The indicator progresses at a steady speed from left to right in approximately 10 seconds, and it remains at the fully right (complete) position for 10 seconds before starting again.

progressbar20.gif

The indicator progresses at a steady speed from left to right in approximately 20 seconds, and it remains at the fully right (complete) position for 10 seconds before starting again.

progressbar30.gif

The indicator progresses at a steady speed from left to right in approximately 30 seconds, and it remains at the fully right (complete) position for 10 seconds before starting again.

progressbarlog.gif

The indicator progresses in logarithmic fashion from left to right in approximately 30 seconds, starting quickly and then getting slower. It remains at the fully right (complete) position for 10 seconds before starting again.


Displaying the Progress Bar Graphic

In theory, building the progress bar sample page should be easy. You just have to insert an <img> element into the section of the page that is displayed for Stage 2 of the process in the previous example, and you're done, right? However, most Web developers approach these trivial tasks with trepidation and with a knowledge gleaned from experience that nothing ever works quite as you expect when dealing with Web browsers—especially Web browsers from different manufacturers.

It turns out that trepidation is definitely justified here. Simply adding an <img> element fails to work properly because as soon as the redirection is initiated by the <meta> element, most browsers stop loading any images for the current page. In this case, unless the progress bar is already cached (and the server is extremely responsive when the browser checks whether the file has changed since it was cached), the result is a "missing image" placeholder instead of a progress bar.

The solution to this problem is to force the browser to delay for a few seconds—long enough to load the progress bar graphic—before beginning the refresh process that requests the next page. You can set this delay to 3 seconds in the sample page by changing the content attribute you add to the <meta> element in the Page_Load event handler:

mtaRefresh.Attributes.Add("content", "3;url=" & sRefreshURL)

Now the page works fine in recent Netscape, Mozilla, and Opera browsers. But it still doesn't work properly in Internet Explorer. It seems that Internet Explorer "turns off" the animation in animated GIF files as soon as a new page is requested. After the 3-second delay, the progress bar just stalls—which ruins the whole effect! So, for Internet Explorer, you have to find an alternative approach, as described in the following sections.

An Alternative Page-Loading Technique for Internet Explorer

We experimented with several seemingly obvious approaches to loading the progress bar graphic and reloading the page using client-side script in Internet Explorer, all to no avail. It seems that the only way to circumvent the issue with the stalled animated graphic is to find a completely different way to load the next page (that is, reload the current page with the customer ID in the query string).

Internet Explorer 5 and higher have access to the MSXML parser component; it is part of a Windows installation and is distributed with Internet Explorer as well. Part of the MSXML parser component is an object named XMLHTTP, which you can use to request a resource from the server in the background while a page is loaded and displayed in the browser.

The XMLHTTP object is instantiated and manipulated with client-side script within a Web page, and it exposes properties and methods that allow you to make GET and POST requests to a server both synchronously and asynchronously. Although it is ostensibly designed for fetching XML documents, it works equally well fetching any type of resource, including HTML pages that probably aren't fully XML (or XHTML) compliant.

Loading Pages with the XMLHTTP Object

The process for using the XMLHTTP object is relatively simple, especially if you are happy to load the new page synchronously. You can create an instance of the XMLHTTP object by using the following:

var oHTTP = new ActiveXObject("Microsoft.XMLHTTP");

Next you open an HTTP connection, specifying the HTTP method (usually "GET" or "POST"), the URL of the target resource, and the value false to indicate that you want synchronous operation. Then you can use the send method to send the request:

oHTTP.open("method", target-url, false);
oHTTP.send();

After the response has been received from the server, you test the status property (the value of the HTTP status header) to see if it is 200 (which means "OK") and extract the page as a string from the XMLHTTP object by using the following:

if (oHTTP.status == 200)
 sResult = oHTTP.responseText;
else
 // an error occurred

However, if you use synchronous loading, the browser will not respond to any other events (including animating the GIF file) while the request for the next page is executing. Instead, you need to use asynchronous loading to allow the browser to carry on reacting as normal while the server creates and returns the new page.

Asynchronous Loading with the XMLHTTP Object

For asynchronous loading, you first have to specify the name of a callback function that will be executed each time the readystate property of the XMLHTTP object changes and specify true for the third parameter of the open method:

oHTTP.onreadystatechange = myCallbackHandler;
oHTTP.open("method", target-url, true);
oHTTP.send();

The callback function you specify will be executed several times as the XMLHTTP object fetches the response from the server. When the response is complete, the value of the readystate property will be 4, and at that point you can test for an error and extract the page as a string:

function myCallbackHandler () {
 if (oHTTP.readyState == 4) {
  if (oHTTP.status == 200)
   sResult = oHTTP.responseText;
  else
   // an error occurred
 }
}
Using the XMLHTTP Object in the Progress Bar Sample Page

Listing 3.5 shows the client-side code included in the progress bar sample page. It works exactly as just demonstrated, with the only additions being a test to see that an instance of the XMLHTTP object was successfully created and the display of any error messages in a <span> element, located below the progress bar graphic in the page.

Listing 3.5 Loading the Results Page with XMLHTTP

<script language='javascript'>
<!--
// variable to hold reference to XMLHTTP object
var oHTTP;

function loadTarget(sURL) {
 // create instance of a new XMLHTTP object
 oHTTP = new ActiveXObject("Microsoft.XMLHTTP");
 if (oHTTP != null) {
  // specify callback for loading completion
  oHTTP.onreadystatechange = gotTarget;
  // open HTTP connection and send async request
  oHTTP.open('GET', sURL, true);
  oHTTP.send();
 }
 else {
  document.all['spnError'].innerText 
   = 'ERROR: Cannot create XMLHTTP object to load next page';
 }
}

function gotTarget() {
 // see if loading is complete
 if (oHTTP.readyState == 4) {
  // check if there was an error
  if (oHTTP.status == 200) {
   // dump next page content into this page
   document.write(oHTTP.responseText);
  }
  else {
   document.all['spnError'].innerText 
    = 'ERROR: Cannot load next page';
  }
 }
}
//-->

Information on the XMLHTTP Object

You can find a full reference to the XMLHTTP object (effectively the XMLHTTPRequest interface) in the MSDN library, at http://msdn.microsoft.com/library/en-us/xmlsdk30/htm/xmobjxmlhttprequest.asp.

One interesting point about this listing is in the gotTarget callback handler. After you've extracted the complete content of the new page as a string, you simply write it into the current browser window, using the client-side document.write method. This replaces the current content, giving the same output as in the first example in this chapter, after the main customer lookup process has completed (refer to Figure 3.5).

What you've actually achieved here is to reload the same page again in the background, while still at Stage 2 of the process (displaying the "please wait" message and progress bar) and then use it to replace the current page. But because the URL you request contains the customer ID in the query string this time, the new page generated by the server will be the one for Stage 3 of the process (containing the DataGrid control, populated with the results of the database search). Altogether, this is a neat and interesting solution!

The Changes to the HTML and Server Control Declarations in This Example

The only remaining features of this example that we need to examine are how to initiate the client-side code that loads the results page and how to handle cases where client-side scripting is disabled in the browser. In the HTML section of the page, you declare the <body> element as a server control this time, by adding an ID and the runat="server" attribute—just as you did for the <meta> element earlier in this chapter:

<body id="tagBody" runat="server">

Then, in the Page_Load event handler, you can add an appropriate onload attribute to the opening <body> tag in the server-side code. Listing 3.6 shows the changed section of the Page_Load event handler. The only section that differs in this example from the first example is the part where the postback from Stage 1 occurs—where you are generating the "please wait" page for Stage 2 of the process.

Listing 3.6 The Page_Load Event Handler for the Progress Bar Example

If Page.IsPostback Then

 Dim sRefreshURL As String = Request.Url.ToString() _
  & "?custID=" & txtCustomer.Text

 ' if it's IE, need to load new page using script because
 ' the META REFRESH prevents the animated GIF working
 If Request.Browser.Browser = "IE" Then
  tagBody.Attributes.Add("onload", "loadTarget('" _
                  & sRefreshURL & "');")

  ' set META REFRESH as well in case script is disabled
  ' use long delay so script can load page first if possible
  mtaRefresh.Attributes.Add("http-equiv", "refresh")
  mtaRefresh.Attributes.Add("content", "30;url=" & sRefreshURL)

 Else

  ' not IE so use META REFRESH to start loading next page
  ' allow 3 seconds for progress bar image to load
  mtaRefresh.Attributes.Add("http-equiv", "refresh")
  mtaRefresh.Attributes.Add("content", "3;url=" & sRefreshURL)

 End If

 frmMain.Visible = False
 divWait.Visible = True

Else
...

You use the ASP.NET Request.Browser object, which exposes a property also named (rather confusingly) Browser. This property indicates the browser type, and if it is "IE", you know that you are serving to an Internet Explorer browser—so we can add the onload attribute to the <body> element by using the Attributes collection of the HtmlGenericControl class that implements it in ASP.NET. The result, when viewed in the browser, looks like this:

<body id="tagBody" onload="loadTarget('/daveandal/books/6744
/loadwait/progressbar.aspx?custID=a');">

You also add a "catch all" feature in case scripting is disabled, by setting the attributes of the <meta> element. In this case, the <meta> element will cause a page reload after 30 seconds. You can also see in Listing 3.6 the changed value of the content attribute that you apply for non–Internet Explorer browsers, to allow the progress bar graphic to load before the redirection commences (as discussed earlier in this chapter).

Checking for the Version of Internet Explorer

In theory, you should test for the browser version as well as the type because the XMLHTTP object is available only in version 5 and higher of Internet Explorer. However, the "catch all" you build in for when scripting is disabled will also make the page work (after a fashion) on earlier versions of Internet Explorer. Whether anyone is still using version 4 or earlier, with all the security issues inherent in those versions, is open to discussion.

Implementing a Staged Page Load Process

We hinted earlier in this chapter that there are ways you can generate "real" status messages in the browser while executing a complex or lengthy operation on the server. Although the technique of simply flushing chunks of content back to the browser as the process runs does work, it's not particularly efficient in terms of connection usage or server loading.

Web servers are designed to receive a connection and resource request, generate the required response, and disconnect as quickly as possible to allow the next user to connect and make a resource request. Because it's likely that most complex operations will involve database access on the server, holding open a connection to the database while you flush chunks of content back to the client is probably not a good idea.

However, if you can break down the complex or lengthy process into separate individual stages, it is possible to provide useful "real" status feedback in the browser. In fact, it's reasonably easy to do this in Internet Explorer 5 and higher, by using the XMLHTTP object used in the previous example.

Flushing Intermediate Content to the Client

Of course, if the process has to access several different data sources to generate the resulting page, as is most likely the case with the MSN Expedia example mentioned earlier in this chapter, you can flush the individual chunks of "status" content to the browser in between opening each connection, extracting the data, and closing it again.

The Steps in Implementing a Staged Page Load Process

Figure 3.7 shows a flowchart of a staged process that is implemented as the next example in this chapter. The main page, named stagedloading.aspx, uses the XMLHTTP component to request a separate operation page, named stagedfetchpage.aspx, four times. Each request contains, in the query string, a customer ID that the user provides and a step value that indicates which stage of the process is currently being performed. The operation page uses these values to collect the appropriate row set from the Northwind database at each stage and add to a DataSet instance a table that is stored in the user's ASP.NET session.

In between requests, the main page can display progress and status information, or it can display any error messages returned by the operation page. When the process is complete in this example, the value returned (the total for all matching orders) is displayed—together with a button that allows the user to view the list of orders. This data is in the DataSet instance stored in the user's ASP.NET session, so it can be extracted and displayed without requiring another trip to the database.

Of course, you can easily tailor this example to display different data at any stage and provide links to access any of the tables in the DataSet instance. In fact, this process opens up a whole realm of opportunities for collecting data of all kinds and combining and then querying it afterward. Figure 3.8 shows a screenshot of the sample page while it is collecting details of orders for all customers whose ID starts with m and building up the DataSet instance.

Figure 3.7Figure 3.7 A flowchart of the steps in implementing a staged page load process.

Figure 3.8Figure 3.8 The staged processing and reporting sample page in action.

You'll learn about this page in more detail shortly, but first you need to see how you can pass status and other information back to the XMLHTTP object. Then you'll see how the operation page, which collects the data and stores it in the user's session, works. After that, you'll see how the main page calls this operation page and how it displays the status information and results.

Accessing Physically or Geographically Separated Data Sources

The set of steps used in this example could easily be performed in one pass. However, using separate stages demonstrates how you could in a more complex scenario access multiple different data sources that could be physically and geographically separated. These data sources might be Web services, XML documents, or other types of data sources—and not just relational databases. For instance, take the MSN Expedia example mentioned earlier: It's likely that the data sources being accessed would be hosted by different airlines, hotels, rental car companies, and so on.

Status Information in ASP.NET and the XMLHTTP Object

When a browser or any other client (such as XMLHTTP) requests an HTML page, the server returns an HTTP status header, followed by the page that was requested. If there is no error (that is, the page can be found and executed by the server), it returns the status header "200 OK".

However, even if the process of loading and executing the page succeeds, you can still control the status code that is returned by setting the Status, StatusCode, and/or StatusDescription properties of the current ASP.NET Response object. The values of these properties will be exposed by the status and statusText properties of the XMLHTTP object after it loads the page (see Table 3.2). You can find a full list of the standard HTTP status codes at http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.

Table 3.2 The Equivalent Status-Related Properties of the ASP.NET Response and XMLHTTP Objects

ASP.NET Response Object Property

XMLHTTP Object Property

Description

Status

No direct equivalent

A combination of the status code and status description (for example, "200 OK" or "302 Object Moved")

StatusCode

status

The numeric part of the status information (for example, 200 or 302)

StatusDescription

statusText

The text or description part of the status information (for example, "OK" or "Object Moved")


By default, the server will automatically set the ASP.NET Status property to "200 OK" if there is no error or to the standard HTTP status code for any error that does occur (for example, "500 Internal Server Error" if there is an ASP.NET code execution error). However, if you trap ASP.NET errors in the code—for example, a failed database connection or a numeric calculation error—you must set the Status property (or the StatusCode and StatusDescription properties) if an error does occur.

The Staged Process Operation Page

The main page that the user sees makes repeated requests to the operation page (stagedfetchpage.aspx), passing the customer ID and the appropriate step number each time. Because it does this by using the XMLHTTP component, the operation page doesn't have to generate any HTML or output. All it has to do is indicate to the main page whether there was an error or whether this step of process succeeded.

However, not all the values you pass back to the XMLHTTP object in this example are strictly status messages; for example, the order value total that is displayed at the end of the process must be returned to the main page. So rather than use the StatusDescription property (statusText in XMLHTTP), you can write these messages directly into the page that is returned. The XMLHTTP object can retrieve this as the responseText property, as shown in the previous example.

The Page_Load Event Handler for the Staged Loading Example

Listing 3.7 shows the Page_Load event handler in the operation page, together with the page-level variable that holds a reference to the DataSet instance stored in the session. The values for the customer ID and the current step are collected from the query string each time the page loads.

Listing 3.7 The Page_Load Event Handler for the Staged Loading Example

Dim oDS As DataSet

Sub Page_Load()

 Dim sCustID As String = Request.QueryString("custID")
 Dim sStep As String = Request.QueryString("step")
 Dim sSelect As String

 ' force current thread to sleep for 3 seconds
 ' to simulate complex code execution
 Thread.Sleep(3000)

 Select Case sStep
  Case "1"
   oDS = New DataSet()
   sSelect = "SELECT CustomerID, CompanyName, City, " _
    & "Country, Phone FROM Customers " _
    & "WHERE CustomerID LIKE @CustomerID"
   AddTable("Customers", sCustID, sSelect)
  Case "2"
   oDS = CType(Session("thedata"), DataSet)
   sSelect = "SELECT OrderID, OrderDate FROM Orders " _
    & "WHERE CustomerID LIKE @CustomerID"
   AddTable("Orders", sCustID, sSelect)
  Case "3"
   oDS = CType(Session("thedata"), DataSet)
   sSelect = "SELECT [Order Details].OrderID, " _
    & "Products.ProductID, Products.ProductName, " _
    & "[Order Details].Quantity, [Order Details].UnitPrice " _
    & "FROM [Order Details] JOIN Products " _
    & "ON [Order Details].ProductID = Products.ProductID " _
    & "WHERE [Order Details].OrderID IN " _
    & " (SELECT OrderID FROM Orders " _
    & " WHERE CustomerID LIKE @CustomerID)"
   AddTable("OrderDetails", sCustID, sSelect)
  Case "4"
   oDS = CType(Session("thedata"), DataSet)
   CalculateTotal()
  Case Else
   Response.Status = "500 Internal Server Error"
   Response.Write("Error: Invalid Query String Parameter")
 End Select

End Sub

Next, to simulate a long process, you force the current thread to sleep for 3 seconds (as you did in the "please wait" example) before using the step value from the query string to decide which action the page will carry out. The first three stages of the operation must create and execute a database query to extract the appropriate set of rows and then add these to the DataSet instance in the user's session. The AddTable routine, which you'll see shortly, achieves this. Obviously, you have to a create new DataSet instance at Stage 1, but the remaining stages can extract this DataSet instance from the user's session.

At Stage 4 in this example, the operation page has to calculate the order total and return it to the main page, using the routine CalculateTotal (which you'll see shortly). Any value greater than 4 for the step parameter is treated as an error, and the page returns the server-side execution error "500 Internal Server Error". A more detailed error message is also sent back as the content of the returned page.

Accessing the Customer ID Value

The value of the customer ID entered into the text box cannot be extracted directly as the Text property of the ASP.NET TextBox control when this page is executed. The page is loaded with the "GET" method by the XMLHTTP object, with the customer ID appended to the query string, so it must be collected from there each time.

What Happens if Cookies Are Disabled?

The sample page will fail to work properly if the user has cookies disabled in his or her browser because ASP.NET will not be able to maintain a user session. One solution would be to enable cookieless sessions by adding the element <sessionState cookieless="true" /> to the <system.web> section of the web.config file for the application. In this case, you must also modify the src attribute of the non–server control <img> elements to specify the full path to the images because the inclusion of the session key in the page URL breaks the links to images that are specified only as relative paths from the URL of the page that hosts them.

Adding Tables to the DataSet Instance

Adding a table to the DataSet instance you extract from the user's session is simple, and the code in Listing 3.8 demonstrates the traditional techniques you use. Notice that, in this code, you check whether you actually managed to find a DataSet instance in the session, and you return an error status and message if not. After adding the table, you push the updated DataSet instance back into the session. If there is an error while extracting the rows, a suitable error status and message are returned to the user instead.

Listing 3.8 The AddTable Routine for the Staged Loading Example

Sub AddTable(sTableName As String, sCustID As String, _
       sSelect As String)

 If oDS Is Nothing Then

  Response.Status = "500 Internal Server Error"
  Response.Write("Error: Cannot access DataSet in session")

 Else

  Dim sConnect As String = ConfigurationSettings.AppSettings( _
                "NorthwindSqlClientConnectString")
  Dim oConnect As New SqlConnection(sConnect)
  Dim oDA As New SqlDataAdapter(sSelect, oConnect)
  oDA.SelectCommand.Parameters.Add("@CustomerID", sCustID & "%")

  Try

   ' fill table in DataSet and put back into session
   oDA.Fill(oDS, sTableName)
   Session("thedata") = oDS
   Response.Status = "200 OK"
   Response.Write("OK")

  Catch oErr As Exception

   Response.Status = "500 Internal Server Error"
   Response.Write("Error: " & oErr.Message)

  End Try

 End If

End Sub

Calculating the Total Value of the Orders

The final section of the operation page in the staged loading example is shown in Listing 3.9. This simply references the OrderDetails table in the DataSet instance and sums the values in each row by multiplying the quantity by the unit price. The result is written back to the response as a fixed-point number with two decimal places.

Listing 3.9 The CalculateTotal Routine for the Staged Loading Example

Sub CalculateTotal()

 Dim dTotal As Decimal = 0

 Try

  For Each oRow As DataRow In oDS.Tables("OrderDetails").Rows
   dTotal += (oRow("Quantity") * oRow("UnitPrice"))
  Next

  Response.Status = "200 OK"
  Response.Write(dTotal.ToString("F2"))

 Catch oErr As Exception

  Response.Status = "500 Internal Server Error"
  Response.Write("Error: " & oErr.Message)

 End Try

End Sub

The Staged Process Main Page in the Staged Loading Example

Now that you have seen how the operation page performs the updates to the DataSet instance and returns status and information messages, you can now look at the main page that calls this operation page at each stage of the overall process. Listing 3.10 shows the HTML content of the main page. You can see that there is an ASP.NET TextBox control for the user to enter the full or partial customer ID and an <input> element that creates the submit button captioned Calculate.

Listing 3.10 The HTML Declarations for the Main Page in the Staged Loading Example

<form runat="server">

 <!----- form for selecting customer ----->
 <asp:Label id="lblEnter" runat="server" 
       Text="Enter Customer ID:" />
 <asp:Textbox id="txtCustomer" runat="server" /><br />
 <input id="btnGo" type="submit" value="Calculate"
     onclick="return getResults();" runat="server"/>

 <!----- "please wait" display ----->
 <table border="0">
 <tr>
  <td><img id="img1" src="../images/False.gif" width="12" 
       height="12" hspace="5" /></td>
  <td><span id="spn1">Loading Customer Data</span></td>
 </tr><tr>
  <td><img id="img2" src="../images/False.gif" width="12" 
       height="12" hspace="5" /></td>
  <td><span id="spn2">Loading Orders Data</span></td>
 </tr><tr>
  <td><img id="img3" src="../images/False.gif" width="12" 
       height="12" hspace="5" /></td>
  <td><span id="spn3">Loading Order Details</span></td>
 </tr><tr>
  <td><img id="img4" src="../images/False.gif" width="12" 
       height="12" hspace="5" /></td>
  <td><span id="spn4">Calculating Total</span></td>
 </tr>
 </table>

 <!----- section for displaying total ----->
 <div id="divResult">
  <b><span id="spnResult"></span></b><p />
 </div>

 <!----- section for displaying orders ----->
 <div id="divOrderList">
  <asp:Button id="btnOrders" style="visibility:hidden"
     Text="Show Orders" OnClick="ShowOrders" runat="server" />
  <asp:DataGrid id="dgrOrders" EnableViewState="False" 
         runat="server" /><p />
 </div>

</form>

<img id="imgTrue" style="visibility:hidden" 
   src="../images/True.gif" />
<img id="imgThis" style="visibility:hidden" 
   src="../images/This.gif" />

You use the HTML <input> element here because this is easier to connect to a client-side event handler than the ASP.NET Button element. (You don't have to add the onclick attribute on the server via the Attributes collection.) You always return false from the event handler that is attached to this button because you must prevent it from submitting the page to the server.

Declaring the Button as a Server Control

You could omit the runat="server" attribute from the button. This would mean that the <input> element would not be a server control. However, you want to be able to hide the button if the browser is not Internet Explorer 5 or higher, and, because you perform this check on the server side when the page loads (as you'll see shortly), you need to be able to reference it in the server-side code.

You could also use the HTML <button> element instead of the <input> element. The <button> element is not supported in all browsers, but because this page will work only in Internet Explorer (where it is supported), this would not be an issue.

The HTML table that follows the text box and button contains an <img> element and a <span> element for each stage of the process. The client-side code that executes the operation page will update the src attribute of the <img> element to change the image that is displayed and the font-weight style selector of the text as each stage takes place.

The other two sections of the page are a <div> section, where any error messages and the final order total will be displayed as each stage of the process executes, and another <div> section, where the list of orders is displayed if the user clicks the Show Orders button. You'll learn about this aspect of the sample page after you see how it performs the initial four stages of calculating the order total.

Finally, right at the end of the page are two more <img> elements that are hidden from view with the visibility:hidden style selector. You use these to preload the images for the list of operation stages. You display the image named This.gif (a right-pointing arrow) for each stage as it starts and then replace it with the image True.gif (a large check mark) if it completes successfully. You can see these two images in Figure 3.8.

Displaying the Current Operation Progress in the Staged Loading Example

Listing 3.11 shows the two client-side JavaScript functions you use to manipulate the progress indicators in the page. As each stage of the process is started, you make a call to the setCurrent function. As each stage completes, you call the setCompleted function. In both cases, you supply the stage number (a value from 1 to 4 in this example) as the single parameter.

Listing 3.11 The Client-Side Routines to Display Operation Progress in the Staged Loading Example

function setCurrent(iStep) {
 // get reference to image and change to "arrow"
 // using image pre-loaded in hidden <img> element
 var oImg = document.getElementById('imgThis');
 var oElem = document.getElementById('img' + iStep.toString());
 oElem.src = oImg.src;
 // get reference to span and change text to bold
 oElem = document.getElementById('spn' + iStep.toString());
 oElem.style.fontWeight = 'bold';
}

function setCompleted(iStep) {
 // get reference to image and change to "tick"
 // using image pre-loaded in hidden <img> element
 var oImg = document.getElementById('imgTrue');
 var oElem = document.getElementById('img' + iStep.toString());
 oElem.src = oImg.src;
 // get reference to span and change text back to normal
 oElem = document.getElementById('spn' + iStep.toString());
 oElem.style.fontWeight = '';
}

The code in the setCurrent and setCompleted functions is very similar. It starts by getting a reference to the preloaded and hidden <img> element that contains either the arrow image (This.gif) or the check mark image (True.gif).

The <img> and <span> elements that indicate the four process stages shown in the page have values for their id attributes that indicate which stages they apply to. For example, the first stage uses the id attributes "img1" and "spn1", respectively, for the <img> and <span> elements. So the code can get references to the correct elements by using the step number passed to it as a parameter.

With these references, it's then just a matter of updating the src property of the <img> element to display the appropriate image and setting the style.fontWeight property of the <span> element.

Executing the Operation Page with XMLHTTP

Listing 3.12 shows the code that executes the operation page discussed earlier in this chapter. Three page-level variables are declared to hold references to items that will be accessed from separate functions: the <span> element, where the status and any error messages are displayed, the XMLHTTP object, and the customer ID that the user entered.

Listing 3.12 The Client-Side Routines to Execute the Operation Page

var oResult;
var oHTTP;
var sCustID;

function getResults() {
 // get reference to "result" label and texbox value
 oResult = document.getElementById('spnResult');
 var oTextbox = document.getElementById('txtCustomer');
 sCustID = oTextbox.value;
 if (! sCustID == '') {
  // hide DataGrid control
  var oElem = document.getElementById('dgrOrders');
  if (oElem != null) oElem.style.visibility = 'hidden';
  // get Customers data
  fetchData(1)
 }
 else
  oResult.innerText = 'No customer ID specified';
 // return false to prevent button from submitting form
 return false;
}

function fetchData(iStep) {
 // create instance of a new XMLHTTP object because we
 // can't change readystate handler on existing instance
 oHTTP = new ActiveXObject('Microsoft.XMLHTTP');
 if (oHTTP != null) {
  // update status display and build data page URL
  setCurrent(iStep);
  var sURL = 'stagedfetchpage.aspx?custid=' + sCustID
       + '&step=' + iStep.toString();
  // set correct handler for XMLHTTP instance
  switch (iStep) {
   case 1: {
    oHTTP.onreadystatechange = gotCustomers;
    break;
    }
   case 2: {
    oHTTP.onreadystatechange = gotOrders;
    break;
    }
   case 3: {
    oHTTP.onreadystatechange = gotDetails;
    break;
    }
   case 4: {
    oHTTP.onreadystatechange = gotTotal;
    }
  }
  // open HTTP connection and send async request
  oHTTP.open('GET', sURL, true);
  oHTTP.send()
 }
 else
  oResult.innerText = 'Cannot create XMLHTTP object';
}

Next comes the main getResults function, which is executed when the Calculate button is clicked. It collects a reference to the <span> element that will hold the results, along with the customer ID that the user entered into the text box on the page. If there is a value here, it hides the DataGrid control that could still be displaying the list of orders from a previous query, and then it calls the fetchData function with the parameter set to 1 to perform Stage 1 of the process. If there is no customer ID, it just displays an error message instead.

The fetchData function (also shown in Listing 3.12) will be called at each stage of the process, starting—as you've just seen—with Stage 1. This function's task is to create an instance of the XMLHTTP object and execute the operation page with the correct combination of values in the query string. It first checks that an instance of XMLHTTP was in fact created, and then it calls the setCurrent function shown in Listing 3.11 to update the status display in the page. Then it creates the appropriate URL and query string for this stage of the process.

However, recall that you have to access the operation page asynchronously to allow the main page to update the status information, so you must specify a client-side event handler for the readystatechange event of the XMLHTTP object. The page contains four event handlers, and you select the appropriate one by using a switch statement before opening the HTTP connection and calling the send method of the XMLHTTP object to execute the operation page.

Handling the XMLHTTP readystatechange Events

Listing 3.13 shows the four event handlers that are declared in the switch statement in Listing 3.12. They are all very similar, and by looking at the first of them, gotCustomers, you can see that they do nothing until the loading of the operation page is complete (when the readystate property is 4). Then, if the status code returned from the operation page is 200 ("OK"), they call the setCompleted function shown in Listing 3.11 to indicate that this stage completed successfully. If any other status code is returned, the code displays the value of the responseText property (the content of the page returned, which will be the error details) in the page.

Listing 3.13 The Event Handlers for the XMLHTTP readystatechange Event

function gotCustomers() {
 // see if loading is complete
 if (oHTTP.readyState == 4) {
  // check if there was an error
  if (oHTTP.status == 200) {
   // update status display and fetch next set of results
   setCompleted(1);
   fetchData(2);
  }
  else
   oResult.innerText = oHTTP.responseText;
 }
}

function gotOrders() {
 // see if loading is complete
 if (oHTTP.readyState == 4) {
  // check if there was an error
  if (oHTTP.status == 200) {
   // update status display and fetch next set of results
   setCompleted(2);
   fetchData(3);
  }
  else
   oResult.innerText = oHTTP.responseText;
 }
}

function gotDetails() {
 // see if loading is complete
 if (oHTTP.readyState == 4) {
  // check if there was an error
  if (oHTTP.status == 200) {
   // update status display and fetch next set of results
   setCompleted(3);
   fetchData(4);
  }
  else
   oResult.innerText = oHTTP.responseText;
 }
}

function gotTotal() {
 // see if loading is complete
 if (oHTTP.readyState == 4) {
  // check if there was an error
  if (oHTTP.status == 200) {
   // update status display
   setCompleted(4);
   // display result in page and show Orders button
   oResult.innerText = 'Total value of all orders $ '
            + oHTTP.responseText;
   var oElem = document.getElementById('btnOrders');
   oElem.style.visibility = 'visible';
  }
  else
   oResult.innerText = oHTTP.responseText;
 }
}

As each stage completes, the code must initiate the next stage. In the first three event handlers (shown in Listing 3.13), this just involves calling the fetchData function (shown in Listing 3.12) again—but with the next stage number as the parameter. The instance of the XMLHTTP object that is created will then have the event handler for the next stage attached to the readystatechange event.

At Stage 4, when the gotTotal function is called after the operation page has successfully calculated and returned the total value of matching orders, the responseText property will return the total as a string. The function displays this value in the page and then changes the visibility style selector of the Show Orders button to make it visible. However, if there is an error, the error message is displayed instead.

Figure 3.9 shows the sample page after the four steps have completed successfully. You can see that the order total is displayed and the Show Orders button is now visible as well.

Figure 3.9Figure 3.9 The sample page, after successfully processing all the stages.

Fetching and Displaying a List of Orders

After the four stages of the process in the staged loading example have completed successfully, the user's session contains a DataSet instance that is fully populated with lists of matching customers, orders, and order details rows from the database. This means that you can easily display some or all of the results of the four-stage process (as well as the total already displayed in the page) by querying this DataSet instance—without having to hit the database again.

The Show Orders button (refer to Figure 3.9), which appears only after all four stages of the operation are complete, runs a server-side routine that extracts a list of order lines from the DataSet instance and displays them in the DataGrid control included in the HTML declarations of the page. Figure 3.10 shows the result.

Figure 3.10Figure 3.10 The sample page, displaying the list of orders from the cached DataSet instance.

Why Do the Check Mark Images Disappear?

Notice that the check mark images disappear from the page following the postback that populates the DataSet instance. Remember that unlike changes made in server-side ASP.NET code, any changes made to the page displayed in the browser using client-side script are not persisted across postbacks.

The Server-Side Code in the Staged Process Main Page

Most of the action in the main page in the staged loading example is the result of the client-side script examined in the previous section. However, two tasks require server-side code. Because the page will work only in Internet Explorer 5 and higher, you really should make some attempt to test the browser type and display an error message in other browsers. Second, you need to handle click events for the Show Orders button and populate the DataGrid control that displays the list of order lines.

Listing 3.14 shows the complete server-side code for the main page. In the Page_Load event, you can access the BrowserCapabilities object that is exposed by the Request.Browser property and test the browser name and version. If the browser is not Internet Explorer 5 or higher, you display an error message and hide the text box and Calculate button so that the page cannot be used.

Listing 3.14 The Server-Side Page_Load and ShowOrders Event Handlers

Sub Page_Load()
 ' check that the browser is IE 5 or higher
 If Request.Browser.Browser <> "IE" _
 Or Request.Browser.MajorVersion < 5 Then
  ' display message and hide input controls
  lblEnter.Text = "Sorry, this page requires Internet Explorer 5 or higher"
  txtCustomer.Visible = False
  btnGo.Visible = False
 End If
End Sub

Sub ShowOrders(sender As Object, args As EventArgs)

 ' bind DataGrid to contents of DataSet in user's Session
 dgrOrders.DataSource = CType(Session("thedata"), DataSet)
 dgrOrders.Datamember = "OrderDetails"
 dgrOrders.DataBind()

End Sub

When the Show Orders button is clicked (after the four stages of the process in the sample page are complete), the routine named ShowOrders is executed. This simply accesses the DataSet instance stored in the user's session, binds the OrderDetails table to the DataGrid control, and calls the DataBind method.

Catching and Displaying Errors from the Operation Page

The code shown in the preceding sections is designed to cope with any errors that might occur in the operation page, which does the real work of querying the database and building up the DataSet instance that contains all the results. As with any database operation, there is a possibility that something will go wrong—from a failed connection to changed permissions within the tables, changed column names, or even network failure if the database server is remote from the Web server.

Making the Staged Process Work in Other Browsers

The staged loading example absolutely requires that the MSXML parser be available on the client and so it works only in Internet Explorer 5 and higher. However, it could be implemented in other browsers (and different types of clients), using other suitable client-side software components. There are Java applets available that could be used in other browsers, or you could create your own Java applet or ActiveX controls. The main issue will be persuading the user to install these. Although this solution would be fine on an intranet where you can install the code on each machine and keep control, users out there on the Internet might be less keen to download unknown components and allow them to run.

As you've seen, the operation page returns one of the standard HTTP status codes each time, and it writes output into the page it generates. This content consists of just the text "OK" for the first three stages (where the DataSet instance is being created), but this text is not displayed in the main page. However, if there is an error within the operation page, the XMLHTTP object detects it because the status code is not 200, and it displays the contents of the returned page.

As an example, if you change the SQL statement used for Stage 3 (extracting the order details) so that it references a non-existent column in the database, the Try...Catch construct in the operation page code (refer to Listing 3.8) catches the error. It returns the status code "500 Internal Server Error" and the text "Error:", followed by the error message (as returned by ASP.NET when the data access operation failed) as the content of the page. The client-side code then displays the returned page content, as shown in Figure 3.11.

Figure 3.11Figure 3.11 The sample page, reporting a data access error.

Although it's taken a while to examine the code used in this example, you can see that it is not really very complicated. It allows you to create and manage staged processes that provide accurate feedback to users and that can manage errors and display useful status information.

Summary

This chapter is devoted to the topic of finding ways to present users with status information while a complex or lengthy process is taking place. This chapter looks at two different approaches: displaying a simple "please wait" message or animated GIF image and implementing the server-side process as a series of staged individual operations.

The first of these techniques doesn't really provide feedback because the user is just looking at what is effectively the shadow of the last page that the browser displayed. Underneath, it is waiting for a response from the server. However, displaying a message indicating that the user should wait gives the impression that something really is happening. And removing from the page any buttons or other controls that the user might be tempted to play with prevents the page from being resubmitted and upsetting your server-side code.

This chapter also shows how you can improve on the simple "please wait" text message by using an animated GIF image—in this case, a progress bar. By choosing an image that progresses at a rate matching the average page load time, you can make it look as though your server is working flat out to satisfy their request.

Displaying a progress bar image should be a simple task, but as you discovered, there are issues that arise. (And they say that Web development is child's play!) You ended up having to find two different solutions: one for Internet Explorer and another for other types of browsers. This gave you the opportunity to look into how you can load pages in the background by using the XMLHTTP object that is part of the standard installation of Internet Explorer 5 and above.

Finally, this chapter looks at a process that uses the XMLHTTP object to implement a staged execution and page loading process. This is a really neat solution for an application that has to perform separate tasks to build up the final page that is returned to the client. And, of all the techniques examined in this chapter, this one alone has the advantage of providing accurate real-time status information as the server processes proceed.

If you decide to follow the asynchronous page-loading route, you might like to look at an implementation designed for the .NET Framework by Microsoft, called the Asynchronous Invocation Application Block for .NET. See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/paiblock.asp for more details.

800 East 96th Street, Indianapolis, Indiana 46240