January 2012

Volume 27 Number 01

The Cutting Edge - Enhancing the Context-Sensitive ASP.NET MVC Progress Bar

By Dino Esposito | January 2012

Dino EspositoIn the Web world, the term “progress bar” means too many different things to different people. Sometimes it refers to static text that simply shows that some operation is taking place somewhere. The text provides basic feedback to the user and essentially invites her to just relax and wait. Sometimes, the progress bar displays a simple animation while sending the same message to the user—please wait—which at least focuses the user’s attention because users tend to follow the moving elements. A typical animation shows a graphic element that moves around a circular or linear path indefinitely. When the moving element reaches the end of the path, the animation starts over and keeps running until the underlying operation terminates and the animation is stopped programmatically. This is a fairly common pattern on, for example, most airline Web sites.

Last month, I introduced a basic version of an ASP.NET MVC framework—the SimpleProgress Framework—that allows you to quickly and effectively set up a truly context-sensitive progress bar (msdn.microsoft.com/magazine/hh580729). Context-sensitive is probably just what a progress bar should be. That is, it should be an element of the UI that progressively describes the status of an ongoing operation. The progressive display can be as simple as a sequence of messages or it can be more compelling—for example, a gauge.

In this article, I’ll show how to enhance the progress bar by adding cancel capabilities. In other words, if the user interacts with the interface and cancels the operation, the framework will direct the manager of the ongoing operation to interrupt any work being done.

Canceling Ongoing Tasks

Canceling an ongoing server-side task from within a client browser is not a trivial operation. Don’t be fooled by the very basic examples you’ll find that just abort the client request and pretend that everything is also cleared on the server.

When you trigger a server operation via AJAX, a socket is opened that connects your browser to a remote endpoint. This connection remains open, waiting for the request to complete and return a response. The trick I discussed last month for setting up a context-sensitive progress bar used a parallel flow of requests to check a second controller method responsible for returning the status of the operation—sort of a mailbox that client and server can use to communicate.

Now suppose there’s an Abort button that lets the user cancel the current server operation. What kind of code is that going to require? At the very least, you want to abort the AJAX request. If the server operation was started using the jQuery AJAX API, you can do the following:

xhr.abort();

The xhr variable is the reference to the XmlHttpRequest object used in the call. In jQuery, this reference is returned directly by the $.ajax function. Figure 1 shows an excerpt from the progress framework (renamed ProgressBar) from last month’s code that adds a new abort method.

Figure 1 Adding Abort Functionality to the Progress Framework

var ProgressBar = function () {
  var that = {};
  // Store the XHR object being used.
that._xhr = null;// Get the user-defined callback that runs after
// aborting the call.
that._taskAbortedCallback = null;
...// Set progress callbacks.
that.callback = function (userCallback, completedCallback,
                          abortedCallback) {
  that._userDefinedProgressCallback = userCallback;
  that._taskCompletedCallback = completedCallback;
  that._taskAbortedCallback = abortedCallback;
  return this;
};
// Abort function.
that.abort = function () {
  if (_xhr !== null)
       xhr.abort();
};
...
// Invoke the URL and monitor its progress.
that.start = function (url, progressUrl) {
  that._taskId = that.createTaskId();
  that._progressUrl = progressUrl;
  // Place the AJAX call.
    xhr = $.ajax({
      url: url,
      cache: false,
      headers: { 'X-ProgressBar-TaskId': that._taskId },
      complete: function () {
        if (_xhr.status != 0) return;
        if (that._taskAbortedCallback != null)
            that._taskAbortedCallback();
        that.end();
      },
      success: function (data) {
        if (that._taskCompletedCallback != null)
            that._taskCompletedCallback(data);
        that.end();
            }
  });
  // Start the progress callback (if any set).
  if (that._userDefinedProgressCallback == null)
      return this;
  that._timerId = window.setTimeout(
    that._internalProgressCallback,
    that._interval);
};
  return that;
}

As you can see, the new abort method doesn’t do much beyond calling abort on the internal XmlHttpRequest object. Aborting an ongoing AJAX call still triggers the complete event on the AJAX manager, however, though not any success or error functions. To detect whether an operation was aborted by the user, you attach a handler for complete and check the status property of the XmlHttpRequest object—it will be 0 if the operation was aborted. The complete handler then performs whatever cleanup operation is required—stopping timers, for example. Figure 2 shows a nice interface from which users can stop remote operations at will.

