Cutting Edge

Context-Sensitive Feedback with AJAX

Dino Esposito

Code download available at: Cutting Edge2007_07.exe(170 KB)

Contents

Built-In Tools for Update Progress
Limitations of UpdateProgress
The Progress Indicator Pattern
The Progress Monitor Server API
Building a Sample Page
The Progress Monitor Client API
The Event Sink for Client Calls
Wrapping Up a Sample Page
Tricks for UpdatePanel Controls
Summary

When users of any computer application start a potentially lengthy operation, best practices dictate that the user interface should be updated to indicate that work is in progress and that results may not be available for a while. This is easy to accomplish in a desktop application, but it can be rather difficult in a Web scenario. In Web applications, displaying text such as "Please wait" just before an operation begins is easy, but if you want to provide some useful feedback, such as the estimated time to completion or the percentage of work finished, it’s a bit more difficult to accomplish. This is because most lengthy tasks in Web apps execute on the server, and there are no built-in mechanisms for pushing state information to the client.

In my September and October 2006 Cutting Edge columns, I discussed progress bars in ASP.NET Web applications. In the first column, I focused on a pure ASP.NET 2.0 solution using ASP.NET script callbacks. In the second column, I built a solution based on ASP.NET AJAX Extensions. At the time of that writing, ASP.NET AJAX was still in a pre-beta stage and, as you probably know, a good number of things have changed since then. In this month’s column, I revisit the topic and build a server and client ASP.NET AJAX infrastructure to start and control a potentially lengthy server-side task.

Built-In Tools for Update Progress

ASP.NET AJAX Extensions support two different programming models: partial rendering and direct remote method calls. Partial rendering is an evolution of the traditional, control-based programming model of ASP.NET. It lets you define regions within the page that can be updated independently from one another and from the page as a whole. Postback events that relate to a particular region, or to a subset of regions, trigger the generation of markup for the controls in the region(s). As a result, only a fraction of the page is updated or refreshed. No new page is displayed when these controls are refreshed (unless you explicitly navigate to another page)—but the Document Object Model (DOM) of the currently displayed page is updated.

With partial rendering, you still use codebehind methods and server-side event handlers to define the expected behavior of a page. The partial refresh of pages is entirely managed by the ASP.NET AJAX runtime environment. Although a few client events are publicly exposed, partial rendering is essentially a black-box process. Users click buttons, the system determines that a postback is necessary, and it uses the client-side AJAX infrastructure to make an asynchronous request. When data is ready, the DOM of the current page is automatically updated.

Because the server-side postback event may take a while to complete, ASP.NET AJAX provides the UpdateProgress control to keep the user informed. Figure 1 lists a sample ASP.NET AJAX page that uses partial rendering and displays a progress screen.

Figure 1 Page with UpdatePanel and UpdateProgress Controls

<%@ Page CodeFile=”LongUpdates.aspx.cs” Inherits=”LongTask” %>

<html>
<head runat=”server”>
    <title>Lengthy Operations and Updates</title>
    <style type=”text/css”>
        #UpdateProgress1
        {
            width: 30%; background-color: #FFC080; height:15%;
            top: 40%; left: 35%; position: absolute;
            border: solid 1px black;
        }
        #ProgressTemplate1
        {
            font-weight: bold; font-size: 12pt; color: navy; 
            font-family: Verdana; background-color: #ffff99;
        }
    </style>
</head>

<body>
<form id=”form1” runat=”server”>
        <asp:ScriptManager ID=”ScriptManager1” runat=”server” />

        <asp:UpdatePanel ID=”UpdatePanel1” runat=”server” 
                         UpdateMode=”Conditional”>
            <ContentTemplate>
                <asp:Button runat=”server” ID=”Button1” 
                  Text=”Start Task ...” onclick=”Button1_Click” />
                <hr />
                <asp:Label runat=”server” ID=”Label1” /><br />
            </ContentTemplate>
        </asp:UpdatePanel>
        
        <hr />

        <asp:UpdateProgress runat=”server” ID=”UpdateProgress1”>
            <ProgressTemplate>
                <div ID=”ProgressTemplate1”>
                   <img alt=”” src=”Images/indicator.gif” />
                   <span id=”Msg”>Please, wait ... </span>  
                </div>   
                <br /><br />
            </ProgressTemplate>
        </asp:UpdateProgress>
    </form>
