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;
function postSend() {
var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
divResponse.innerHTML = "";
xmlhttp.Open("POST", "ProcessTask.ashx", true);
xmlhttp.onreadystatechange =
function () {
if (xmlhttp.readyState == 4) {
window.clearInterval(tmrProgress);
var response = xmlhttp.responseText;
divResponse.innerHTML += response;
}
}
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");
xmlhttp.Open("POST", "GetTaskProgress.ashx", true);
xmlhttp.onreadystatechange =
function () {
if (xmlhttp.readyState == 4) {
var response = xmlhttp.responseText;
divResponse.innerHTML = "Processing File: " + response;
}
}
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