Easy incremental status updates for long requests
AJAX, ASP.NET, JavaScript, UI By Dave Ward on October 3rd, 2007
A problem that has always plagued web developers has been providing detailed progress indication for server-side tasks. The stateless nature of the HTTP protocol makes implementing a mechanism for constant, stateful progress information cumbersome. The main problem is that a given group of server side tasks will generally only result in one, aggregate response from the server.
In the ASP.NET community, several solutions have been offered. Some even provide an entire framework for monitoring the status of tasks in progress. However, they are convoluted and depend on constant server polling. I find polling for progress indication to be an inefficient approximation, when exact data is readily available.
With that in mind, I’d like to share an alternate method that I’ve used in the past with great success.
Note: While this example is written in ASP.NET and C#, the technique I’m going to describe is very easily portable to any language and platform.
Worse than watching paint dry
Suppose you had a long running group of tasks, such as these:
public void PrepareReport() { // Initialization. System.Threading.Thread.Sleep(10000); // Gather data. System.Threading.Thread.Sleep(6000); // Process data. System.Threading.Thread.Sleep(20000); // Clean up. System.Threading.Thread.Sleep(4000); }
That’s 40 seconds we’re hoping the user will patiently wait on some sign of life from the server. Assuming that something went wrong, users are likely to give up and navigate away during something that runs this long.
Constant, granular progress feedback is exactly what’s needed to keep the user engaged while the tasks complete.
Asynchronously executing the tasks
The first thing we need to do is get away from the HTTP request/response cycle, by using some sort of asynchronous execution model. By doing that, the browser is still available to update the UI throughout the process execution. Rather than use formal AJAX, I chose to do something even easier.
Iframes: They’re underused, given the power and flexibility they provide, but not today. In this scenario, using an iframe is a great way to asynchronously execute the long running process, without being dependent on any particular AJAX framework and without tying up an XMLHttpRequest.
<input type="submit" value="Start Long Running Process" id="trigger" onclick="BeginProcess(); return false;" />
<script type="text/javascript"> function BeginProcess() { // Create an iframe. var iframe = document.createElement("iframe"); // Point the iframe to the location of // the long running process. iframe.src = "LongRunningProcess.aspx"; // Make the iframe invisible. iframe.style.display = "none"; // Add the iframe to the DOM. The process // will begin execution at this point. document.body.appendChild(iframe); } </script>
When the button is clicked, an invisible iframe is dynamically generated and used to execute LongRunningProcess.aspx in the background. Next, let’s take a look at exactly what goes on when LongRunningProcess.aspx is executed.
Streaming update information during the process
In LongRunningProcess.aspx.cs’s Page_Load, I’m going to include the same set of tasks as before, but I’m also going to stream constant updates about what stage of the process is executing.
protected void Page_Load(object sender, EventArgs e) { // Padding to circumvent IE's buffer* Response.Write(new string('*', 256)); Response.Flush(); // Initialization UpdateProgress(0, "Initializing task."); System.Threading.Thread.Sleep(10000); // Gather data. UpdateProgress(25, "Gathering data."); System.Threading.Thread.Sleep(6000); // Process data. UpdateProgress(40, "Processing data."); System.Threading.Thread.Sleep(20000); // Clean up. UpdateProgress(90, "Cleaning up."); System.Threading.Thread.Sleep(4000); // Task completed. UpdateProgress(100, "Task completed!"); } protected void UpdateProgress(int PercentComplete, string Message) { // Write out the parent script callback. Response.Write(String.Format( "<script>parent.UpdateProgress({0}, '{1}');</script>", PercentComplete, Message)); // To be sure the response isn't buffered on the server. Response.Flush(); }
For each step of the process, this outputs a JavaScript callback to the page that opened the iframe. Using the combination of Response.Write and Response.Flush ensures that the callbacks are emitted in real-time, as the tasks are completed.
Since the relative run time of my example tasks are known in advance, I included completion percentages as well as the status messages. The raw output will look something like this:
<script>parent.UpdateProgress(0, 'Initializing task.');</script> <script>parent.UpdateProgress(25, 'Gathering data.');</script> <script>parent.UpdateProgress(40, 'Processing data.');</script> <script>parent.UpdateProgress(90, 'Cleaning up.');</script> <script>parent.UpdateProgress(100, 'Task completed!');</script>
*Note: Internet Explorer buffers the first 256 bytes of any response. To circumvent that behavior, it’s important that you push 256 characters down the response before anything else. Otherwise, the first few status updates will be buffered and the callbacks won’t be executed at the correct time (or at all, visibly) for users running IE.
Handling the callbacks and displaying status updates
The final step is to create a client side function to handle the callbacks coming from the iframe. To keep things simple, we’ll use the submit button’s text value to display the status updates.
function UpdateProgress(PercentComplete, Message) { document.getElementById('trigger').value = PercentComplete + '%: ' + Message; }
Now, the submit button’s text will continuously update throughout the process, to reflect the current status of the process in real-time.
We’ve only just begun (and by we, I mean you)
I purposely kept the presentation very simple in this example, so that the underlying mechanism isn’t obscured by DHTML code.
However, if updating the button’s text isn’t visually stimulating enough for you, it’s easy to dress this technique up any way you want. For example, if you return percentages in your implementation, jsProgressBarHandler would be trivial to integrate with this technique and provides an excellent visualization.
You could do something similar to this, without an iframe, by using a sequence of page method or web service calls that had progress updates in between. The problem is that you’d have to move a lot of your logic into the presentation layer to do that. I would strongly advise against going down that road, when avoidable.
In real world use, you’re probably going to want to specify parameters to the long running process. Loading it in the Session is one way do to that. Even better, if you can manage it, is to provide arguments on the QueryString when setting the iframe’s src attribute. Then, you can check those parameters in LongRunningProcess.aspx.cs and respond accordingly.
As always, taking the source code for a spin is the best way to get a feel for it. Let me know what you think.
Similar posts
What do you think? Your comments are welcome.
I appreciate all of your comments, questions, and other feedback, but please try to stay on topic. If you have a question unrelated to this post, I recommend posting on the ASP.NET forums instead.
If you're replying to an existing comment, please use the threading feature. To do this, click the "Reply to this" link underneath the comment you're replying to.


Awesome.. Thanks a lot. :)
Great post! It’s nice to see that I’m not the only one who has used iframes to simulate ajax. (Or is it still ajax? Who knows anymore.) =)
Thanks, Ira.
I’m right there with you on the iframes. I’d been using them for remote script calls quite a while when the AJAX craze hit.
If only I’d thought of putting an arbitrary label on it back then!
I like the approach, but I don’t understand how the content is actually returned back. For example, say LongRunningProcess.aspx is dynamically generating a PDF. If we are sending back HTML/Javascript periodically to update the status, but then later return the binary PDF content in the same response, how is the browser going to display the PDF? It will assume that the binary content is still “text/html” and thus will just display a bunch of gobblygook in the browser instead of displaying the PDF in the Acrobat plug-in.
I’m probably missing something, so if you could please elaborate on that part, that’d be great!
The final presentation method will vary depending on what sort of data is being returned and how it needs to be displayed.
In the PDF scenario you describe, here are a couple presentation ideas:
If you wanted a PDF download at completion, you could emit a document.href JavaScript block after the 100% progress update. Using that, you could redirect the iframe to a download script that served the PDF up with the correct binary response type. Similar to this iframe download method.
If you wanted to display the PDF in the calling window, you could use parent.document.href to redirect it to a PDF file you had saved during the generation step.
Hope that helps. I may write a more specific post in the future, that combines this progress indication with the iframe download technique, if there’s enough interest in it.
I tried combining the progress indication and the iframe download techique, and it works! The problem is now I get the IE info bar message “IE blocked this site from downloading files…”. If I click the info bar and select Download File, I have to the generate the file again (bummer).
My implementation details: I modified UpdateProgress to call a new function GetReport when we reach 100%. GetReport creates a hidden iframe and sets the iframe.src to a page that sends the report to the browser as a download. (Is that what you meant by issuing a document.href?)
I’d be interested if someone has a work around for the info bar message. If not, I’ll probably resort to displaying a download link on the page after the long process is complete (an extra step for the user.) I plan to hold the generated report temporarily in a session variable.
BTW, Dave, I’m really glad to have found your site. Wonderful information here!
Thanks for the compliment. I’m glad you’ve found the site useful.
If you can, save the file to the file system during generation. Then, after outputting your last parent.UpdateProgress(), output a document.href = ‘filename’.
That will point the already existing iframe to the file and should result in a regular file download prompt.
I would love to see how this could be implemented to track file upload progress.
That’s something I do intend to explore at some point, but don’t have a solid solution for today.
Tracking file upload progress has been an issue for developers for some time now. All I know is to make it ajax(ish) your upload box must be in an iframe. As far as attaching a callback to the upload process, I haven’t quite figured that one out.
The problem is that ASP.NET caches the entire file upload in memory until you do something with it server-side. So, without using a custom HttpHandler, there’s no way to get access to incremental progress information.
could you possibly suggest a way i could use this while my server is creating a data table ? the table takes time and i would like it to send a response to the webpage each time a new row is added to the table. this table is being created on the default.aspx.cs and not the LongRunningProcess.aspx.cs file.
thanks.
You can’t use this technique that way. The separation of your main page and the iframe page is crucial to being able to provide status updates.
document.body.appendChild(iFrame); throws a JScript error. I’m unable to run this code :(
JavaScript is case sensitive. iFrame isn’t the same thing as iframe.
I downloaded the code and ran Default.aspx. It causes the button to blur and disables it in the browser. However, the code in LongRunningProcess.aspx never gets called :(
You might want to check or delete the web.config. I believe it was 3.5 targeted, which caused some trouble for some people.
Hi Dave,
I liked this hack (if i may) alot… Is there anyway I could reproduce something similar in an update panel where response.flush() will not work?
Thanks,
Eugene.
Unfortunately, no. That’s more or less the problem that this technique aims to solve.
Great post i like it :)
I downloaded your sample and I encountered a problem where all the scripts injected into the iframe fire at the end of the processing and not after the 0, 25, 40, 90, and 100 % events when using IE6.0. Using Firefox seems to be ok.
This is a great article and i hope to see it working.
If you call the LongRunningProcess.aspx page directly, does it progressively output its callbacks? Make sure you left the 256 bytes of padding in the beginning. IE buffers the first 256 bytes of a response.
Dave the cause seems to have been Fiddler, which was running in the background. Thanks for the help.
Thanks for the follow up. That’s good to know about Fiddler.
Hi Dave,
I was wasting so much time trying to find a simple solution to this simple problem ! Thanks a lot for your tricky technique using an iframe :)
At this point I need to post a large amount of values to LongRunningProcess.aspx (a list of users ids from a textarea). So far all the solutions I’ve tried couldn’t access my textarea object …
Did I miss something or do you have any workaround or advice ? :-/
Thanks again !
Olivier
You won’t be able to access that control directly in LongRunningProcess.aspx, since nothing is POSTed there and the ViewState information isn’t available. However, you could send that information on the QueryString.
For example, if you would normally have used this statement to set the iframe’s src:
You could instead use something like:
Then, in LongRunningProcess.aspx.cs:
Hope that helps.
Hi Dave and thanks for your help ! Unfortunately, my application has to accept that the user potentially pastes a large amount of station names or user IDs in the textarea, so the HTTP GET method using QueryString won’t be appropriate :(
I suppose there is another unelegant but working option : I can make a “two steps” process where the user first clicks on a button that makes standard form validation (thus storing the textarea content in the Session, as you suggested at the end of your article), and then I could probably enable the button that creates the iframe and fires the LongRunningProcess.
I’ll try it today. I would be very dissapointed if I can’t have this to work, as your trick is really the most simple and efficient way to provide a progress bar dynamically.
Thanks again and … keep up that good “Encosia” phylosophy ! (and forgive bad english from a lost froggy;))
Olivier
If 4k is enough, you could store it in a cookie on the client side, before creating the iframe, then read that in LongRunningProcess.
Another option would be to use a partial postback to store the data in the Session when your button is pressed. In that partial postback, you could use ScriptManager.RegisterStartupScript to call BeginRequest() and automatically start the iframe creation code that would trigger the long running process.
It introduces more complexity than I like to advocate, but it would probably be better than requiring the user to click a sequence of two buttons.
you have suggested
iframe.src = “LongRunningProcess.aspx?foo=bar”;
BUT
how to pass runtime arguments ? I mean what if my default.aspx page has FileUpload control and I want to pass the File name with path to longprocess.aspx page to process that file. how can i accomplish that?
Hi Dave,
4k wouldn’t be enough as the user might potentially paste a list of thousands of station names or user IDs … (for instance 80000 -hey, we got quite a big LAN here huh huh), so no cookie there.
BUT : your second solution is EXACTLY what I was trying to do, and it works LIKE A CHARM (and so easy to implement btw!). I didn’t know about the RegisterStartupScript() function, but it seems like there are lots of things I still don’t know ;)
I have to say a very big THANK YOU for your advices !! When I’ll have time, I’ll read your site entirely; seems like there is holy food there ;)
Thanks & Respect !
Olivier
I just tried to run the Download project in Visual Studio 2008.
I get this error about my web.config: “Could not load file or assembly ‘System.Data.DataSetExtensions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′ or one of its dependencies. The system cannot find the file specified.”
You should be able to delete that line from the web.config without affecting the demo.
OK, now it runs. Thanks!
Hey Dave, I really like this solution of yours, it’s exactly what I’ve been looking for!
However I find that it doesn’t work quite as intended in IE7. Your example works perfectly in FF3, but unfortunately in IE7 it doesn’t display any of the status updates, only the 100% update once the longrunningprocess is completed.
I tried running the longrunningprecess.aspx directly but IE bombs out after outputting the buffer and half of the first status update output, because it (obviously) cannot find a parent object to call the display update function from.
I also deleted the web.config and started over with a fresh new one, but still the same thing.
Any ideas? Do you think it could be a setting on my test web server?
I noticed that hitting the escape key stops the update… any way to stop that?
Otherwise this is GREAT! Thanks!
Which browser are you seeing that happen in?
sorry.. it’s IE7
How can I force a file download (save-as) dialog after I show the status? Basically, I’m geting files saved in a database, zipping them up, and streaming the zip-file up to the browser for the user to “save-as” at their end. But, because I’m already sending responses back to the browser, I get the “can’t add header” error when I try to stream the file. I can’t create a physical file on the server to redirect to, I need to stream the file up from memory. Is this a possibility? I like how this works, and the user gets prompted that something is going on while the files are downloaded and zipped, but they then need to get the zip file somehow. I have the whole zipping files from database and sending to browser from memory working, just can’t send it after I show status per this method… (great method by the way..) thanks.
The best way I’ve found to handle this is to output a document.location as the final progress update, which redirects the iframe to a location that serves the newly generated file.
Usually, I just write the file to a temp folder and redirect the iframe to that physical path. However, depending on the size of the file, you could also shuffle it into memory (Session or Cache) temporarily, redirect to a page that serves it, then destroy it in memory.
Hi Dave,
I’m trying to use it in a content page within my master page (VS2005) but unfortunately it does not update the label :-( Any ideas? Many thanks!!
From a usability point of view, the page is always in loading state and if user stops it with browser, the connection is closed.
What is the alternative for server push in .Net?
Other than using a plugin or ActiveX control, this is it for now.
Although this workaround has been used for some time, it still feels very hackey. I’d suggest putting in the tiny bit of effort, and using web services or callbacks to get the desired ‘Progress indicator’ effect for long running tasks.
For extremely long running tasks, think you’re right that polling is often a better solution.
For most common scenarios, I usually prefer this comet method, for the reasons outlined in the post.
Hi,
This article was very helpful due to its simplicity. I tried to adapt it to a chat page which relies on the Application object, but placing a scriptmanager and an updatepanel with a button into the page with the hidden iframe is giving me trouble.
For some reason, pushing the button gives me a javascript exception:
Sys.ParameterCountException
when calling method: [nsIDOMEventListener::handleEvent]
I searched for a long time and haven’t found the problem. I guess where I started is the best place to ask.
The Sys.ParameterCountException means that one of the ASP.NET AJAX client-side functions is being called with too few parameters. For example, if you called $addHandler with less than the 3 parameters it requires you would get this error.
Just put a scriptmanager, updatepanel and a button on the page. Insert this iframe at <body onload and you can see that pressing the button doesn’t do postback. Pressing twice causes the above exception.
As long as the .src doesn’t finish its Page_Load, I can’t do any ajax postback on the page. Is there a way of preventing this? I thought iframe was for asynchronious stuff. Am I doing something wrong?
Is there no way that i can Response.Write from inside a thread?
I have the following and its throwing an error: Response is not available in this context by the UpdateProgress method.
protected void Page_Load(object sender, EventArgs e)
{
Response.Write(new string(‘*’, 256));
Response.Flush();
}
private void LoadSDIBatch()
{
UpdateProgress(“Retrieving list of users found per SDI match”);
List UserBatchList = new List();
UserBatchList = sm.UserGetBatchList(datefrom, DateTime.Now);
UpdateProgress(“Found ” + UserBatchList.Count.ToString() + ” matches for the selected date range.”);
}
private void LoadSDIBatch_Thread()
{
Thread _thread = new Thread(LoadSDIBatch);
_thread.Priority = ThreadPriority.Lowest;
_thread.Start();
}
protected void UpdateProgress(string Message)
{
// Write out the parent script callback.
Response.Write(“parent.UpdateProgress(‘” + Message + “‘);”);
// To be sure the response isn’t buffered on the server.
Response.Flush();
}
No, you can’t do that. Your thread doesn’t have access to that request’s HttpContext. There’s not really any way to make it work that way, unless you set up a web service to poll that thread’s status, or something like that.
Hi,
Thanks for sharing this neat trick. I try to make this a more general callback technique for my webservices.
I have an IFrame that have its src set to an aspx page which in its page_load waits in a loop for a session variable change.
This triggers a Response.write of a javascript function that calls something on the main javascript file doing the real update.
The session variable will be changed by different webservices doing lengthy operations.
The problem which I encounter is that the Page_load feeding the IFrame seems to blocks any webservices until it returns.
protected void Page_Load(object sender, EventArgs e)
{
Response.Write(new string(‘*’, 256));
Response.Flush();
bool hold = false;
do
{
if (Session["hold"] != null)
hold = (bool)Session["hold"];
System.Threading.Thread.Sleep(100);
}
while (!hold);
Response.Write(“parent.admctest(‘interupt wservice’)”);
Response.Flush();
}
So I adapted this technique to a python framework – and it works perfectly in firefox – but does not seem to work in safari. Safari doesn’t seem to execute any of the scripts until everything has been sent.
-Preston
The issue with Safari (at least as of version 4) appears to be the same as with IE, but to a greater extent. If you increase the padding from 256 bytes to 1,024, this works in Safari.
Yes – the padding was part of it.
In addition to the padding – I found I had to add some sort of closing style tag – so in my cherrypy controller the padding line looks like:
yield ’0′ * 1200 + ”
working great now.
-P
ack – tag was stripped:
yield ’0′ * 1200 + ”
Great!
Found this after a looooooong search and loads of discarded complex methods.
Works a treat with vb, aspx, web developer 2008 and even master pages.
Remember to set AutoEventWireup=”true” in your LongRunningProcess.aspx
vb example:
Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
Response.Write(Space(256))
Response.Flush()
For N = 0 To 10
System.Threading.Thread.Sleep(1000)
UpdateProgress(N, “some message”)
Next
End Sub
Sub UpdateProgress(ByVal PercentComplete As Integer, ByVal Message As String)
Response.Write(“parent.UpdateProgress(” & PercentComplete & “, ‘” & Message & “‘);”)
Response.Flush()
End Sub
Many thanks Mr Ward.