</body>
</html>

The UpdateProgress control is designed to automatically provide visual feedback in the browser window when the contents of one or more UpdatePanel controls are updated. As a page developer, you don’t have to worry about showing and hiding the control explicitly. All that you have to do is define the visual template to show to the user and its style and position. The ASP.NET AJAX client infrastructure takes care of everything else, showing and hiding the template as appropriate (see Figure 2).

Figure 2 UpdateProgress Control in Action

Figure 2** UpdateProgress Control in Action **(Click the image for a larger view)

The UpdateProgress control is easy to use and particularly suited for relatively fast operations that complete in just a few seconds. For longer operations—those that take more than a few seconds—you should look for a more comprehensive solution.

Limitations of UpdateProgress

UpdateProgress is a powerful control with many handy properties, but it can’t obtain server process information because no interaction between the UpdateProgress control and the code being executed is supported. Plus, UpdateProgress can’t display a percentage progress bar or a list of messages describing the current stage of the server operation.

Another reason for looking elsewhere is that the UpdateProgress control is tightly coupled with both the partial rendering engine and the UpdatePanel control itself, so it won’t work if you use your ASP.NET AJAX pages to invoke a server method either via local Web services or page methods. You can reuse the same visual template, however, and show and hide it via script code. For example, the progress template of Figure 1 becomes a simple <div> tag as shown here:

<div id=”UpdateProgress1” style=”display:none”>
    <div id=”ProgressTemplate1”>
        <img alt=”” src=”images/indicator.gif” />
        <span id=”Msg”>Please, wait ... </span>  
    </div>   
</div>

Figure 3 shows the JavaScript code required to manage the progress template when a remote call is made to a server method. Let’s see how to build a client and server infrastructure that returns timely and informative feedback when you execute partial updates and remote method calls.

Figure 3 Show and Hide a Visual Progress Template

<script type=”text/javascript”>
function startTask()
{
    // Display the progress screen
    enableProgress(true);

    // Invoke the remote method (ExecuteTask in codebehind class)
    PageMethods.ExecuteTask(taskCompleted, taskFailed);
}

function taskCompleted(results, context, methodName)
{
    // Hide the progress screen and update the UI
    enableProgress(false);
    $get(“Label1”).innerHTML = results;
}

function taskFailed(results, context, methodName)
{
    // Hide the progress screen
    enableProgress(false);
}

function enableProgress(shouldShow)
{
    var template = $get(“UpdateProgress1”);
    if (shouldShow)
        template.style.display = “”;
    else
        template.style.display = “none”;
}
</script>

The Progress Indicator Pattern

Whatever approach you take to execute a partial page refresh, no information is pushed to the client during the operation. As a result, there’s no easy way for a client to grab status information and update a progress bar. You can, however, create a custom server operation that can write its current status to a known location as it makes progress. A second operation would then read periodically from that location and report the status to the client.

As an example, imagine a server operation that goes through a number of distinct steps. At the beginning of each step, the server code updates a durable store with some task-related information. Concurrently, a client-controlled monitoring service reads the text and brings it back to the client for display. This behavior is associated with one of the most popular AJAX patterns—the Progress Indicator pattern. (See www.ajaxpatterns.org for further information on common AJAX design patterns.)

But there’s another common scenario to consider. Say the user triggers a server operation that will loop through a sequence of records. If you accomplish all the work in a stored procedure using one large, set-based transaction, you’re issuing just one database request, resulting in great performance. However, if giving feedback to the user is essential, you might want to break the original transaction into multiple steps controlled by the middle-tier. The state of the transaction could then be properly exposed to the monitoring service before each step is processed and you could inform users that the system has completed, say, 35 percent of the work. Figure 4 provides a graphical view of the Progress Indicator pattern.