Cancelable AJAX Operations
Figure 2 Cancelable AJAX Operations

Notifying the Server

The nice UI in Figure 2 doesn’t necessarily guarantee that the server-side operation has been stopped as the user requested and as the application’s feedback seems to prove. Calling abort on XmlHttpRequest simply closes the socket that connects the browser to the server. Put another way, by calling abort on XmlHttpRequest, you simply say you’re no longer interested in receiving any response the server method might generate. Nothing really guarantees the server received and acknowledged the abort request; more likely, the server will continue to process the request regardless of whether a browser is listening for a response. While this particular aspect may vary on different platforms and servers, there’s another aspect to consider that depends strictly on your application. If the request triggered either an asynchronous operation or a long-running operation, can you stop it? The truth is, there is no reliable and automatic way to stop the processing of a request; you have to build your own framework and write your server methods to be interruptible. Let’s extend the progress framework, then.

Extending the Progress Framework

The server-side manager component is the part of the framework that controllers work with. Controller methods call methods on the following interface to post messages for the client progress bar and to receive notifications from the UI to stop processing:

public interface IProgressManager
{
    void SetCompleted(String taskId, String format, params Object[] args);
    void SetCompleted(String taskId, Int32 percentage);
    void SetCompleted(String taskId, String step);
    String GetStatus(String taskId);
    void RequestTermination(String taskId);
    Boolean ShouldTerminate(String taskId);
}

Compared with the code I presented last month, there are a couple of extra methods—RequestTermination, which clients will call to request termination, and ShouldTerminate, which action methods will invoke to see if they’re supposed to stop and roll back.

Each progress manager works on top of a data provider that holds the status of pending tasks, each identified with a client-generated ID. The default data provider in the source code uses the ASP.NET cache to store the status of tasks. It creates an entry for each task and stores the entry inside an object of type TaskStatus, like so:

public class TaskStatus
{
  public TaskStatus(String status) : this (status, false)
  {
  }
  public TaskStatus(String status, Boolean aborted)
  {
    Aborted = aborted;
    Status = status;
  }
  public String Status { get; set; }
  public Boolean Aborted { get; set; }
}

When the controller method calls SetCompleted, it ends up saving a status message for the task in the underlying store. Before proceeding with the next step, the controller method checks whether it has to abort.

Putting It All Together: The Server

Let’s see what it takes to create a controller method for a multistep, monitorable and interruptible operation. You start with a sample controller class that inherits from ProgressBarController:

public class TaskController : ProgressBarController
{  public String BookFlight(String from, String to)
  {
    ...
  }
}

The sample method returns a String for simplicity; it can be a partial view or JSON or whatever else you need it to be.

The base class, shown in Figure 3, is a simple way to endow the final controller with a bunch of common methods.

Note, in particular, the Status and Abort methods that define a public and standard API for jQuery clients to call to query for the current status and to request termination. By using a base class, you avoid having to rewrite that code over and over again.

Figure 3 The ProgressBar Super Class

public class ProgressBarController : Controller
{
  protected readonly ProgressManager ProgressManager;
  public ProgressBarController()
  {
    ProgressManager = new ProgressManager();
  }
  public String GetTaskId()
  {
    // Get the header with the task ID.
    var id = Request.Headers[ProgressManager.HeaderNameTaskId];
    return id ?? String.Empty;
  }
  public String Status()
  {
    var taskId = GetTaskId();
    return ProgressManager.GetStatus(taskId);
  }
  public void Abort()
  {
    var taskId = GetTaskId();
    ProgressManager.RequestTermination(taskId);
  }
}

Figure 4shows the pattern for a controller method that needs to be monitored from the client.

Figure 4 A Monitorable Controller Method Using the Progress Framework

public String BookFlight(String from, String to)
{
  var taskId = GetTaskId();
  // Book first leg
  ProgressManager.SetCompleted(taskId,
    "Booking flight: {0}-{1} ...", from, to);
  Thread.Sleep(4000);
  if (ProgressManager.ShouldTerminate(taskId))
  {
    // Compensate here
    //
    return String.Format("One flight booked and then canceled");
  }
  // Book return flight
  ProgressManager.SetCompleted(taskId,     "Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(4000);
  if (ProgressManager.ShouldTerminate(taskId))
  {
    // Compensate here
    //
    return String.Format("Two flights booked and then canceled");
  }
  // Book return
  ProgressManager.SetCompleted(taskId,     "Paying for the flight ...", taskId));
  Thread.Sleep(5000);
  if (ProgressManager.ShouldTerminate(taskId))
  {
    // Compensate here
    //
    return String.Format("Payment canceled. No flights booked.");
  }
  // Some return value
  return "Flight booked successfully";
}

