Click here to Skip to main content
Click here to Skip to main content
Go to top

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

, 17 Nov 2012
Rate this:
Please Sign up or sign in to vote.
The article presents a web forms project that demonstrates some techniques for starting and monitoring a long running task using ASP.NET and AJAX.

AsyncWebRequest-Events Image

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 using HttpRuntime.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:

  1. 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.

  2. 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Nasir Darwish
Instructor / Trainer KFUPM
Saudi Arabia Saudi Arabia

Nasir Darwish is an associate professor with the Department of Information and Computer Science, King Fahd University of Petroleum and Minerals (KFUPM), Saudi Arabia.

Developed some practical tools including COPS (Cooperative Problem Solving), PageGen (a tool for automatic generation of web pages), and an English/Arabic full-text search engine. The latter tools were used for the Global Arabic Encyclopedia and various other multimedia projects.

Recently, came up with an algorithm for creating a large population of symmetric curves that are handy for the design of aesthetic tiles. Samples of these tiling designs can be browsed at the author's homepage.


Comments and Discussions

 
GeneralMy vote of 5 PinmemberPham Dinh Truong14-Jun-13 6:47 
GeneralMy vote of 5 PinmemberHaBiX15-Nov-12 23:25 
GeneralMy vote of 5 PinmemberAhsan Murshed15-Nov-12 21:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140922.1 | Last Updated 17 Nov 2012
Article Copyright 2012 by Nasir Darwish
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid