Are We There Yet?

Alleviate the Anxiety of Long-running Tasks with ASP.NET AJAX

CoverStory

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.

 

Solution

Advantages

Disadvantages

Do nothing.

  • No work required.
  • If the delay is less than 10 seconds this will probably be satisfactory.

 

  • Users may try to resubmit the work, doubling the server workload.
  • Users might assume the Web site is not working.
  • Users may use a competitor s Web site that provides feedback and gives them the confidence that the Web site is working.
  • Browser/Client may time out.

 

Implement a Please Wait page.

  • Simple, minimal amount of work.
  • The user is given some feedback.

 

  • For longer waits it doesn t give the user the confidence that the task is still working.
  • Browser/Client may time out.

 

Provide user with feedback on task status and expected run time by implementing META Refresh.

  • User is confident that the server is still processing the task.

 

  • Requires extra code.
  • Web page will flicker or go blank with each request.

 

Provide user with feedback on task status and expected run time by implementing AJAX.

  • User is confident that the server is still processing the task.
  • Uses ASP.NET AJAX, a free add-on to ASP.NET 2.0

 

  • Requires extra code.

 

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

ASP.NET AJAX is a free add-on to ASP.NET 2.0 that can be downloaded from Microsoft at http://www.microsoft.com/downloads/details.aspx?FamilyID=ca9d90fa-e8c9-42e3-aa19-08e2c027f5d6&displaylang=en. Once installed, ASP.NET AJAX integrates into Microsoft Visual Studio 2005 or Microsoft Visual Web Developer, allowing easy implementation of AJAX. AJAX allows parts of Web pages to be updated individually without refreshing the whole page. Although AJAX can be implemented manually using JavaScript and the XMLHTTP object, it is more easily completed using add-ons and components such as 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.

 

Imports System

Imports System.Collections

Imports System.Threading  

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

       Return m_Instance

   End Function

   Public Function FindTask(ByVal id As String) As ITask

     If m_tasks.ContainsKey(id) = False Then

       Return Nothing

     Else

       Return CType(m_tasks(id), ITask)

     End If

   End Function

   Public Sub CancelTask(ByVal id As String)

     CType(m_threads(id), Thread).Abort()

     RemoveTask(id)

   End Sub

   Public Sub RemoveTask(ByVal id As String)

     m_tasks.Remove(id)

   End Sub

   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)

     tr.Start()

     m_threads(id) = tr

     Return id

   End Function

 End Class

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:

 

TaskHandler.Instance()

 

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:

 

TaskHandler.AddTask(myLongTask)

 

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

       Sub Start()

       Function Finished() As Boolean

       Function HasError() as Boolean

       Function Percent() As String

       Function StatusDescription() as String

       Function RunningTime() as String

       Function TimeRemaining() As String

End Interface

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.

 

Providing Feedback

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.

 

Pitfalls

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

End Function

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)

End Function

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

   Next

   Return result

End Function

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.

 

Conclusion

Users expect responsive Web applications; pages that take a long time to return because of necessary processing, or even waiting for other Web applications or services, must be handled effectively. Separating the task from the presentation layer, then executing the task on a separate thread whilst returning a page that will report task progress, will not only improve the Web application, but also user confidence. However, the progress shown must be accurate for the user to truly gain confidence. This can only be achieved if the right metrics are used, although the accuracy of the progress reading will come at a performance cost as the processing of the task may take longer. Therefore, a crude indication of progress completion displayed only by the progress bar will probably suffice. The release of ASP.NET AJAX by Microsoft has made this approach a reality without having to resort to complex JavaScript or third-party controls.

 

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

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish