Design Patterns

Asynchronous Wait State Pattern in ASP.NET

Lyn Robison

Contents

The Long Run
A Long Process Begins with a Single Step
Please Wait...
Redirection
Making Excellent Progress
Cleaning Up the Code
A Pattern Emerges
Conclusion

Putting a browser UI on business applications makes them easily accessible to users in remote locations, but it presents some challenges for the developer. One of those challenges is how best to construct it for a long-running process.

For example, when a user enters data into a Web Form and clicks the submit button, the page does not change and the UI is unresponsive until the server sends a new page to the browser. If the UI is unresponsive for more than a few seconds, users are inclined to press the submit button repeatedly. These multiple submissions can cause problems for the application, forcing the developer to write code to handle the repeats.

A cleaner approach would be to keep the user informed about the server process with updated messages and a progress bar in the browser. In this column, you will learn how to create an asynchronous ASP.NET wait page that does exactly that. Let's begin by creating a page that initiates a long-running process. (The samples in this article are written in C# unless otherwise noted.)

The Long Run

To experience the need for a wait page, create a page named ProcessTheData.aspx. This page will contain two label controls: one for showing the results of a process, and another displaying the text of the exception, only visible if an exception has occurred.

Figure 1 shows the design view of the ProcessTheData.aspx page inside Visual Studio® .NET. As you can see, I applied a stylesheet to give the page a little polish.

Figure 1 Design View

Figure 1** Design View **

The Page_Load event of ProcessTheData.aspx instantiates a business object named BusinessService and calls a method named ProcessMyData on that object. As you can see in Figure 2, the code in the Page_Load event takes the contents of the query string variable named Data, passes it to the ProcessMyData method, then sets the Text property of ResultLabel to show the results of that method call. If the ProcessMyData method encounters any exceptions, it places the exception message in the processedString out parameter and returns false. The code in the Page_Load event uses the ErrorLabel control to show the exception message on the page.

Figure 2 Page_Load Event

private void Page_Load(object sender, System.EventArgs e) { if (!IsPostBack) { if (null == Request.QueryString["Data"] || 0 == Request.QueryString["Data"].Length) { ErrorLabel.Text = "You must provide data for processing. " + "<BR> Press the Back button to enter some data."; ErrorLabel.Visible = true; } else { string inputString, processedString; inputString = Request.QueryString["Data"]; if ((new BusinessService()). ProcessMyData(inputString, out processedString)) { ResultLabel.Text = processedString; } else { ErrorLabel.Text = processedString; ErrorLabel.Visible = true; } } } }

The code in ProcessMyData is very simple. It processes the data by converting it to uppercase. To turn this operation into a long-running process, I added a Thread.Sleep(30000) call to make ProcessMyData take 30 seconds (see Figure 3).

Figure 3 ProcessMyData

public bool ProcessMyData(string DataIn, out string DataOut) { try { DataOut = DataIn.ToUpper(); System.Threading.Thread.Sleep(30000); return true; } catch (Exception except) { DataOut = except.Message; return false; } }

It is easy to imagine calling a database query or performing some other real-world operation that might take seconds or even minutes to complete.

You can test the ProcessTheData.aspx page from the server with a URL such as https://localhost/MyWebApp/ProcessTheData.aspx?Data=test. When you test the ProcessTheData.aspx page, you will see that because of the Thread.Sleep(30000) call, the page will take at least 30 seconds to display.

A Long Process Begins with a Single Step

Next, create another page that opens this page and launches this process. Create a page named StartTheProcess.aspx and place a Hyperlink control on it with a Text property of

Process the Data

and a NavigateUrl property of:

ProcessTheData.aspx?Data=test

Test the StartTheProcess.aspx page by clicking on the hyperlink. You will see that it takes at least 30 seconds for the ProcessTheData.aspx page to display.

Add a TextBox control and a Button control to the StartTheProcess.aspx page. The button control redirects to ProcessTheData.aspx, passing the contents of the TextBox to be processed, as shown in the following code:

private void ButtonData_Click(object sender, System.EventArgs e) { Response.Redirect("ProcessTheData.aspx?Data=" + TextBoxData.Text); }

My StartTheProcess.aspx page looks like Figure 4 in the Visual Studio .NET design view.

Figure 4 The ASPX Page

