How to show a progress message while writing a file to the response stream as an attachment

Posted by Blake on 8/11/2011
)

There are a lot of times where I’ve wanted to show a progress bar or waiting message on the screen, then be able to update it after a file has been sent to the browser (invoking the “Save As” dialog). The problem is, the response ends after the file is sent and you can’t send anything else with it (at least not in all browsers, IE for instance doesn’t support multipart attachments). The typical responses to this are:

  1. Write the file to disk, then present the user a link where you can have an ajax callback checking to see if it exists yet.
  2. Open a new window that does the processing and have that window write the attachment (it will then be closed).
  3. Response.Redirect to an ashx handler that will do the same thing as #2.

These are all fine solutions, but what if we already have code in place that makes the above a decent amount of code that has to be rewritten? My first thought was to use the JavaScript “document.readyState” property and have a callback method running in the background that would see if it was “complete”. This worked wonderful for IE9, but didn’t work with any other browser. Chrome and FireFox both accessed this property, but after the initial load of the page it was always “complete” even if it was making new posts to the same page. I can’t just support IE so this wasn’t an option. My fix was this:

  1. When the report generate button was clicked, I use “setInterval” to setup a JavaScript function to run every 100 milleseconds. I then set a cookie with the JavaScript that has a blank value (it is very important that you set the “path” on the cookie to the same that the path will be set at on the web server otherwise the ASP.NET and JavaScript won’t be able to read/write to each others cookies).
  2. The JavaScript method on the page then looks for that cookie… once it has a value it sets the span tags “innerHTML” to blank, uses “clearInterval” so it stops running and then sets the cookie’s value to blank (because we’re finished).
  3. On the server side, before the file is written to the response stream a cookie is set. The value doesn’t matter, I use the current timestamp… it just matters that the value isn’t null or blank which will tell the JavaScript on the client side that the request is done.

I’m going to include the basic code I used below. I’ve tested this in IE9, FireFox 5.0.1 and Chrome 13.0.782.112 m for Windows:

  1. Add the “OnClick” code to your ASP.NET button (or, if you’re not using ASP.NET, add it to your HTML button for the onclick event). I’m using an ASP.NET label which renders out to a span tag:
    If IsPostBack = False Then
        btnGenerateReport.Attributes.Add("onclick", String.Format("interValRef = setInterval('checkState();', 100);setCookie('ReportComplete', '', 1);document.getElementById('{0}').innerHTML = 'Your report is being generated, please wait...';", lblInfo.ClientID))
    End If
            
  2. Add the JavaScript to the page. It is very important that the “path” is set to the same path that you set your cookie with in step #3 so that the JavaScript can write to your ASP.NET cookies and vice versa. The <%=LabelClientId> is just a reference to a property that has the HTML id for the label we want to write to.
     <script language="javascript" type="text/javascript">
        function setCookie(cName, value, exdays) {
            var exdate = new Date();
            exdate.setDate(exdate.getDate() + exdays);
            var c_value = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString());
            document.cookie = cName + "=" + c_value + ';path=/';
        }
        function getCookie(cName) {
            var i, x, y, ARRcookies = document.cookie.split(";");
            for (i = 0; i < ARRcookies.length; i++) {
                x = ARRcookies[i].substr(0, ARRcookies[i].indexOf("="));
                y = ARRcookies[i].substr(ARRcookies[i].indexOf("=") + 1);
                x = x.replace(/^\s+|\s+$/g, "");
                if (x == cName) {
                    return unescape(y);
                }
            }
        }
        function isEmpty(str) {
            return (!str || 0 === str.length);
        }
        ref = 0;
        ref = setInterval("checkState();", 100);
        function checkState() {
            var finished = getCookie("ReportComplete");
            if (!isEmpty(finished)) {
                setCookie('ReportComplete', '', 1);
                clearInterval(ref);
                document.getElementById('<%=LabelClientId%>').innerHTML = '';
            }
        }
    </script>
        
            
  3. The final part, before you write your file to the output stream, append a cookie that will tell the browser the file has finished processing and is included in that stream (note specifically setting the path just like in the JavaScript):
    Dim cookie As New HttpCookie("ReportComplete", Now.ToString)
    cookie.Expires = Now.AddMinutes(30)
    cookie.Path = "/"
    HttpContext.Current.Response.AppendCookie(cookie)
            

That should do it. I would include a link to the getCookie and setCookie functions as I didn’t write them, but they’ve been reproduced so many times over the Internet that I have no clue where the originals came from after some Google and Bing searching.