Figure 4 Progress Indicator Pattern

Figure 4** Progress Indicator Pattern **(Click the image for a larger view)

Implementing the Progress Indicator pattern in ASP.NET AJAX is a three-step process. You define the API that reads and writes the status information from a persistent store, such as a database, a disk file, or a shared block of memory. Next, you provide an event sink for the client to connect via XMLHttpRequest to the server and read the current status. Finally, you set up a client JavaScript API that represents the monitoring service that periodically connects to the server to measure progress. Note that the pattern should be implemented to work regardless of the technique you use to start the partial page update—be it the UpdatePanel control, a page, or a Web service method.

The Progress Monitor Server API

To read and write status information from a known container, you need a server-side, contract-based API. In Figure 5 you see the source code of a ProgressMonitor class that implements a given interface. The interface features two methods—GetStatus and SetStatus—to read and write the status of the task. The choice of the data container is ultimately up to the class author. The ProgressMonitor class in Figure 5 saves data to the ASP.NET cache. The SetStatus method adds a new item and gives it an absolute expiration policy of five minutes. The value can be modified to suit your needs. You can also make it an external parameter and have the class read the cached item duration from the configuration file. An explicit expiration date is helpful so that the ASP.NET cache doesn’t fill up with too many unnecessary items.

Figure 5 ProgressMonitor Server Class

namespace Samples.Server 
{
    public interface IProgressMonitor
    {
        void SetStatus(int taskID, object message);
        string GetStatus(int taskID);
    }

    public class ProgressMonitor : IProgressMonitor 
    {
        // Sets the current status of the task
        public void SetStatus(int taskID, object message)
        {
            HttpContext.Current.Cache.Insert(
                taskID.ToString(), 
                message, 
                null, 
                DateTime.Now.AddMinutes(5), 
                Cache.NoSlidingExpiration);
        }

        // Reads the current status of the task
        public string GetStatus(int taskID)
        {
            object o = HttpContext.Current.Cache[taskID.ToString()];
            if (o == null)
                return String.Empty;

            return (string) o;
        }
    }
}

The ProgressMonitor class must be visible to the ASP.NET server-side code that runs and controls the lengthy task. The code below shows an ASP.NET AJAX page method exposed to the JavaScript client. When invoked, the method triggers the server-side progress monitor to store its status to a server data container—in this case, the ASP.NET cache.

[WebMethod]
public static string ExecuteTask(int taskID)
{
   ProgressMonitor progMonitor = new ProgressMonitor();
   progMonitor.SetStatus(taskID, “5% done”);
   DoStep1();
   progMonitor.SetStatus(taskID, “25% done”);
   DoStep2();
   ...
   progMonitor.SetStatus(taskID, “100% done”);
}

Similar code is required in the body of any ASP.NET AJAX Web service method exposed to the client for direct calls. As another example, here’s a server event handler for when an UpdatePanel control is used to trap postbacks and other partial page updates.

protected void Button1_Click(object sender, EventArgs e)
{
    ProgressMonitor progMonitor = new ProgressMonitor();
    int taskID = RetrieveTaskID();

    progMonitor.SetStatus(taskID, “15% done”);
    DoStep1();
    ...
}

The SetStatus method takes a value and stores it in the internal data container. The value can be a string as well as a number. Generally speaking, you can express the status of a task using any Microsoft® .NET Framework object. However, if you use something other than a primitive type, you should then guarantee that it can be correctly transformed into a JavaScript object. This means that status information must be serialized to a JavaScript Object Notation (JSON) stream and a JavaScript class with an analogous structure must be available on the client. Finally, you need to preprocess the returned JSON string to get an instance of that class properly initialized.

Because two distinct software entities—the server-side progress monitor and the client-side monitoring service—operate on the status of the same task, a unique identifier for the task is required. The task ID is generated as a random number on the client and passed as an additional argument to the remote method. The task ID is used as the key to set and retrieve status information in whatever data container you decide to use.

Building a Sample Page

