User Feedback For Long Running Tasks in ASP.NET





5.00/5 (4 votes)
Giving a web page user regular indication of the progress of a server task
Introduction
The simple code snippets presented here are a solution to the problem of how to provide regular feedback to a user of an ASP.Net page whilst they are waiting for a long running task to complete on the server.
Background
This solution came about, as is often the case, in response to late changes in the requirements for a newly developed system. The system originally was a simple intranet page to display a report, with options to filter and sort the data on the report.
The late change was that the users requested the ability to save the underlying data as a series of xml documents on the file system, for offline use. Clearly if this had been known at the outset then the architecture of the solution may have been designed differently.
The problem became one of allowing the user to see that something was actually happening on the server, in a process that could take many minutes to complete. Options considered included just displaying progress bar, or hourglass, or firing off a the task asynchronously and expecting the user to refresh the page for updates. Niether of these were deemed totally satisfactory.
As a server task is unable to push status updates to the client (browser), the solution is to get the browser to request regular updates from the server.
In a nutshell, the solution chosen involves firing off the long running task asynchronously, and monitoring it on a separate thread initiated by browser requests.
In practice, the long running task writes progress updates to a shared area, and the web page makes regular requests to the server to read that shared area and display the progress to the user.
Using the code
A simplified version of the solution is shown below, in order to demonstrate the principles.
In summary, we have a web page, Default.aspx, on which there is a button which triggers the long running server task. The task is deployed server-side as a generic handler, ProcessTask.ashx.
Initiation of this handler takes place in Javascript fired when the button is clicked:
var tmrProgress; //SENDING XML function postSend() { var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); divResponse.innerHTML = ""; // 'true' specifies that it's a async call xmlhttp.Open("POST", "ProcessTask.ashx", true); // Register a callback for the async, call and display a completion message xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4) { window.clearInterval(tmrProgress); var response = xmlhttp.responseText; divResponse.innerHTML += response; } } // Send the actual request xmlhttp.Send(); tmrProgress = window.setInterval("showSendProgress();", 500); }
The Javascript fires off an asynchronous request to the handler, registers a callback for completion of the task, and initiates a timer which will request progress updates fromt the server.
For test purposes, the server task is simulating time taken for creation of files by means of a thread.sleep command. On "submission" of each file, a count is incremented and saved to the shared area, in this case the .NET cache is used.
Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest Const NumberOfFilesToSend As Integer = 50 context.Response.ContentType = "text/plain" Try For i As Integer = 1 To NumberOfFilesToSend 'report the current status of the task to shared cache area context.Cache("FileSending") = i 'simulate a time intensive task Threading.Thread.Sleep(100) Next context.Response.Write(String.Format("Finished sending {0} files", NumberOfFilesToSend)) Catch ex As Exception context.Response.Write(String.Format("Error sending files: {0}", ex.Message)) End Try End Sub
For
progress reporting, the function showSendProgress()
is called on the
browser at 500ms intervals, triggered by the timer tmrProgress. This
function calls a separate generic handler to get the current status from
the server:
function showSendProgress() {
var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
// 'true' specifies that it's a async call
xmlhttp.Open("POST", "GetTaskProgress.ashx", true);
// Register a callback for the call
xmlhttp.onreadystatechange =
function () {
if (xmlhttp.readyState == 4) {
var response = xmlhttp.responseText;
divResponse.innerHTML = "Processing File: " + response;
}
}
// Send the actual request
xmlhttp.Send();
}
Responses from handler GetTaskProgress.ashx
are captured in the callback, and update the status on the page, in divResponse.
GetTaskProgress.ashx
simply queries the cache and reports back to the client
Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
context.Response.ContentType = "text/plain"
If Not HttpContext.Current.Cache("FileSending") Is Nothing Then
context.Response.Write(HttpContext.Current.Cache("FileSending"))
End If
End Sub
Points of Interest
One point with the solution presented is that it is not accurately reporting on the exact status of the server task in real-time, it's just giving a regular indiction to the user that something is progressing. Some fine tuning of the refresh rate would be necessary depending of the duration of the task, as well as the frequency it updates it's own status.
.Net cache has been used as a simple shared area for simplicity's sake, but this could easily be changes to use session state, SQL server or any other shared area.
History
Version 1.0 27 April 2012