Starting and Monitoring a Long Running Task Using Web Forms and AJAX






4.92/5 (11 votes)
The article presents a web forms project that demonstrates some techniques for starting and monitoring a long running task using ASP.NET and AJAX.
Introduction
This ASP.NET web site project (can be run from here) illustrates some basic techniques for starting a long running task on the web server side, with provisions for tracking the progress of the task using AJAX.
Note: This application was tested on several shared hosting sites and was found to work properly. However, you should be aware that the application saves the downloaded file to its folder on the web server. For this to work, the IIS user account running ASP.NET must have a write-access to the application's folder. It is quite risky to give anonymous users this level of access and, therefore, you should consider additional security measures.
It is interesting that ASP.NET allows a thread started from a server-side ASPX page to continue running even after the page's script has finished processing (i.e. there in no need to have the Page_Load()
event wait for the auxiliary thread to finish).
In this application, the following options are considered or implemented.
- The long running method can be started as a new Thread instance or through asynchronous method invocation (using
BeginInvoke()
). The latter uses a thread from a pre-allocated set of threads known as the ThreadPool. - The state of the long running task can be kept in some object that survives page postbacks
like Application, Session, or
HttpRuntime.Cache
. For our application, we have settled on usingHttpRuntime.Cache
. This class is thread-safe and thus items can be added or removed from cache without the need for locking on our part. On the other hand, the Session object has few problems (for example, many shared hosts disable support for InProcess Session, yet we cannot store a thread object in an Out-of-Process Session). - If multiple concurrent tasks (possibly from different clients) are started, then we need a "TaskID" for unique identification.
For our purpose, the TaskID is obtained from a call to
Guid.NewGuid()
. The call returns a random string with 32 hex digits. Alternatively, we could design a custom method that returns a random string of digits (and/or letters) of sufficient length to guarantee (with high probability) uniqueness.
Besides its use of threading and callbacks, the project serves as a good example of employing AJAX concepts.
The project uses a minimal amount of AJAX code without using any AJAX library.
AJAX uses the XMLHttpRequest
object to enable the browser to interact with the web server
and use the received data to modify parts of the page without page refresh.
How the Application Works
The application is a Web Site (MS Visual Studio) project consisting of a single ASPX page default.aspx and its code-behind default.aspx.cs.
As it is normally the case with ASP.NET web applications, the ASPX file provides the necessary HTML for the user interface along with client-side JavaScript AJAX code.
The code-behind includes a custom class AsyncWebRequest
that encapsulates the long running method.
A proper starting place for understanding the code is the Page_Load()
method, since it is the application’s entry point, given below.
public delegate void ProgressCallback(object sender, int Progress);
Boolean useDummyTask = false; // Set this as you desire
public string DummyTaskString = "";
protected void Page_Load(object sender, EventArgs e)
{ // Instruct browser not to cache the results
Response.AddHeader("cache-control", "no-cache");
if (useDummyTask) DummyTaskString = "(uses a dummy task)";
string TaskID;
// Handle one of three verbs: startTask, cancelTask, getStatus
if (Request["startTask"] != null)
{ TaskID = Request["startTask"];
AsyncWebRequest req = new AsyncWebRequest();
req.useDummyTask = useDummyTask;
string page_path = Server.MapPath("");
string downloadURL = Request["url"];
int i = downloadURL.LastIndexOf('/');
string fileName = downloadURL.Substring(i+1);
req.localfile = page_path + "\\" + fileName;
req.url = downloadURL;
req.ProgressChanged += new ProgressCallback(bw_ProgressChanged);
req.TaskID = TaskID;
HttpRuntime.Cache["DownloadRquest_" + TaskID] = req;
req.ExecuteRequest();
Response.Write("Task started");
Response.End();
return;
}
if (Request["cancelTask"] != null)
{ TaskID = Request["cancelTask"];
AsyncWebRequest req = (AsyncWebRequest)HttpRuntime.Cache["DownloadRquest_" + TaskID];
req.Cancel = true;
while (req.CompletionStatus != "Canceled")
{ Thread.Sleep(100); }
HttpRuntime.Cache.Remove("DownloadRquest_" + TaskID);
Response.Write("Task canceled");
Response.End();
return;
}
if (Request["getStatus"] != null)
{ TaskID = Request["getStatus"];
string st = (string)HttpRuntime.Cache["Task_state_" + TaskID];
if (st.Length > 3) HttpRuntime.Cache.Remove("DownloadRquest_" + TaskID);
Response.Write(st);
Response.End();
return;
}
}
In Page_Load()
, the application handles one of three verbs (parameters in the URL's query string):
startTask
, cancelTask
, getStatus
.
The verb's value is the TaskID of the task involved. Because the response to any of these verbs is just some little text, the block handling the verb ends with:
Response.End();
return;
This ends the response stream and terminates the Page_Load()
method.
If we do not do this, ASP.NET will send the HTML text from "Default.aspx".
With the above code for Page_Load()
, the html text from Default.aspx
is only sent when the query string is empty (i.e., when the application is
started), because we are not executing Response.End()
in this case.
Task Execution and Monitoring
We can run a method (a long running task) asynchronously by wrapping it into a delegate and calling
BeginInvoke()
.
This will have the method run on a thread from the ThreadPool
.
To monitor the progress of method execution, we will use an AJAX function
getStatus()
.
The function will post back a query string getStatus=TaskID
.
This will be processed by Page_Load()
and return a string back to the client
(unless there is some error, the string will simply be a value 0-100 indicating progress percentage).
Next, we need to settle on a mechanism for the called method to report progress. For this, we can use one of the following approaches:
- Use some kind of object that is shared between the method and the caller. This is a rather simple solution. Note that access to such object would require locking.
- Use a callback delegate that is called by the long running method whenever progress needs to be reported.
We use the second approach in our application.
Our long running task will use an instance of WebRequest
to download some URL. The data will be read from the response streams in chunks using the statement:
RecvStream.Read(Buffer, 0, BlockSize);
The preceding statement will be part of a while
-loop in which we update and report the progress, as shown by the following code snippet (from our
AsyncWebRequest
class):
long TotalBytes = resp.ContentLength;
int BlockSize = 8192;
Byte[] Buffer = new Byte[BlockSize];
FileStream fs = new FileStream(Obj.localfile, FileMode.Create);
long ByteCount = 0;
while (true)
{ // Read incoming data
int BytesRead = RecvStream.Read(Buffer, 0, BlockSize);
if (BytesRead == 0) break;
ByteCount += BytesRead;
fs.Write(Buffer, 0, BytesRead);
// Update and report progress
int Progress = (int)( (double)ByteCount / TotalBytes * 100);
if (Progress <= 100) ProgressChanged(this, Progress);
}
The completed AsyncWebRequest
class is given below.
public class AsyncWebRequest
{
public string url, localfile;
public string TaskID;
public event ProgressCallback ProgressChanged;
public Boolean Cancel = false;
public Boolean useDummyTask = false;
public string CompletionStatus;
delegate void MethodInvoker();
public void ExecuteRequest()
{ MethodInvoker simpleDelegate;
if (useDummyTask) simpleDelegate = new MethodInvoker(this.RunDummyTask);
else simpleDelegate= new MethodInvoker(this.RunTask);
simpleDelegate.BeginInvoke(null,null);
}
private void RunDummyTask()
{ for (int i = 1; i <= 100; i++)
{ if (Cancel)
{ CompletionStatus = "Canceled"; break; }
// Update progress
ProgressChanged(this, i);
Thread.Sleep(200);
}
}
private void RunTask()
{
WebRequest req = WebRequest.Create(url);
WebResponse resp;
try { resp = req.GetResponse(); }
catch (WebException e)
{ CompletionStatus = e.Message;
ProgressChanged(this,-1);
return;
}
long TotalBytes = resp.ContentLength;
int BlockSize = 32768;
Stream RecvStream = resp.GetResponseStream();
// RecvStream.ReadTimeout = 3000;
// RecvStream.WriteTimeout = 3000;
Byte[] Buffer = new Byte[BlockSize];
using (FileStream fs = new FileStream(localfile, FileMode.Create))
{ long ByteCount = 0;
while (true)
{ if (Cancel)
{ CompletionStatus = "Canceled"; break; }
// Read incoming data
int BytesRead = RecvStream.Read(Buffer, 0, BlockSize);
if (BytesRead == 0) break;
ByteCount += BytesRead;
fs.Write(Buffer, 0, BytesRead);
// Update progress
int Progress = (int)( (double)ByteCount / TotalBytes * 100);
if (Progress <= 100) ProgressChanged(this, Progress);
}
}
}
}
Our AsyncWebRequest
class encapsulates the long running method (RunTask()
or
RunDummyTask()
).
The public members of this class correspond to some relevant input-output parameters.
Furthermore, the callback delegate ProgressChanged
(of type ProgressCallback
) used for reporting progress
is a public member marked as an event (so that it can be called (fired) from its owning class only).
By utilizing delegates, the AsyncWebRequest
class has less coupling with other code.
The class can be used with different types of .NET applications: Web Forms, Windows Forms, or Console applications.
As illustrated by the code in Page_load()
, to use the class, set the necessary members including
the ProgressChanged
event and call ExecuteRequest()
method:
AsyncWebRequest req = new AsyncWebRequest();
req.localfile = page_path + "\\" + "file1.wmv";
req.url = "http://download.microsoft.com/file1.wmv";
req.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);
req.ExecuteRequest();
Note that a call to ExecuteRequest()
is synchronous (blocking). However, the call finishes quickly because all
it does is that it instantiates a delegate and calls BeginInvoke()
.
History
- 10th November, 2012: Version 1.0.