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.
33 comments:
Nice article but some things are escaping me - I am pretty new at this!
How should I hook this up to real processes like functions to send some Email or doing database stuff.
How would I return and display status text from that process.
Thanks
Bill
Hi Bill,
The most important code line is :
ThreadPool.QueueUserWorkItem(
newWaitCallback(ProcessStatuses.StartProcessing),
new object[] {newProcess, allProcesses}
);
the line is in LaunchNewProcess method.
So what it does -
It uses ThreadPool.QueueUserWorkItem method to start static method ProcessStatuses.StartProcessing in a new thread of ThreadPool.
So my answer will be - you should make long-running database calls, send emails etc in StartProcessing static method of ProcessStatuses class.
At the moment it looks like this:
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);
}
But - actual implementation of the method is up to you, Instead of calling Thread.Sleep - you can send several emails. For example if there is 3000 emails to send, 3000/100 = 30 emails will reflect one percent of process completeion, so you could send 30 emails and then update process status incrementing it's completion percent by one.
But this is only one approach among many.
If you need to read or write very large file to disk, or communicate over the network with remote machines to send/recieve some data - .NET framework gives us Asynchronous Programming Model ready to use. With asunchronous programmiung model you can poll for a status of executing process using "Polling Model" :
// -------------------
// C#
byte[] buffer = new byte[100];
string filename =
string.Concat(Environment.SystemDirectory, "\\mfc71.pdb");
FileStream strm = new FileStream(filename,
FileMode.Open, FileAccess.Read, FileShare.Read, 1024,
FileOptions.Asynchronous);
// Make the asynchronous call
IAsyncResult result = strm.BeginRead(buffer, 0, buffer.Length, null, null);
// Poll testing to see if complete
while (!result.IsCompleted)
{
// Do more work here if the call isn't complete
Thread.Sleep(100);
}
// Finished, so we can call EndRead and it will return without blocking
int numBytes = strm.EndRead(result);
// Don't forget to close the stream
strm.Close();
Console.WriteLine("Read {0} Bytes", numBytes);
Console.WriteLine(BitConverter.ToString(buffer));
// -------------------
here using result.IsCompleted property you can find out what is a status of asynchronous operation.
and more thing - Thread pool is for relatively short tasks, you should use ThreadStart Delegate to launch really long running process.
Hope this helps,
Kirill
Got it!
But I would also like to return status text from the server - eg "sending Email to Bill"
Any suggestions?
Thanks
Bill
Yes actually the ProcessStatus class is used for this - it is nothing more then a object holding a status of a process.
In the sample it defines two properties -
Status - integer that explains what is percentage of task completion,
Name - the description of the process - this can be the string "Sending emails to subscribed members"
You can easily add another property, CurrentActivity(string) for example, which will reflect current activity of a process("Sending email to subscriber No719"). Then you should modify it's value from running thread in thread-safe manner. Please see how to do this on MSDN documentation.
The Idea is:
You have the method running in the thread, you have ProcessStatus object passed into the method, so ProcessStatus object can be modified from the method in any desirable way.
Same time the client browser is requesting the array of ProcessStatus objects (also in thread-safe manner), so browser gets the statuses and displays them to the user independently from the running threads.
And threrefore you can change the status class structure to hold more properties determining the state of a process, and you can analyze/display that object from javascript.
Thanks.
Kirill
Thanks very much for the article!
Isn’t it possible to make a stop button that stops the working progress?
//Peter
I modified your code as follows:
public static void StartProcessing(object data)
{
ProcessStatus process = (ProcessStatus)((object[])data)[0];
while (process.Status < 100)
{
process.IncrementStatus();
RunMyProcess();
}
ArrayList.Synchronized((ArrayList)((object[])data)[1]).Remove(process);
}
I only need RunMyProcess() to run once but the code as it is now runs it 100 times. How should I fix this code, to run once only?
Brilliant, Thank you this blog helped me more to learn.
I have also updated some content about displaying progress bar yesterday in my blog but using XMLHttpRequest and DHTML.
Cheers,
Srinivas
Excellent articale, this saved me alot of time and effort.
Thanks
Matt
Hi Krill
Great article.. But i have a query this progress bar is not working fine with Mozilla Firefox browser . Could you please send back to me if any solution for the same. Appreciate your quick
responses
Thanks
Sarath
Hi,
The sample works in Firefox and IE just fine, here is a live example:
http://www.devarchive.net/displaying_progress_bar_for_long_running_processes.aspx
Hi
I Integrated your progress bar code with my application..But same progress bar is coming again if i'm doing the next process after one is completed. So i just want to know if any way to refresh or remove all the process bar.
Thanks
Rob
Hi Krill,
As per your code now for each click one progress bar will come. I just want to modify it like. for the second click first progress bar need to be cleared and the new one should come. Could you please help me on this
Thanks
Rob
Hi Rob,
What exactly do you want to do - just remove visualization on client, or stop the process on the server as well ?
Kirill
I just want to clear the session. If the user press second time
Thanks
Rob
Rob,
You have to modify ProcessStatus class by adding the following property:
-------------
private bool m_ExitRequested = false;
public bool ExitRequested
{
get { return m_ExitRequested; }
set { m_ExitRequested = value; }
}
----------------
Then you can analyze that property inside threads and decide is the thread in a crytical section or not - and exit the thread or continue untill safe point is reached. For example you can modify the thread code (StartProcessing method in ProcessStatuses class) the following way:
--------------------
public static void StartProcessing(object data)
{
ProcessStatus process = (ProcessStatus)((object[])data)[0];
while (process.Status < 100)
{
if (process.ExitRequested)
{
ArrayList.Synchronized((ArrayList)((object[])data)[1]).Remove(process);
return;
}
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);
}
--------------------
Finally you can request exit of threads in the code requesting start of new threads (LaunchNewProcess method in Command class), by modifying it the following way:
-------------------------
public object LaunchNewProcess(object data)
{
ProcessStatus newProcess =
new ProcessStatus(
String.Format(
"{0}, started:{1}",
data, DateTime.Now.ToString("HH:m:ss")
)
);
ArrayList allProcesses = ProcessStatuses.Get();
foreach (ProcessStatus status in ArrayList.Synchronized(allProcesses))
{
status.ExitRequested = true;
}
ArrayList.Synchronized(allProcesses).Clear();
ArrayList.Synchronized(allProcesses).Add(newProcess);
ThreadPool.QueueUserWorkItem(
new WaitCallback(ProcessStatuses.StartProcessing),
new object[] {newProcess, allProcesses}
);
return null;
}
--------------------------
Regards,
Kirill
Hi Kirill,
Really Great !.. Thanks.. now its working fine. Actually i'm configuring multiple number of process using your progressbar.. Its really working great. Thanks Again
I'm Rob from india
Thanks
Rob
Rob,
You are welcome,
I am glad sample is useful for you.
Thanks,
Kirill
I’d weakness to tattle-tale that too!
hi Kirill,
Its really great article.
I m using this approch to generate so many reports.
I want to know one thing. I have used MyProcess() method instead of StartProcessing(). I want to pass some variables which need to be use in report generation process. How can i pass those variables? Please suggest.
I Have a functionality in which id not have the estimate for DB Operation Running time and its a single Call for database .in this case How can i Update the progress bar percentage.
Hi Ashwin,
Actually this sample just shows the idea of synchronising data between threads.
You can provide "Loading..." description instead of updating percentage if you want.
Regards,
Kirill
Once again, this is absolutely fantastic! I will learn quite a bit from it. I decided to go ahead and publish so you can see the problem I am experiencing. I believe I have implemented it exactly as your code. The progress bar fills immediately, even though you can see the status is not 100%. It even changes to green properly. Please visit www.twsdb.com and select "test progress bar" from the top of the left hand menu. Can anybody help? Please? I really want to use this. Thanks, Mark
Kirill,
I was hoping my web site might pique your interest. I first downloaded your code and tested it thoroughly to ensure it worked, which it did perfectly. I then implemented the same code into my website and encountered one problem. Please visit www.twsdb.com and select "test progress bar" from the top of the left-hand menu. You will see a page that should be quite familiar. It illustrates the problem I am having. The status increments properly and the bar then changes to green properly. However, it is immediately filled all the way to 100%. I don't think I have the ability to figure out why it is doing this and was really hoping you could. I do have some more information that will probably tell you what is happening. I changed the inner div to a hard-coded 50% and I experienced the same result. I also was able to duplicate this problem in your working example merely by changing the inner div slightly (I added an X after the percent sign.) So the problem must be related to this inner div. Can you please help? I really want to use this. It is a fantastic bit of coding! Thanks, Mark
Hi Mark,
This is simple to explain and expected that if you add X sign after percent in css, the widths of div will not be correct again. This requires knowledge of CSS and HTML, I really have no better explanation than this.
Regards,
Kirill
Kirill,
Thanks for your feedback. I finally got it (although I haven't published the working version yet.) I can't tell you how much I appreciate your excellent coding. I spent an entire weekend searching for progress bars and every one I found had some deficiency. Yours seems to be perfect! That was a very nice touch where you sleep the process for a random amount of time from 0.1 to 0.5 seconds to simulate a non-linear process. Very cool. I'm sure I will have more questions when I get to using "real" long-running processes, but everything is working so far. Thanks again for supplying this and thanks for your support.
Regards,
Mark
Kirill,
I was so appreciative that I found this I felt compelled to acknowledge your work. Please visit twsdb.com and select the first menu item on the left-hand side of the home page.
Thanks again,
Mark
This sample stopped working when I changed sessionState mode to "StateServer". Do we have any workaround?
Thanks
I have spent all day puzzling through this code and trying to integrate it with my own. It appears to be both dazzling and incomplete, and leads me to several questions that I will ask as individual posts.
First, I need to have multiple variants of the StartProcessing method. How do I accomplish this? I cannot yet figure out how, especially since this method is part of the ProcessStatuses class.
One thought would be to have three points in this code that raised events in my own:
1) Initialize event
2) Main loop event
3) Completion event
and possibly a fourth: Cancellation event (invoked when ExitRequested).
Can you comment on this and possibly give some examples of how to implement multiple completely different versions of StartProcessing that are outside your ProcessStatuses class?
Thanks!
My second question is, how do I disable the event posting when I am not processing a task. The events every .5-5 seconds serve no purpose if a job is not started, so I would like to be able to turn them off and on myself... but I don't yet see how.
I managed to get one of my issues working - I can now start the web traffic only when the "Start" button is pressed, and stop it automatically when all processes are completed. But I am stuck with a threading issue (and I have never written any threading code).
In StartProcessing, just before the while loop, I need to call a method in my code that will run until it completes, posting status updates to public variables in this class. This needs to run as a separate thread. Within the while loop, I want to check the public variables that record status towards completion. This will allow me to avoid having to break the work up into 100 small chunks.
I can see that StartProcessing is already in a new thread, but I need to launch yet another thread in order to make this work.
Ultimately, I want to be able to identify the method I want to launch and the status checking methods as declarations way up in Default.aspx and have the code fire these events properly, but I am not at all clear how to pull this off.
Any comments, advice and help appreciated.
When I am done with all of this, I plan to have it build as a user control that I can use anywhere and I will be glad to share what I come up with, if I can ever get there.
Thanks,
Bob Jones
Bob, I wish you the best of luck. You have taken this much farther than I did. I basically just had to make some modifications to get it to display properly with my existing CSS. I didn't pursue the issues that you have (although I certainly will use them if you ever post them!). Because it wasn't critical to the operation I let it slide and used the asp:UpdateProgress instead. Thanks for your efforts.
Thank you so much it really helped me
Hi,
This is Ravi,
Provided code was very nice. But it is not working with server side processing. when am calling this script for showing the progress for server side processing, i am getting delay in progress. Could you please help me in how to handle this scenario?
Thanks,
Ravi
Post a Comment