To see how it all works, let’s consider a sample ASP.NET AJAX page with a button. The user clicks the button and a remote method call is made. To monitor the progress, you need the server method to use the ProgressMonitor class and receive a task ID from the client. Here’s the JavaScript code that starts the remote method:

function startTask() {
    // Start the remote task using a page method
    var taskID = progressManager.getTaskID();
    PageMethods.ExecuteTask(taskID, taskCompleted, taskFailed);
    
    // Enable the monitoring service
    updateUI(true);
    progressManager.startMonitor(taskID, 2000, updateProgress, 
        updateProgressCompleted);
}

ExecuteTask is the name of the ASP.NET AJAX page method to invoke from the client. This method takes a task ID in addition to all input parameters it needs and the usual callbacks for success and failure. In the preceding code snippet, the updateUI JavaScript function is a local function that brings up the progress screen.

The progressManager object in the listing is implemented as a JavaScript class and is based on the Microsoft AJAX library. It represents the client-side monitoring service. The getTaskID method returns the ID for the task being started. The task ID is a random number generated using a JavaScript helper class. The startMonitor method sets a timer that periodically sends a remote call to the server page. The server code receives the task ID, connects to the data container (the ASP.NET cache), reads the current status of the specified task, and returns any value found. Once status information has been downloaded to the client, the updateProgress callback is invoked to update the client UI. The updateProgressCompleted callback is invoked when the task is completed to clear the progress screen.

The progressManager object is initialized when the ASP.NET AJAX page is loaded. You use the pageLoad JavaScript function to define any code you want to execute upon page loading once the Microsoft AJAX library has been fully initialized and set up.

var progressManager = null; 
function pageLoad() {
   progressManager = new Samples.Progress();
}

If you need to accomplish ASP.NET AJAX-specific tasks upon loading, the pageLoad method is more useful than the body’s onload event. The Samples.Progress class is the client-side monitoring service that periodically checks the status of the task and updates the user interface.

The Progress Monitor Client API

The Samples.Progress class is defined across a couple of JavaScript files—progress.js and random.js. Both are to be bound to the page either using the <script> tag or the ScriptManager control, as shown here:

<asp:ScriptManager ID=”ScriptManager1” runat=”server” 
     EnablePageMethods=”true”>
  <Scripts>
      <asp:ScriptReference path=”random.js” />
      <asp:ScriptReference path=”progress.js” />
  </Scripts>
</asp:ScriptManager>

Both script files take advantage of the Microsoft AJAX client library. Their classes are defined in the same namespace and built using the extended set of object-oriented capabilities provided by the Microsoft AJAX client library. The classes use the prototype model and require an explicit constructor (for more information on the prototype model, see Ray Djajadinata’s article on JavaScript in the May 2007 issue of MSDN® Magazine.). The random.js file defines the Samples.Random class. The class has only one method—the one that generates a random number using the JavaScript native Math object, as you see here:

Type.registerNamespace(‘Samples’);
Samples.Random = function Samples$Random() {
    Samples.Random.initializeBase(this);
}
function Samples$Random$getNumber(minNumber, maxNumber) {
    var num = minNumber + Math.floor(Math.random() * maxNumber);
    return num;
}
Samples.Random.prototype = {
    getNumber:  Samples$Random$getNumber
}
Samples.Random.registerClass(‘Samples.Random’);

The Samples.Progress class, on the other hand, does much more (see Figure 6). The class has three essential public methods—getTaskID, startMonitor, and stopMonitor—plus a fourth method that gets invoked only when an UpdatePanel control is used to trigger the potentially lengthy task. As mentioned, getTaskID returns a randomly generated number used to uniquely identify the task. If you don’t trust the JavaScript Math object, you can place an additional server-side call to a page method or a Web service and then ask for a GUID.

Figure 6 Samples.Progress JavaScript Class

// 
// Samples.Progress :: 
// Client-side monitoring service
//

// Define the namespace of the class
Type.registerNamespace(‘Samples’);