The task ID is generated on the client and transmitted to the server through an HTTP header. The GetTaskId method shields controller developers from having to know these details. The controller method does its work piecemeal and invokes SetCompleted each time it accomplishes a significant part of the work. It indicates the work done using a string, which could be a percentage as well as a status message. Periodically, the controller method checks whether a request for termination has been received. If this is the case, it does whatever is possible to roll back or compensate, and then returns.

Putting It All Together: The Client

On the client side, you need to link the Progress Framework Java­Script API and the jQuery library:

<script src="@Url.Content("~/Scripts/progressbar-fx.js")"
        type="text/javascript"></script>

Each monitorable method will be invoked via AJAX and will therefore be triggered by a client-side event—for example, a button click, created with markup as shown in Figure 5.

Figure 5 Markup for Triggering a Monitorable and Interruptible Action

<fieldset>
  <legend>Book a flight...</legend>
  <input id="buttonStart" type="button" value="Book a flight..." />
  <hr />
  <div id="progressbar_container">
    <span id="progressbar2"></span>
    <input id="buttonAbort" type="button"
      value="Abort flight booking"
      disabled="disabled" />
  </div>   
</fieldset>
Click handlers are attached unobtrusively when the page is loaded:
<script type="text/javascript">
  var progressbar;
  $(document).ready(function () {
    $("#buttonStart").bind("click", buttonStartHandler);
    $("#buttonAbort").bind("click", buttonAbortHandler);
  });
</script>

Figure 6shows the JavaScript for starting and aborting a remote operation and updating the UI accordingly.

Figure 6 JavaScript Code for the Sample View

function buttonStartHandler() {
  updateStatusProgressBar ();
  progressbar = new ProgressBar();
  progressbar.setInterval(600)
             .callback(function (status) {
                             $("#progressbar").text(status); },
                       function (response) {
                             $("#progressbar").text(response);
                             updateStatusProgressBar(); },
                       function () {
                             $("#progressbar").text("");
                             updateStatusProgressBar(); })
             .start("/task/bookflight?from=Rome&to=NewYork",
                    "/task/status",
                    "/task/abort");
}
function buttonAbortHandler() {
    progressbar.abort();
}
function updateStatusProgressBar () {
    $("#buttonStart").toggleDisabled();
    $("#buttonAbort").toggleDisabled();
}

The ProgressBar JavaScript object consists of three main methods. The setInterval method specifies the interval between two successive checks for state updates. The value to pass is in milliseconds. The callback method sets a bunch of callback functions for updating the status and for updating the UI when the operation is successfully completed or when the operation is aborted by the user. And the start method begins the operation. It takes three URLs: the endpoint of the method to run and endpoints for the methods to be called back to catch state updates and to abort the pending operation.

As you can see, the URLs are relative and expressed in the form /controller/method. Of course, you can change the names of the method status and abort to whatever you like—as long as such methods exist as public endpoints. The status and abort methods are guaranteed to exist if you inherit your controller class from ProgressBarController. Figure 7 shows the sample application in action. Though the UIs in Figure 2 and Figure 7 look the same, the underlying code and behavior are really different.

The Framework in Action
Figure 7 The Framework in Action

Making Progress

AJAX supplies the tools to poll the server and ask what’s going on. You have to build your own framework if you want it to provide monitorable and interruptible methods. It should be noted that in a real example, the action method itself might be calling other asynchronous services. Such code can be complicated. Luckily, writing asynchronous code should get simpler in the upcoming ASP.NET MVC 4. And in my next column, I’ll show another approach to implementing and monitoring remote tasks based on a new client-side library that could also make it to the ASP.NET MVC 4 bundle—the SignalR library. For now, get the source code from msdn.com/magazine/msdnmag0112 and let me know your thoughts!


Dino Esposito is the author of “Programming Microsoft ASP.NET MVC3” (Microsoft Press, 2011) and coauthor of “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Based in Italy, Esposito is a frequent speaker at industry events worldwide. You can follow him on Twitter at twitter.com/despos.

Thanks to the following technical expert for reviewing this column: Phil Haack