Executing Long-Running Tasks with the Progress Bar in ASP.NET
This article demonstrates the use of multi-threading in ASP.NET web applications to execute long running tasks. It also shows the status of that long running task with the help of a progress bar.
A downloadable file for this article is available here.
The problem
Every scenario in IT, at some level, needs a long running task (process) to be performed. Long running tasks may also include taking a huge database backup, installing software, formatting a disk, and so on. When we perform any of those tasks, we will watch its progress.
When a lengthy task starts, some applications get developed with a simple message such as “Please wait” or “Wait a moment” or something similar. Some applications show the progress of the same type of task (sometimes in the form of “percentage complete” or “time remaining”). Showing the progress of a task is pretty common during installations of certain software (or even small packages).
Any user would generally ask (or suggest) that we show the progress of achievement when the system starts processing a lengthy task. If it is a small task (say, going to complete within one minute), it is reasonable to provide messages such as “Please Wait…” or “Wait a moment.” But, for long running tasks, it is the programmer’s responsibility to show the progress of achievement to the user visually (before the user complains for it).
It is fairly easy to show the progress of achievement (for long running tasks) on desktop (non–browser based) applications. But, it is a bit difficult for browser based applications, because we will not be able to maintain a dedicated HTTP request. Once the browser receives the information (response) from server, the HTTP connection gets disconnected automatically. If the server takes too much time to serve for the browser (or client) through HTTP, we may even experience a “Timeout” error.
This article concentrates on working with long running tasks in ASP.NET effectively without consuming too many resources from the server (using multi-threading), and also showing the progress of the task to the user (by auto-refreshing the page every two seconds).
The entire VS.NET solution can be freely downloaded from this site to reuse the code in your applications.
Now, what does my VS.NET solution contain? It simply contains three web forms and one class file as follows:
StartPage.aspx (web form)
UnderProcess.aspx (web form)
Finished.aspx (web form)
ProcessingStatus.vb (Class file)
“StartPage.aspx” contains only a button to start a dummy long running process. The control gets transferred to “UnderProcess.aspx” after the process starts. The “UnderProcess.aspx” shows the progress of achievement in the form of “Percentage complete.” Once the process completes, the control is transferred to “Finished.aspx”, which simply shows the message “Completed Successfully” using a label.
Even though I did not mention “ProcessingStatus.vb” in the above paragraph, it works behind the scenes. It gets involved with both “StartPage.aspx” and “UnderProcess.aspx.” We create a separate thread (for our process) in the “StartPage.aspx” and store information (or status) about the thread using “ProcessingStatus.vb.” This “ProcessingStatus.vb” supplies the information to “UnderProcess.aspx” (whenever requested).
As we start a separate thread at the server, it remains in memory and continuously works with our process (long running task) without having any relationship with “StartPage.aspx” any more. We update the status of thread using “ProcessingStatus.vb” at every milestone of achievement. “UnderProcess.aspx” always gets the information (or status) of the thread from “ProcessingStatus.vb” and displays it visually. Once it receives the information about the completion of thread (indirectly the process), it immediately jumps to “Finished.aspx.”
Starting the Thread
As explained in the previous section, “StartPage.aspx” starts a new thread for a long running process (a dummy task in this scenario). The first important issue is that we need to import “System.Threading” to “StartPage.aspx” because we deal with threads.
I need to provide a unique id for the process, which is going to start within the thread’s context. It should be globally unique, because of the potential issues caused by simultaneous access to the same page by more than one user. I declare and initialize a variable (or object) to hold a globally unique id as follows:
' declaring the Guid Dim RequestId As Guid ' Create a new request id RequestId = Guid.NewGuid()
The next step is that we need to start a new thread as follows:
' Create and start a worker thread, to process "something" Dim ts As New ThreadStart (AddressOf doProcess) Dim ProcessingThread As New Thread(ts) ProcessingThread.Start()
What is the funny “doProcess” in the first statement above? It is nothing but our sub-program, which starts our long running process! I separated my entire long process to be under a single sub-program for my convenience (just to attach to the thread).
The above statement redirects to “UnderProcess.aspx” by carrying the globally unique id we created above. This statement gets executed immediately after the “ProcessingThread.Start()” statement.
The beauty of the thread is that it would never stop (or interrupt) the flow of the execution of the program, even if it doesn’t complete the process. It hangs on in memory to complete the process independently by itself, without disturbing the flow of the execution.
All of the above happens when the user simply clicks on the button provided in “StartPage.aspx.”
For the long running process, I could not find a proper example other than providing a loop with a certain amount of delay. The loop is iterated ten times and is delayed for about three seconds on every iteration. In that way, I made a dummy long-running process, which takes about thirty seconds to complete.
I don’t think that the long-running process is the main issue here in this article. The most important issue is to update the thread information using “ProcessingStatus.vb” and to show it visually with “UnderProcess.aspx.” The following is the code fragment which is used as the long-running task in this article:
'This method is executed by the processing thread. Private Sub doProcess() 'do some long task processing here ProcessingStatus.add(RequestId, 0) 'start with percentage 0 Dim i As Integer For i = 1 To 10 Thread.Sleep(New TimeSpan(0, 0, 0, 3,0)) 'wait for 3 seconds for every iteration ProcessingStatus.update(RequestId, i * 10) 'update percentage with a value between 0 to 100 Next ProcessingStatus.update(RequestId, -1) 'after completing the task succesfully, just update status as -1 End Sub
I provided commenting everywhere necessary. The first important issue to understand is that the above sub-program (doProcess) gets executed in a separate thread (as explained in the previous section). Let us consider the first statement within the above fragment:
ProcessingStatus.add(RequestId, 0)
The above statement adds new GUID with percentage completion as zero using “ProcessingStatus.vb.” This gets executed when a new process is about to start (taking into the scenario of multiple users).
Thread.Sleep(New TimeSpan(0, 0, 0, 3, 0))
The above statement creates a delay of three seconds.
ProcessingStatus.update(RequestId, i * 10)
The above statement updates the status of GUID added above with a certain percentage value for every iteration of the loop.
ProcessingStatus.update(RequestId, -1)
The above statement updates the percentage of same GUID added above with a percentage value of “-1”. I made a rule in my application that, if the percentage value is “-1”, the process has been successfully completed (this would in turn make “UnderProcess.vb” understand easily that the process has been completed).
This is the actual section which shows the progress to the user in the form of a progress bar. The “UnderProcess.vb” takes care of this process.
The “UnderProcess.vb” mainly contains two labels, “lblMsg” and “lblProgressBar.” “lblMsg” shows what percentage it has completed in the form of words. “lblProgressBar” was designed with a blue background color. This gets expanded (indirectly; the width of the label) based on the percentage value for every auto-refresh of the same page. The following is the entire code present in the “page load“ event of “UnderProcess.vb.”
' Get the request id Dim requestId As New Guid(Request. QueryString("RequestId").ToString()) 'Check the processing thread status collection If (ProcessingStatus.Contains(requestId))Then 'StatusValue contains either a value between 0 to 100 or -1 (-1 shows that the task is finished) Dim StatusValue As Integer = CType(ProcessingStatus.getValue(requestId), Integer) If StatusValue = -1 Then 'processing task succesfully finished ProcessingStatus.remove(requestId) 'remove the status Response.Redirect("finshed.aspx") 'go to result page EndIf
'if not finished, display the status ' Remove the status from the collection Me.lblMsg.Text = "Wait for a moment.." & StatusValue & "% Completed.." Me.lblProgressBar.Width = Unit.Pixel(2 * StatusValue) '200 pixels for 100% EndIf
' The processing has not yet finished ' Add a refresh header, to refresh the page in 2 seconds. Response.AddHeader("Refresh", "2")
I commented the above program very clearly to help you understand every statement in detail. I hope you can understand it.
The “Finshed.aspx” has nothing other than a simple label with a message “Completed Successfully.” No processing is done at “Finished.aspx”.
You must have observed that I wrote plenty of statements starting with “ProcessingStatus” in the above fragments of code (of the previous sections). Actually, it is my own class having only “shared” members. I developed this class to hold all GUIDs and their percentage values respectively. Of course, you could also develop a very good concrete class rather than a class with all “shared” members. But, this class has already passed my requirements (apart from thread safety issues). Let’s see the class:
Imports System.Collections
Public Class ProcessingStatus
Private Shared Status As New Hashtable
Public Shared Function getValue(ByVal itemId As Guid) As Object Return Status(itemId) End Function
Public Shared Sub add(ByVal ItemId As Guid, ByVal oStatus As Object) 'make sure that oStatus contains only the values 0 through 100 or -1 Status(ItemId) = oStatus End Sub
Public Shared Sub update(ByVal ItemId As Guid, ByVal oStatus As Object) 'make sure that oStatus contains only the values 0 through 100 or -1 Status(ItemId) = oStatus End Sub
Public Shared Sub remove(ByVal ItemId As Guid) Status.Remove(ItemId) End Sub
Public Shared Function Contains(ByVal ItemId As Guid) As Boolean Return Status.Contains(ItemId) End Function
End Class
In the above program, I maintained a single “HashTable” throughout the class to hold all GUIDs and their respective percentage values. All of the methods in the above class provide a better interface to the “HashTable,” so that you can do all CRUD operations with “HashTable” efficiently.
Summary
The sample downloadable solution is entirely developed using Visual Studio.NET 2003 Enterprise Artchitect on Windows Server 2003 Standard Edition. But, I am confident that it would work with other versions of Windows (which support .NET 1.1) versions as well.
You can further enhance the solution to show the status (or progress) in a small separate window as well to make it convenient to the user. But this article focussed only on the core issue of handling tasks rather than UI. I hope this article will help you to solve your needs.
Any comments, suggestions, bugs, errors, feedback etc. are highly appreciated at jag_chat@yahoo.com.