LANGUAGES: C# | VB.NET
ASP.NET VERSIONS: 2.0 | ASP.NET AJAX 1.0
Are We There Yet?
Alleviate the Anxiety of Long-running Tasks with ASP.NET AJAX
By Mike Trebilcock
The responsiveness of Web-enabled applications can be critical to gaining user acceptance and satisfaction. However, many organizations design guidelines often state an initial page-load time of somewhere between two and five seconds and are then rather vague about how long subsequent pages may take to deliver content to the user.
Web pages that initiate business processes or tasks may be triggering complex database queries, which, despite optimizations and fine tuning still take tens of seconds (or even a couple of minutes) to complete. The Example 1 solution demonstrates how unnerving it can be to wait for a Web server to deliver a page (the examples are available in the accompanying download files; see end of article for details). The application calculates all the prime numbers in a given range using an inefficient, but effective, algorithm. On my machine, calculating prime numbers between 1 and 100000 will take about 20 seconds.
As demonstrated, during this time the user is left with what appears to be an unresponsive application, resulting in them trying to resubmit the request or giving up with the task altogether. There are many solutions that provide feedback to the user; a summary of the options is shown in Figure 1.
Implement a Please Wait page.
Provide user with feedback on task status and expected run time by implementing META Refresh.
Provide user with feedback on task status and expected run time by implementing AJAX.
Figure 1: Solutions that provide feedback to the user.
This article demonstrates how a long-running task can be executed on a Web server whilst keeping the user informed of progress. The pattern given here is based on Microsoft ASP.NET AJAX. The Web browser will use AJAX to make a call to the Web server to retrieve the task progress and provide the user with a cancel option. The task will be run on a new thread that will continue to execute after the initial Web page has been delivered back to the user. To track the progress of the task, and be able to retrieve any results, a handle to the thread must be maintained. This is achieved using a TaskHandler class. Each task must implement the Task Interface and provide key methods that allow the progress of the task to be monitored and the results to be retrieved. The example solution will be improved to demonstrate how the user experience can be enhanced when long-running tasks are encountered.
Implementing ASP.NET AJAX
To improve the user experience by introducing ASP.NET AJAX, the logic that completes the long-running task must be separated from the presentation code contained either inline in the Web page or in the code-behind file. Before conducting this refactoring and creating a task class, the classes required to utilize the new task class will be constructed first. A TaskHandler class will be required to execute and track the running tasks, and an ITask interface defined that allows the TaskHandler to interact with each task.
The TaskHandler Class
The function of the TaskHandler class is to initiate tasks and maintain a handle to them so that the status and results can be retrieved at a later time. The TaskHandler class therefore requires only four methods:
- AddTask. Adds and starts the task running.
- FindTask. Returns the task.
- CancelTask. Stops task execution.
- RemoveTask. Deletes the task.
There should be only one TaskHandler instance per application; therefore, it must be implemented as a singleton (this ensures that the application can effectively manage multiple tasks running from multiple users). Because this is a singleton class, there must be a fifth method, named Instance, that is static or shared and that will return the single instance of TaskHandler. The code for this class is shown in Figure 2.
Public Class TaskHandler
Private Shared m_Instance As TaskHandler = New TaskHandler
Private m_tasks As Hashtable = New Hashtable
Private m_threads As Hashtable = New Hashtable
Public Shared Function Instance() As TaskHandler
Public Function FindTask(ByVal id As String) As ITask
If m_tasks.ContainsKey(id) = False Then
Return CType(m_tasks(id), ITask)
Public Sub CancelTask(ByVal id As String)
Public Sub RemoveTask(ByVal id As String)
Public Function AddTask(ByVal task As ITask) As String
Dim id As String = Guid.NewGuid().ToString()
m_tasks(id) = task
Dim ts As ThreadStart = New ThreadStart(AddressOf task.Start)
Dim tr As Thread = New Thread(ts)
m_threads(id) = tr
Figure 2: The TaskHandler class.
The TaskHandler class maintains a hashtable of tasks and a hashtable of thread handles. To use TaskHandler, we must first obtain TaskHandler itself by calling:
A new task can then be added to TaskHandler by calling the AddTask method. When a task is added, it is started automatically using the security context of the current user:
The task to be run must implement the ITask interface.
The ITask Interface
Figure 3 contains the source code for the ITask interface. Implementing the ITask interface ensures that all tasks have a method for starting the task, a method to check if the task has finished, a method to check the progress, and methods to report running time and time remaining. These methods will all need to be coded into the task class that implements this interface.
Public Interface ITask
Function Finished() As Boolean
Function HasError() as Boolean
Function Percent() As String
Function StatusDescription() as String
Function RunningTime() as String
Function TimeRemaining() As String
Figure 3: Source code for the ITask interface.
Modifying an Existing Task to Implement the ITask Interface
The code required to complete a task must be separated from the presentation layer, either from the inline code or from the code-behind file. The code in Example 1 already has the task code in a separate function, making it easier to identify and remove. The code is then made into its own class, myLongTask, which implements the ITask interface. An additional line as been added that will allow the task to execute properly in a multi-threaded environment. A call to Thread.Sleep with a parameter of Zero allows other threads to execute. The original page must now be modified to start the task by adding the code in Figure 4.
Dim myTaskHandler as TaskHandler
Dim UniqueTaskID as String
myTaskHandler = TaskHandler.Instance()
UnqiueTaskID = mytaskHandler.AddTask(new myLongTask)
Figure 4: Modify the original page.
The key to improving the user s experience is providing accurate feedback. The ITask interface that has been created will allow four pieces of information to be displayed to the user: time taken, time left, percent complete, and a status description. This information must be displayed and updated regularly.
A simple table has been created to provide feedback to the user; the code is shown in Figure 5. The ASP.NET AJAX UpdatePanel is used to update the table; the frequency of update can be tuned depending on the task. The code-behind file has two private methods to enable the table to be updated and reset. The submit button action has been modified to start the task and a timer event subroutine has been added that will call the table update method. The table approach allows the interface to be improved easily using CSS, which enables the potential to implement themes.
Figure 5: Code for a simple table to provide feedback to the user.
The UpdateUI method in the code-behind retrieves the status of the task by getting the instance of the TaskHandler and finding the correct task by passing the unique task ID. The TaskHandler will return an ITask object, which can be queried using the interface methods. The status retrieval implementation is shown in Figure 6.
'Get the Task Handler
myTaskHandler = TaskHandler.Instance()
'Get the task
myTask = myTaskHandler.FindTask(UniqueTaskID.Text)
Dim percent As Integer = myTask.Percent
ProgressBar.Width = (percent * 3).ToString + "px"
ProgressBarOpposite.Width = (3 * (100 - percent)).ToString + "px"
ProgressText.InnerText = percent.ToString + " %"
timeLeft.InnerText = myTask.TimeRemaining + " seconds"
timeTaken.InnerText = myTask.RunningTime + " seconds"
Figure 6: Implementing status retrieval.
Example 2 in the accompanying download files contains the modified code, which now implements the TaskHandler as described (again, see end of article for download details). Unfortunately, the progress reporting is frustrating for the user because the calculations that provide progress indicators are poor. This has resulted in a progress bar, which is commonly seen in Windows, where the task claims it will be finished in 20 seconds, then proceeds to take five minutes all the while promising to finish shortly. If the aim of improving the user experience is to be achieved, the task metrics produced must be accurate. For the metrics to be accurate, it s important to understand what the task is doing.
In the example, the task is dividing each number by an integer until it reaches the square root of that number. With a little thought, it s obvious that if the progress calculations are based on the quantity of numbers to be checked rather than the number of calculations to be performed, the results will be too optimistic and users will not trust them. Figure 7 shows how the progress is currently calculated.
Private Function Progress() As Double
'This calculation is very rough and does not take into
account the actual number of calculations required for
the remaining numbers.
Dim result As Double
result = m_current / (m_finish - m_start)
Return 100 * result
Figure 7: Calculating progress.
To accurately report the progress of this task, the number of actual calculations to perform must be taken into account. Figure 8 illustrates a method that will produce better results.
Private Function Progress() As Double
Dim NumOfCalcsToDo As Long
Dim NumOfCalcsDone As Long
NumOfCalcsToDo = Factor(m_finish) * (m_finish - m_start)
NumOfCalcsDone = Factor(m_current) * (m_current - m_start)
Return ((100 * NumOfCalcsDone) / NumOfCalcsToDo)
Private Function Factor(ByVal i As Long) As Long
'Calculate the Factorial of integer i
'i.e 5! = 5+4+3+2+1 = 15
Dim result As Long
For j As Long = i To 0 Step -1
result += j
Figure 8: Account for the number of actual calculations to accurately report progress.
Example 3 (also available for download) incorporates this code, but at a cost. The more calculations that are performed to work out task progress means less processor time for the task. Therefore, there needs to be a balance between the accuracy of progress reporting and speed with which the task is complete. It is likely that speed of task completion will be the dominant requirement, and a crude estimate of progress will suffice, such as removing the information about time remaining (avoid indicating to the user that we cannot accurately calculate duration) and only provide a progress bar.
The source code accompanying this article is available for download.
Mike Trebilcock is the Information Systems Manager with Cornwall College, Cornwall, UK, one of the largest further education colleges in the UK (http://www.cornwall.ac.uk). Mike has been developing information system solutions since 2000. He specializes in data-driven Web applications and has recently concentrated on utilizing .NET and SQL Server 2005. Mike can be contacted at mailto:[email protected].