// Constructor
Samples.Progress = function Samples$Progress() 
{
    Samples.Progress.initializeBase(this);
    this._timerID = null;
    this._taskID = null;
    this._msInterval = null;
    this._progressCallback = null;
    this._callback = null;
}

// Start the timer to periodically check the status of the ongoing task
function Samples$Progress$startMonitor(taskID, msInterval,
    progressCallback, progressCompletedCallback) { 
    if (arguments.length !== 4) throw Error.parameterCount();
    
    // Update internal members
    _taskID = taskID;
    _msInterval = msInterval;
    _progressCallback = progressCallback;
    _progressCompletedCallback = progressCompletedCallback;
    
    this._startTimer();
}

// Stop the timer  
function Samples$Progress$stopMonitor() { 
    window.clearTimeout(_timerID);
    if (_progressCompletedCallback !== null)
        _progressCompletedCallback();    
}

// Get task ID
function Samples$Progress$getTaskID(taskID) { 
    return Samples.Random.getNumber(0, 10000000);
}

// Start the timer to control progress
function Samples$Progress$_startTimer() {
    this._callback = Function.createDelegate(this, this._checkProgress);
    _timerID = window.setTimeout(this._callback, _msInterval);
}

// Modify the request to add the task ID to a hidden field 
//(for UpdatePanel pages)
function Samples$Progress$modifyRequestForTaskId(request, taskID,
    hiddenField) {
    var body = request.get_body();
    var token = “&” + hiddenField + “=”;
    body = body.replace(token, token + taskID);
    request.set_body(body);
    
    return request;
}

// Timer function(s)
function Samples$Progress$_checkProgress() {
    PageMethods.GetCurrentStatus(_taskID, this._onFeedbackReceived,
        this._onFeedbackFailed, this);
}

function Samples$Progress$_onFeedbackReceived(results, context) {
    context._startTimer();
    
    if (_progressCallback !== null)
        _progressCallback(results);
}

function Samples$Progress$_onFeedbackFailed(results) {
}

// Class prototype
Samples.Progress.prototype = 
{
    getTaskID:              Samples$Progress$getTaskID,
    startMonitor:           Samples$Progress$startMonitor,
    stopMonitor:            Samples$Progress$stopMonitor,
    modifyRequestForTaskId: Samples$Progress$modifyRequestForTaskId,
    _startTimer:            Samples$Progress$_startTimer,
    _checkProgress:         Samples$Progress$_checkProgress,
    _onFeedbackReceived:    Samples$Progress$_onFeedbackReceived,
    _onFeedbackFailed:      Samples$Progress$_onFeedbackFailed    
}

// Register the new class
Samples.Progress.registerClass(‘Samples.Progress’);

The startMonitor method takes the ID of the task to monitor and the desired interval in milliseconds. In addition, the method accepts a couple of callbacks—one to update the user interface with status information and one to reset the user interface at the end of the operation.

Implemented through the window’s setTimeout function, the timer calls back an internal method when the interval has elapsed. The method is named _checkProgress and looks like this:

function Samples$Progress$_checkProgress() {
    PageMethods.GetCurrentStatus(_taskID, 
           this._onFeedbackReceived, 
           this._onFeedbackFailed, 
           this);
}

The method does one key thing: it calls the event sink on the server that retrieves the current status of the task. The event sink is a publicly exposed method that the JavaScript client can invoke. In this implementation, I assume it is a page method named GetCurrentStatus. The call to the event sink is a classic ASP.NET AJAX remote method invocation. Hence, it requires callbacks for success and failure and may optionally carry a context object. No special action is required in case of failure; it just won’t update the user interface. Instead, whenever significant status information is downloaded to the client, you need to update the user interface and restart the timer for the next update, as you see here:

function Samples$Progress$_onFeedbackReceived(results, context) {
    context._startTimer();
    
    if (_progressCallback !== null)
        _progressCallback(results);
}

The success callback receives the Samples.Progress object through the context parameter and restarts the timer. After that, it just invokes the page-defined callback and updates the progress bar. In the ASP.NET page, you start the monitoring service when the user clicks to begin the operation and stop the service from within any callback (success or failure) that runs after the operation has completed.

