This post continues the idea that I wrote in Calling page methods from javascript by method name (ASP.NET AJAX) earlier post.
This time I want to use server page methods to query the server from browser about status of some long-running processes.
There are some situations when some processes take too long to complete, send hundreds of emails for example, or perform some complex operations etc. If we don't return the control to the user shortly, UI will not be the user-friendly, user will just wait and look at blank screen trying to guess what is going on on server, and what is the status of current operation.
Actually there are two problems to solve.
1) We need to execute methods for long-running tasks asynchronously on the server. For this we can use additional threads in thread pool or any other asynchronous processing approach(start new thread using ThreadStart delegate, execute method using timer control in System Threading namespace, etc).
2) We should not freeze UI for the user while tasks are in progress. Even if we use asynchronous methods on the server, there will be a moment when task completes and results should be shown to the user. So if we just wait the completion of tasks on the server user will see blank screen.
To go around these problems I want to use the following scheme:
1) We call server-side method from browser when we need to start new long-running process. The method returns without waiting for process completion.
2) We query for statuses of processes being executed on the server from client browser polling server with ajax requests periodically. The time (polling frequency) is to be chosen carefully. If we query the server every half a second, the network with low bandwidth will work very slowly and too much asynchronous calls will be queued by client browser. In the other hand if we choose 10 seconds, the status that is displayed to the user will update less frequently. The time depends on the type of application, type of executing tasks. It is ok to update the status even once in a minute if process takes to complete 1 hour. So this is to choose by software developer, or give this chance to the user - add some control for him to choose the refresh rate.
Completed sample application looks like:
You can see the application online or download source code
I have created a class named ProcessStatus, the class represents one process running on the server, and it contains some basic properties for process, - Name and Status(completion). The code for the class looks like:
namespace Devarchive_net { public class ProcessStatus { public ProcessStatus(string name) { m_Name = name; } private int m_Status = 0; public int Status { get { return m_Status; } set { m_Status = value; } } private string m_Name = ""; public string Name { get { return m_Name; } set { m_Name = value; } } public void IncrementStatus() { lock (this) { m_Status++; } } } }
Only one thing to note here is IncrementStatus method. As you see I surrounded increment operation inside the method with sync lock applied to current instance of the class(this). This way we avoid threading issues and make status modification operation thread-safe. For the simple operation as increment is, you could use the Interlocked class as an alternative.
Here is a class diagram for the ProcessStatus class:
Please remember this two public properties. We will be pleased with some nice surprise later in this article - Page method's result will be serialized to the object using JSON to use in client script. Ok later I will return to this.
The second class is ProcessStatuses:
using System; using System.Collections; using System.Threading; using System.Web; namespace Devarchive_net { public class ProcessStatuses { private const string m_SessionKey = "ProcessStatusesKey"; public static ArrayList Get() { if (HttpContext.Current.Session[m_SessionKey] == null) { HttpContext.Current.Session[m_SessionKey] = new ArrayList(); } return (ArrayList)HttpContext.Current.Session[m_SessionKey]; } public static void StartProcessing(object data) { ProcessStatus process = (ProcessStatus)((object[])data)[0]; while (process.Status < 100) { process.IncrementStatus(); Random rnd = new Random(DateTime.Now.GetHashCode()); Thread.Sleep(((int)(rnd.NextDouble()*40)+10) * 10); } Thread.Sleep(2000); ArrayList.Synchronized((ArrayList)((object[])data)[1]).Remove(process); } } }
The class contains two important methods.
Get() method returns ArrayList object that contains the set of ProcessStatus objects. Get methods is static. It stores collection of process items in Session. This way sets of processes are separated between users.
StartProcessing method is used for imitation of long-running process, in this case for demonstrating purposes I use Thread.Sleep() method to imitate some time-expensive task. I also pass random number of millisecond to the Thread.Sleep method to imitate the different progresses for different threads (in real life it is true, one process may complete faster than another). Last line of StartProcessing method just removes the process object from collection of active processes in a thread-safe manner. The moment when this is done will be after status of a process reaches 100% . Also note - StartProcessing method will be executed in different thread, in order not to lock main thread in which the asynchronous call from client browser will be processed.
StartProcessing method takes parameter of type object, actually we will pass here array of objects - object with index 0 is instance of ProcessStatus class, this is process we are working on, and second - with index 1 - is collection of objects. I am removing process from the collection when job is done.
Now lets create new method in Command class (please see what is the Command class in previous post, I use that architecture to call server methods).
In that class I will add two methods:
/// <summary>
///Launch the new process with
///name specified in object "data"
/// </summary>
/// <param name="data">
///Name of a process
/// </param>
/// <returns>null</returns>
public objectLaunchNewProcess(objectdata)
{
ProcessStatus newProcess =
newProcessStatus(
String.Format(
"{0}, started:{1}",
data, DateTime.Now.ToString("HH:m:ss")
)
);
ArrayList allProcesses = ProcessStatuses.Get();
ArrayList.Synchronized(allProcesses).Add(newProcess);
ThreadPool.QueueUserWorkItem(
newWaitCallback(ProcessStatuses.StartProcessing),
new object[] {newProcess, allProcesses}
);
return null;
}
/// <summary>
///Returns Array of ProcessStatus
///objects to the client script
/// </summary>
/// <param name="data">
///anything, this data
///is not processed in this method
/// </param>
/// <returns>
///Array of ProcessStatus objects
/// </returns>
public objectGetProcessStatuses(objectdata)
{
ArrayList allProcesses = ProcessStatuses.Get();
lock(allProcesses.SyncRoot)
{
returnallProcesses.ToArray();
}
}
The first method LaunchNewProcess creates object of type ProcessStatus and assigns to it name passed from client browser in method parameter (data). It retrieves current set of processes that are executed at the moment, then it adds the newly created process to that set and starts execution of long-running process in a new thread. I use ThreadPool for this purpose, however you can use any asynchronous execution approach, .NET gives us a lot of alternatives here.
Method returns after this. This way the thread of a context where the method executes does not freeze.
The second method - GetProcessStatuses is called from browser every several seconds. The method returns array of ProcessStatuses objects. Remember I said about nice surprise? Now also keep in mind the following fact - the method returns array of ProcessStatuses objects. Lets see later how we use that returned object in javascript.
Lets write client side UI and scripts. Here is a complete code for Default.aspx page:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> <%@ Register assembly="AjaxControlToolkit" namespace="AjaxControlToolkit" tagprefix="ajaxToolkit" %> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title> Displaying Progress Bar for Long-Running Processes </title> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager EnablePageMethods="true" ID="MainSM" runat="server" ScriptMode="Release" LoadScriptsBeforeUI="true"> <Scripts> <asp:ScriptReference Path="~/Scripts/Main.js" /> </Scripts> </asp:ScriptManager> <script type="text/javascript"> Sys.Application.add_load( applicationLoadHandler ); Sys.WebForms.PageRequestManager.getInstance().add_endRequest( endRequestHandler ); Sys.WebForms.PageRequestManager.getInstance().add_beginRequest( beginRequestHandler ); var mHandlers = {}; mHandlers.Void = function(obj) { ; // nothing to process }; mHandlers.GetStatuses = function() { // call server method // which gets an array // of currently executing processes mainScreen.ExecuteCommand( 'GetProcessStatuses', 'mHandlers.ProcessStatuses', null); setTimeout( "mHandlers.GetStatuses();", parseInt( $get("<%=lblSlider.ClientID %>").innerHTML ) ); }; mHandlers.ProcessStatuses = function(obj) { var resultDiv = $get("resultDiv"); if(obj) { resultDiv.innerHTML = mHandlers.BuildProcessList(obj); } else { resultDiv.innerHTML = " "; } }; mHandlers.BuildProcessList = function(obj) { var i = 0; if (obj.length==0) return " "; var result = "<table " + "cellspacing='0' " + "cellpadding='3' " + "width='99%'>"; result += "<td " + "align='left' " + "style='width:1%; white-space:nowrap'>" + "<b>Process Name</b>" + "</td>"; result += "<td " + "align='left' " + "style='width:89%;'>" + "<b>Progress</b>" + "</td>"; result += "<td " + "align='left' " + "style='width:10%; white-space:nowrap'>" + "<b>Completion</b>" + "</td>"; for (i=0; i<obj.length; i++) { result += "<tr>"; result += "<td " + "align='left' " + "style='width:1%; white-space:nowrap'>" + obj[i].Name + "</td>"; result += "<td " + "align='left' " + "style='width:89%;'>" + "<div " + "style='width:100%; " + "background-color:white' " + ">" + "<div " + "style='width:" + obj[i].Status + "%; background-color:" + (obj[i].Status < 100 ? "red" : "green") + ";'>" + " </div>" + "</div>" + "</td>"; result += "<td " + "align='left' " + "style='width:10%; white-space:nowrap'>" + obj[i].Status + " %" + "</td>"; result += "</tr>"; } result += "</table>"; return result; }; </script> <div> Click "Start new process" several time and update refresh speed to see status of server-side processes real-time </div> <br /><br /> <div> Process Name: <input type="text" id="processName" value="File Download" /> <input type="button" value="Start new process" onclick=" mainScreen.ExecuteCommand( 'LaunchNewProcess', 'mHandlers.Void', $get('processName').value); " /> <br /> <br /> </div> <div> <br /> <div style="float:left; width:100px;"> Update speed: </div> <div style="float:left; width:200px;"> <ajaxToolkit:SliderExtender ID="seUpdateSpeed" runat="server" BehaviorID="tbSlider" TargetControlID="tbSlider" BoundControlID="lblSlider" Orientation="Horizontal" Minimum="500" Maximum="5000" Steps="10" EnableHandleAnimation="true" TooltipText="Slider: value {0}. Please slide to change value." /> <asp:TextBox ID="tbSlider" runat="server" style="right:0px" Text="500" /> </div> <div style="float:left"> <asp:Label ID="lblSlider" runat="server" Text="500" /> Milliseconds</div> </div> <br /> <br /> <br /> <div style="border: dashed 1px black;" id="resultDiv"> </div> </form> </body> </html>
In short here we call LaunchNewProcess server method when new process should be launched (button click), and we call GetProcessStatuses server method periodically to get processes and their statuses from the server. When server returns results of second call, I just dynamically create HTML and show it to the user.
The main important thing here - is the surprise I was talking about above. note how I access object returned from the server - "if (obj.length==0)". So actually from javascript we see the object as array ! This means we returned the array from server and AJAX framework serialized this object into array in js, this is great isn't it? It is not all. See how I access the properties of each object in array : obj[i].Name, obj[i].Status. This means - we returned array of ProcessStatus objects from server, and they are serialized into similar objects on client side !
Also I have modified the Main.js file (See full listing of the file in previous post). I modified Init function:
mainScreen.Init = function() { /// <summary> /// Initializes mainScreen variables /// </summary> setTimeout("mHandlers.GetStatuses();", 100); };
As you see I just initiate call to GetStatuses function first time, after this it calls itself periodically using setTimeout function.
To see whole picture download the source code, and see it live !
Hope this helps.