Figure 4** The ASPX Page **

When you click on the button, the browser will be redirected to the ProcessTheData.aspx page. If you have not entered any text in the textbox, the ProcessTheData.aspx page will say "You must provide data for processing." If you have entered text in the textbox, the ProcessTheData.aspx page will display that text in uppercase, after 30 or more seconds.

You will notice that when you click the button, the UI does not respond and the page does not change until the new page arrives from the server. Clicking the button repeatedly actually lengthens the time that you must wait for the ProcessTheData.aspx page to arrive. This is a problem for users because their wait gets longer, as well as a problem for the developer because repeated submissions can cause extraneous data to be submitted to the database. A wait page can solve the problem.

Please Wait...

One way to implement a wait page would be to use a client-side script that disables the submit button once it has been pressed. This script might also render a progress bar until the new page arrives from the server. However, there are a couple of problems with this approach.

First, client-side wait scripts embedded in your Web Forms might conflict with validator controls you might be using. Second, a client-side script could be impractical for a process that consists of multiple steps on the server. In other words, a server-side wait page could be updated more easily from the server at each step, providing a more responsive UI.

Let's take a look at how a server-side ASP.NET wait page might work. Instead of redirecting the submit button on the StartTheProcess.aspx page directly to ProcessTheData.aspx, it could redirect to the wait page and pass ProcessTheData.aspx as a query string, as shown in the following code:

private void ButtonData_Click(object sender, System.EventArgs e) { Response.Redirect("Wait.aspx?redirectPage=" + "ProcessTheData.aspx&Data=" + TextBoxData.Text + "&secondsToWait=30"); }

The wait page redirects to the destination page that is specified in the redirectPage query string variable. Then it passes the rest of the query string variables to the destination page. The wait page displays a progress bar, and the secondsToWait query string variable specifies the ultimate length of that progress bar. At this point, the page goes away from the user's browser as soon as the destination page comes up.

How do you create this type of wait page? There are two major features that you must implement: the progress bar and the redirect functionality to load the destination page. Let's look at how to create the redirect first.

Redirection

I will talk about two ways to code an automated redirect. One option is to use an HTTP response header to cause the browser to automatically redirect itself after a certain amount of time. Here is an example of the HTML to do this:

<HTML> <HEAD> <META HTTP-EQUIV="Refresh" CONTENT="5; URL=ProcessTheData.aspx"> </HEAD>

As you can see, a META tag with the HTTP-EQUIV element, which has a value of "Refresh," is included in the <HEAD> section of the Web page. The CONTENT element contains the number of seconds that the page should wait, followed by the URL of the page to which the browser is to be redirected. In this example, after five seconds the browser will request the ProcessTheData.aspx page from the server.

To implement this type of wait page, create a C# Web Form named Wait.aspx. Add an ASP.NET Label control named ProcessingLabel that says "Processing...". Add the following META tag in the <HEAD> section of the Wait.aspx page:

<META HTTP-EQUIV="Refresh" CONTENT="5; URL=ProcessTheData.aspx">

View the Wait.aspx page in your browser, and you will see that after five seconds, the ProcessTheData.aspx page will be displayed. (The page should indicate that you did not provide any data in order for it to process.)

Of course, because this Wait.aspx page is hardcoded to always redirect to the ProcessTheData.aspx page, it is of limited usefulness. Let's make the wait page a bit more flexible. Instead of hardcoding the URL in the CONTENT element of the HTTP-EQUIV META tag, change it so that the value of the URL is generated by C# code, like this:

<META HTTP-EQUIV="Refresh" CONTENT= "5; URL=<%Response.Write(redirectPage+Request.Url.Query);%>">

This code sets the URL value of the CONTENT element to the contents of a member variable (of the Wait.aspx page's Wait class) named redirectPage, followed by whatever query string was passed to the wait page.

To make this work, add the following few lines of code to your Wait.aspx.cs file:

protected string redirectPage = ""; private void Page_Load(object sender, System.EventArgs e) { if (null != Request.QueryString["redirectPage"]) { redirectPage = Request.QueryString["redirectPage"]; } }

This code snippet declares the member variable named redirectPage in the Wait.aspx page's Wait class, and in the Page_Load event sets the redirectPage member variable to the contents of the redirectPage query string variable. So, when the page is loaded, the redirectPage member variable of the Wait.aspx page's Wait class is set to the destination page where the browser is to be redirected; when the HTML from the page renders, the URL value of the CONTENT element is set appropriately.

You can see that the META tag uses Request.Url.Query to append the entire query string that was passed to the wait page to the name of the destination page. You can test this procedure by making the button's click event on the StartTheProcess.aspx page match the code shown here:

private void ButtonData_Click(object sender, System.EventArgs e) { Response.Redirect("Wait.aspx?redirectPage=" + "ProcessTheData.aspx&Data=" + TextBoxData.Text + "&secondsToWait=30"); }

You will notice that when you click the submit button, five seconds pass before the wait page requests the ProcessTheData.aspx page from the server. This is because after the CONTENT element, the META tag specified that the page should wait five seconds. It then takes 30 or more seconds for the ProcessTheData.aspx page to arrive from the server.

This five-second delay is not ideal, but it mitigates a problem. If you press the browser's Back button from the ProcessTheData.aspx page, you will notice that you go back to the wait page. You have to press the browser's Back button again to get all the way back to the StartTheProcess.aspx page. If you don't get off the wait page, it will request the ProcessTheData.aspx page again after five seconds have elapsed. If it were not for the five-second delay on the wait page—say it was set to zero—you would not be able to get back to the StartTheProcess.aspx page using the browser's Back button because the wait page would immediately request the ProcessTheData.aspx page once again.

I mentioned earlier that I would talk about two ways to code an automated redirect. This process of using the HTTP response header with a META tag and the HTTP-EQUIV element tends to work, but is not ideal because of the Back button problem. The second option is to use the replace method of the DHTML location object. The location.replace method avoids the dreaded Back button problem by only keeping the destination page in the browser's history. The location.replace method is supported in Microsoft® Internet Explorer 4.0 and above and Navigator 4.0 and above, which means that most browsers that are in use today support it.

To implement the location.replace method, first remove the HTTP-EQUIV META tag from the <HEAD> section of your wait page. Then change the opening BODY tag of your wait page so that it looks like this:

<body id="b1" onload="redirectTo( '<%Response.Write(redirectPage);%>', '<%Response.Write(Request.Url.Query);%>');" MS_POSITIONING="GridLayout">

You should note that the setting of the MS_POSITIONING element is not at all relevant.

In the BODY tag of your wait page, in the body's onload event, you are calling a client-side script function named redirectTo. Add the code for the redirectTo function in the <HEAD> section of your wait page. The client-side script code should look like this:

<SCRIPT language="javascript"> function redirectTo(targetPage, querystring) { if (0 < targetPage.length) { b1.style.cursor="wait"; location.replace(targetPage + querystring); } } </SCRIPT>

Now test your wait page from your StartTheProcess.aspx page, and you will see that it redirects to the ProcessTheData.aspx page without that five-second delay. In other words, it takes 30 or more seconds instead of 35 or more seconds for the ProcessTheData.aspx page to come up. You will also see that you do not have to press the Back button twice from the ProcessTheData.aspx page to get back to the StartTheProcess.aspx page. The wait page does not get in the way of the Back button because the location.replace method replaces the wait page in the history with the ProcessTheData.aspx page and prevents that vicious cycle.

So, you have two ways to code an automatic redirect: the HTTP-EQUIV META tag and the DHTML location.replace method. The HTTP-EQUIV META tag seems like it might be a better choice when you want to display a "This page/site has been moved" page, which pauses for a set time and then redirects to the new location. The location.replace method tends to provide a cleaner implementation for the type of wait page that I'm creating.

Now let's tackle the progress bar.

Making Excellent Progress

To begin implementing a progress bar in your wait page, add these two members to your Wait class in Wait.aspx.cs:

protected string secondsToWait = "0"; protected string minutesToWait = "0";

Also add the code in Figure 5 to the Page_Load event of your wait page. The code in Figure 5 sets the secondsToWait and the minutesToWait values appropriately. When calling the wait page, you can specify the number of seconds or minutes to wait. If you specify minutesToWait, the code disregards secondsToWait because the seconds are generally not significant if you are waiting several minutes.

Figure 5 SecondsToWait and MinutesToWait

if (null != Request.QueryString["secondsToWait"]) { secondsToWait = Request.QueryString["secondsToWait"]; if ((Int32.Parse(secondsToWait) % 60 == 0) && ((Int32.Parse(secondsToWait)) / 60) > 1) { int minutes = Int32.Parse(secondsToWait) / 60; minutesToWait = minutes.ToString(); secondsToWait = "0"; } } if (null != Request.QueryString["minutesToWait"]) { secondsToWait = "0"; minutesToWait = Request.QueryString["minutesToWait"]; }

Change your wait page's HTML body tag to look like this:

<body id=b1 onload="redirectTo('<%Response.Write(redirectPage);%>', '<%Response.Write(Request.Url.Query);%>', <%Response.Write(secondsToWait);%>, <%Response.Write(minutesToWait);%>);"> <form id="Form1" method="post" runat="server"> <asp:Label id="ProcessingLabel" runat="server" CssClass="DarkMediumSizeText">Processing...</asp:Label> <p></p> <DIV id="d1" class="DarkSmallSizeText"></DIV> <DIV id="d2" style="BACKGROUND-COLOR:red"></DIV> </form> </body>

You can see that I added parameters to the redirectTo function for the secondsToWait and minutesToWait values. I also added two DIV tags with ids of d1 and d2. The d1 DIV will show the number of seconds that have elapsed since the wait page came up; the d2 DIV will contain a red progress bar. I used a couple of CSS classes named DarkMediumSizeText and DarkSmallSizeText, which you can implement as you like. The code for the completed redirectTo function is shown in Figure 6.

Figure 6 redirectTo and timedIterations

<SCRIPT language="javascript"> var i = 0; function redirectTo(targetPage, querystring, secondsForWaiting, minutesForWaiting) { if (0 < targetPage.length) { location.replace(targetPage + querystring); b1.style.cursor="wait"; if (secondsForWaiting.valueOf() > 0) { ProcessingLabel.innerText = "This process can take up to " + secondsForWaiting + " seconds..."; timedIterations(secondsForWaiting); } else { if (minutesForWaiting.valueOf() > 0) { ProcessingLabel.innerText = "This process can take up to " + minutesForWaiting + " minutes..."; timedIterations(minutesForWaiting * 60); } } } else { ProcessingLabel.innerText = "Page not found." } } function timedIterations(secondsForIterating) { incrementalWidth = 800 / secondsForIterating; if (i <= secondsForIterating + 10) { d1.innerText="Elapsed time: " + i + " seconds."; d2.style.width=i*incrementalWidth; setTimeout( "timedIterations(" + secondsForIterating + ");", 1000); i++; } else { b1.style.cursor=""; d1.style.visibility = "hidden"; d2.style.visibility = "hidden"; ProcessingLabel.innerText = "The server is taking longer than " + "anticipated to process your request. " + "Thank you for your patience. " + "You can wait a few minutes longer for " + "the process to complete, or you can press " + "the back button and try again later..."; } } </SCRIPT>

Here's a brief summary of what the code in Figure 6 accomplishes. After calling location.replace, the redirectTo function sets the Text property of the ProcessingLabel Label control to let the user know how long the process will take. Then it calls the timedIterations function, which calls itself recursively once each second, setting the length (width) of the progress bar and reporting to the user the number of seconds that have elapsed. The maximum width of the progress bar is set for 800 pixels. When the wait time plus 10 seconds has elapsed, the report of the elapsed seconds and the progress bar are hidden and the ProcessingLabel Label control's text property tells the user that the server is taking longer than expected.

Figure 7 The Wait Page

Figure 7** The Wait Page **

You can see what the wait page looks like in Figure 7.

Cleaning Up the Code

One thing that is still not very clean about this wait page implementation is the fact that the StartTheProcess.aspx page has to know how much time the ProcessTheData.aspx page is going to take. Wouldn't it be nice if the StartTheProcess.aspx page could just point directly to the ProcessTheData.aspx page, and have the ProcessTheData.aspx page bring up the wait page by itself if it needs to? In other words, wouldn't it be better for a slow page to put up its own wait page without the pointing page having to know about it? It turns out that you can add wait page functionality to any page that needs it.

First, change the StartTheProcess.aspx page so that it redirects directly to the ProcessTheData.aspx page, like this:

private void ButtonData_Click(object sender, System.EventArgs e) { Response.Redirect("ProcessTheData.aspx?Data=" + TextBoxData.Text); }

Then, change the Page_Load event code of the ProcessTheData.aspx page so that it looks like Figure 8.

Figure 8 Revised Page_Load

private void Page_Load(object sender, System.EventArgs e) { if (!IsPostBack) { if (null == Request.QueryString["Data"] || 0 == Request.QueryString["Data"].Length) { ErrorLabel.Text = "You must provide data for processing. " + "<BR> Press the Back button to enter some data."; ErrorLabel.Visible = true; } else { if (null == Request.QueryString["redirectPage"] || 0 == Request.QueryString["redirectPage"].Length) { Response.Redirect("Wait.aspx" + Request.Url.Query + "&redirectPage=ProcessTheData.aspx" + "&secondsToWait=30", true); } else { //string inputString, processedString; inputString = Request.QueryString["Data"]; if ((new BusinessService()). ProcessMyData(inputString, out processedString)) { ResultLabel.Text = processedString; } else { ErrorLabel.Text = processedString; ErrorLabel.Visible = true; } } } } }

The purpose of this new Page_Load code for the ProcessTheData.aspx page is that if there is no query string variable named redirectPage, then you know that this request did not come from the wait page. In that case, you do a redirect to the wait page (passing true for the second parameter in order to stop any further processing). The wait page then redirects back to this ProcessTheData.aspx page (and includes the redirectPage query string variable). Now, secure in the knowledge that the user is viewing the wait page, you can go ahead and take your own sweet time to process the data and return the result.

With this code you have a page that displays its own wait page, and the pointing page does not need to know a thing about it.

A Pattern Emerges

You can see that I'm handling a simple state transition in the Page_Load event for the ProcessTheData.aspx page. I'm using a query string variable named redirectPage to tell me which of the two states to enter (either the wait page not sent or the wait page sent). Let's see if I can implement a pattern that would enable me to handle complex state transitions for any process that would take several steps to complete.

To implement such a pattern, add another Button control to the StartTheProcess.aspx page. The Button control redirects to the ProcessTheDataPattern.aspx page, passing the contents of the textbox, as shown here:

private void ButtonData_Click(object sender, System.EventArgs e) { Response.Redirect("ProcessTheDataPattern.aspx?Data=" + TextBoxData.Text); }

Create a Web Form named ProcessTheDataPattern.aspx and add two Label controls as you did on the the ProcessTheData.aspx page. Make the code for the ProcessTheDataPattern.aspx page look like the code in Figure 9.

Figure 9 ProcessTheDataPattern

public class ProcessTheDataPattern : System.Web.UI.Page { protected System.Web.UI.WebControls.Label ResultLabel; protected System.Web.UI.WebControls.Label ErrorLabel; protected string processedString; private void Page_Load(object sender, System.EventArgs e) { if (!IsPostBack) { if (null == Request.QueryString["Data"] || 0 == Request.QueryString["Data"].Length) { ErrorLabel.Text = "You must provide data for processing. " + "<BR> Press the Back button to enter some data."; ErrorLabel.Visible = true; } else { IProcessObject myProcObj; if (null==Session["ProcObj"]) { ProcessObject1 procObj = new ProcessObject1(); myProcObj = (IProcessObject) procObj; Session["ProcObj"] = myProcObj; } else { myProcObj = (IProcessObject) Session["ProcObj"]; } if (myProcObj.ProcessMyData(Request, Response, Session, out processedString)) { ResultLabel.Text = processedString; } else { ErrorLabel.Text = processedString; ErrorLabel.Visible = true; } } } }

You can see in Figure 9 that the Page_Load event looks in a Session variable named ProcObj to get the business object that it will use. If that Session variable is empty, it creates an instance of ProcessObject1, saves it in the ProcObj Session variable, and then calls its ProcessMyData method. The idea is that if an instance of the business object does not already exist, then the page creates a business object that handles the first step of the process. If an instance of the business object already exists, then this is a subsequent step in the process and the Page_Load event calls that instance's ProcessMyData method to perform that step.

Each of the business objects that handle each step in the process implement an interface. So create an interface named IProcessObject and define it as shown here:

public interface IProcessObject { bool ProcessMyData(System.Web.HttpRequest request, System.Web.HttpResponse response, System.Web.SessionState.HttpSessionState session, out string DataOut); }

Now create a class named ProcessObject1 that implements this interface. Now you can implement the ProcessMyData method, as shown in Figure 10.

Figure 10 ProcessMyData in ProcessObject1

public bool ProcessMyData(System.Web.HttpRequest request, System.Web.HttpResponse response, System.Web.SessionState.HttpSessionState session, out string DataOut) { DataOut = ""; try { if (null == request.QueryString["redirectPage"] || 0 == request.QueryString["redirectPage"].Length) { response.Redirect("Wait.aspx" + request.Url.Query + "&redirectPage=ProcessTheDataPattern.aspx" + "&secondsToWait=30", true); } else { DataOut = "Test1"; System.Threading.Thread.Sleep(10000); ProcessObject2 procObj = new ProcessObject2(); IProcessObject myProcObj = (IProcessObject) procObj; session["ProcObj"] = myProcObj; response.Redirect("ProcessTheDataPattern.aspx?Data=" + DataOut); } return true; } catch (Exception except) { DataOut = except.Message; return false; } }

As you can see in Figure 10, the ProcessMyData method of the ProcessObject1 business object redirects to the wait page and then sets the DataOut parameter to "Test1". It sleeps for 10 seconds to simulate a long-running process, and then creates a ProcessObject2 business object to handle the second step in the process, which it stores in the ProcObj Session variable. Finally, it redirects the browser back to the ProcessTheDataPattern.aspx page in order to kick off the second step.

To implement the second step, create another class named ProcessObject2 that implements the IProcessObejct interface. Implement its ProcessMyData method, as shown in Figure 11.

Figure 11 ProcessMyData in ProcessObject2

public bool ProcessMyData(System.Web.HttpRequest request, System.Web.HttpResponse response, System.Web.SessionState.HttpSessionState session, out string DataOut) { DataOut = ""; try { if (null == request.QueryString["redirectPage"] || 0 == request.QueryString["redirectPage"].Length) { response.Redirect("Wait.aspx" + request.Url.Query + "&redirectPage=ProcessTheDataPattern.aspx" + "&secondsToWait=20", true); } else { DataOut = "Test2"; System.Threading.Thread.Sleep(10000); } return true; } catch (Exception except) { DataOut = except.Message; return false; } }

As you can see in Figure 11, the ProcessMyData method of the ProcessObject2 business object redirects to the wait page and then sets the DataOut parameter to "Test2". It sleeps for 10 seconds to simulate a long-running process. Then it simply returns true because this is the last step.

If there were a third step, the ProcessMyData method of the ProcessObject2 would need to create an instance of a ProcessObject3 business object, store it in the ProcObj Session variable, and then redirect to the ProcessTheDataPattern.aspx page to execute the third step.

When you click on the Button control in the StartTheProcess.aspx page that points to the ProcessTheDataPattern.aspx page, you will notice that the data in the query string of the wait page changes. After step one, the data becomes "Test1". After step two, the data becomes "Test2". In this example, both the ProcessObject1 business object and the ProcessObject2 business object use the same wait page. For your own multi-step processes, you would probably want to create a unique wait page for each step, each of which tells the user what is happening.

You will also notice that the progress bar starts over at each step. You may want to enhance your wait pages so that the progress bar takes a parameter for its starting width, enabling you to show a progress bar that grows through all of the steps.

With this pattern, you have a ProcessTheDataPattern.aspx page that either starts the process by instantiating the first business object or simply executes whatever business object is there already. The code for each step in the process is encapsulated in its own business object. Each business object displays its own wait page (if needed) and then instantiates the appropriate business object to handle the next step. Adding new steps is a relatively clean matter of creating a new business object that implements the IProcessObject interface and calls the next step after it if necessary.

Conclusion

Long-running processes are a frequent feature of business applications. A wait page that keeps users informed and prevents them from making extraneous page submissions is helpful for both users and developers. Pages that have the ability to display their own wait page are ideal because they encapsulate their own logic, alleviating the need for other pages to know the details of their operations. Using the design pattern illustrated in this column, you can create a browser UI for a multi-step process in which the wait page is updated throughout the process.

Send questions and comments for Design Patterns to  mmpatt@microsoft.com.

Lyn Robison is a developer at Tualatin Software, a maker of tools for IT developers who specialize in .NET