The Event Sink for Client Calls

Like the remote operation, the monitoring service is based on a remote call that’s repeated periodically. In addition to the endpoint for the potentially lengthy operation, you also need a second public method on the server that can be called from JavaScript to obtain status information. As you know, there are three ways for an ASP.NET AJAX page to expose client-callable endpoints: local Web services, page methods, and UpdatePanel controls. You can use any of these techniques to implement the task to monitor and the monitoring service.

However, note that you can’t use an UpdatePanel bound to a Timer control to periodically refresh the progress bar if you’re already using another UpdatePanel to execute the remote task. By design, UpdatePanel updates execute one at a time under the control of the same page request manager object. This manager object aborts the pending request when a new request is posted. As a result, if you trigger the long task from an UpdatePanel control, and use another timer-based UpdatePanel to provide feedback, the first request for an updated status will abort the operation you want to monitor!

Furthermore, two UpdatePanel updates can’t run concurrently because the view state might be updated improperly if they did. Therefore, to preserve a consistent state of the page, calls going through UpdatePanels must be serialized.

I suggest you consider using an UpdatePanel to start the monitored task, but use a page-specific method to concurrently read its status. You don’t have to put the client-callable page method in each and every page that includes a potentially lengthy operation. It will suffice to inherit any monitorable page from the same base class, as you see here:

public class UpdateProgressPage : System.Web.UI.Page
{
   static ProgressMonitor _progMonitor = new ProgressMonitor();

   [WebMethod]
   public static string GetCurrentStatus(int taskID)
   {
      return _progMonitor.GetStatus(taskID);
   }
}

Note that the name of the public method—GetCurrentStatus—is hardcoded in the progress.js file. If you change the interface of the UpdateProgressPage class, you likewise need to edit the source code of the progress.js script.

Wrapping Up a Sample Page

Let’s outline a step-by-step procedure to add monitoring capabilities to ASP.NET AJAX pages. As an example, imagine that the task is controlled from the codebehind of the page or from some sort of middle-tier where you have access to the progress monitor server API.

You begin by adding the lengthy method to the page as a public page method or as a linked Web service method. Next, you add progress.js and random.js files to the page using <script> tags or the ScriptManager control. The page class must inherit from UpdateProgressPage or expose a GetCurrentStatus static Web method with the following prototype:

public string GetCurrentStatus(int taskID)

At this point, you add the script code to start and monitor the operation. A key element is the callback function you pass to startMonitor to refresh the user interface with the current status. What this function does depends on the markup in the page that is used to show the progress. If all that you have is a <span> tag to display a message, then the following is a good example of a callback:

function updateProgress(perc) {
    $get(“Msg”).innerHTML = perc + “ done.”;
}

The callback function receives a value that indicates the progress made. It can be a string, a percentage, or whatever else you specify through the progress monitor server API. Suppose you signal progress using the following code:

progMonitor.SetStatus(taskID, “5”);

The updateProgress callback function will receive a value of "5". In this case, you’re passing the percentage of work done; as an alternative, you can pass text describing the current stage.

The updateProgress callback is entirely responsible for updating the user interface. If you want to display a classic progress meter, you have to code it here. Here’s one way to do so:

function updateProgress(perc) {
   var table = “<table width=100%><tr><td>{2}%</td></tr>” + 
               “<tr><td bgcolor=blue width=’{0}%’> </td>” + 
               “<td width=’{1}%’></td></tr></table>”;
   table = String.format(table, perc, 100-perc, perc);
   $get(“Msg”).innerHTML = table;
}

You build a dynamic <table> tag and split one of its rows in two cells. The leftmost cell takes a share of the row equivalent to the work done and is rendered with a different color. Figure 7 shows the final result. The Web Development Helper tool at the bottom of the browser window logs all requests from the browser. (You can get it from Nikhil Kothari’s blog at www .nikhilk.net.) As you can see, the monitoring service issues calls for feedback every two seconds. (See the Timestamp column.) The response of each request consists of a number that indicates the percentage of work done. This number is passed to updateProgress and used in the building of a dynamic HTML table.

Figure 7 Context-Sensitive AJAX Progress Bar in Action

Figure 7** Context-Sensitive AJAX Progress Bar in Action **(Click the image for a larger view)

The full source code for this column is available from the MSDN Magazine Web site. In the project, you’ll find three ASP.NET AJAX pages, each covering a different scenario. You’ll see the lengthy operation implemented as a page method, as a method on a local Web service, and also triggered within a partial update operation. In all cases, the monitoring service relies on a page method inherited from the page base class.

Tricks for UpdatePanel Controls

To top off this month’s column, let me point out an issue that arises if you use an UpdatePanel control to start and monitor a remote task. As long as you start the operation using script, you generate and pass the task ID explicitly. The server code receives the task ID as an argument and proceeds. When an UpdatePanel control is used, the user starts a task by simply clicking on a postback control, such as a button. So how would you pass a task ID to the server? A hidden field is always a good choice for sending custom data from the client to the server. To add a hidden field, you can either add it explicitly to the page markup or proceed programmatically as below:

void Page_Load(object sender, EventArgs e)
{
    ScriptManager.RegisterHiddenField(UpdatePanel1, 
          “HiddenField1”, “”);
}

Why use this code instead of a call to ClientScript.RegisterHiddenField as in regular ASP.NET 2.0 pages? If you use the method on the ScriptManager class, you are guaranteed that the hidden field is maintained during partial postbacks. On the server, you retrieve the task ID using the following code:

int taskID;
Int32.TryParse(Request[“HiddenField1”], out taskID);

But how would you start monitoring? Thankfully, the ASP.NET AJAX client component responsible for partial updates has a client eventing model. When a partial update begins and ends, two JavaScript events are fired—beginRequest and endRequest:

function pageLoad() {
   progressManager = new Samples.Progress();
   reqManager = Sys.WebForms.PageRequestManager.getInstance();
   reqManager.add_beginRequest(onBeginRequest);
   reqManager.add_endRequest(onEndRequest);
}

The code associated with onBeginRequest executes just before the partial page update request is sent over. Here’s a sample handler:

function onBeginRequest(sender, args) {
    // Get the task ID
    taskID = progressManager.getTaskID();

    // Must pass the taskID via the hidden field. 
    progressManager.modifyRequestForTaskId(args.get_request(), 
         taskID, “HiddenField1”);

    // Too late for this to work
    progressManager.startMonitor(taskID, 2000, 
         updateProgress, updateProgressCompleted);
}

When the beginRequest event is fired, it is too late to set the contents of the hidden field to the task ID. More precisely, when the event fires, the body of the request has been already prepared and won’t be further updated. Hence, setting the hidden field at this time is useless. To send the task ID value through the hidden field, you have to modify the body of the request.

function modifyRequestForTaskId(request, taskID, hiddenField) {
    var body = request.get_body();
    var token = “&” + hiddenField + “=”;
    body = body.replace(token, token + taskID);
    request.set_body(body);
    
    return request;
}

The request body is a plain string that contains a token of the form &HiddenField=. You replace the token with another one that contains the task ID: &HiddenField=xyz. Finally, in the endRequest event handler you call the stopMonitor method of the Samples.Progress class.

Summary

Monitoring ongoing tasks is relatively easy in Windows® applications, but that hasn’t been the case at all in Web applications. However, the AJAX programming model makes this task much easier to accomplish because ASP.NET AJAX Extensions provide ready-made tools for building the infrastructure you need using client and a server APIs.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is a mentor at Solid Quality Learning and the author of Introducing Microsoft ASP.NET 2.0 AJAX Extensions (Microsoft Press, 2007). Based in Italy, Dino is a frequent speaker at industry events worldwide. Get in touch with Dino at cutting@microsoft.com or join the blog at weblogs.asp.net/despos. For more tips on this topic, keep an eye on his freshly reactivated blog.