In the web application I work on we sometimes need to create PDF documents based on some user input. In most cases, these documents are fairly simple and quick to create, allowing us to create the documents in memory and then send them back to the requesting user with a “Content Disposition: attachment” in the HTTP response. This causes the browser file download mechanism to kick in allowing the user to save or open the resulting PDF. This approach works great and I think has come to be the expected behavior for getting files in 3rd party formats (Word, Excel, Adobe PDF, etc.) from a web application.
Generating a file, however, can sometimes take a few seconds. This short duration is not necessarily long enough to justify offloading the file generation to a separate process (e.g. a service bus, a separate worker thread within your web server process, etc.) but is long enough that the user waiting for the download may think that nothing is happening and attempt to click the “create file” button again. We definitely do not want the user submitting a request to create the same file over and over again, so we might use something like the jQuery Block UI plug-in to display a “please wait” message and prevent further requests from being made from that browser. This works great, but you eventually need to un-block the UI so that your users can continue doing their work after downloading the file. Unfortunately, I don’t know of any way javascript tricks to detect when the file download dialog is displayed to the user. Without being able to hook in to that event, the UI will remain blocked forever, forcing the user to either close and re-open their browser or hit the back button to get away from the “Please Wait” message. You could use some kind of time out to automatically un-block the page, but in most cases you’d likely end up un-blocking too early or too late.
There are some ways to work around this by writing the file to disk or some caching mechanism and then providing a separate URL endpoint to download the finished file, but these approaches require what is, in my opinion, a non-trivial amount of server side code to accomplish. All I really want is a way to let the user know that we’re building their file and to keep them from hitting ‘submit’ multiple times. I recently came across a post at a site called eFreedom (which I had never heard of before) where an interesting idea was presented for solving this problem. While we can’t use javascript to determine when the browser receives a response with the “content-disposition: attachment” header, we can use javascript to determine when the browser receives a cookie with a certain name and value. We can add a very minimal amount of code to our server side implementation to add a cookie to the file download response and then use javascript to equate the presence of that cookie with the fact that the file is available for download. Let’s take a look at some code:
The Client Side Setup
The code I’m showing here is adapted from an ASP .NET Web Forms application, but I’ll try to keep it as a generic looking as possible as this approach should work regardless of the platform you use. First, let’s see a simple HTML form that we might use to capture the needed input from the user to create our PDF file:
<form id="create_pdf_form">
<fieldset>
<legend>Create Customer Info Sheet PDF</legend>
<div>
<label id="first_name_prompt_id" for="first_name_input_id">First Name</label>
<input name="first_name_input" id="first_name_input_id"/>
</div>
<div>
<label id="last_name_prompt_id" for="last_name_input_id">Last Name</label>
<input name="last_name_input" id="last_name_input_id"/>
</div>
<input type="hidden" id="download_token_value_id"/>
<input type="submit" value="Create File"/>
</fieldset>
</form>
Notice the ‘hidden’ input field I included in that form. We’ll use that field to provide a token value to be included in a cookie in the file download response. Now let’s look at the jQuery code that we’ll use when this form is initially submitted. Note that this requires jQuery 1.4.2, the jQuery Block UI plug-in, and the jQuery cookies plug-in.
$(document).ready(function () {
$('#create_pdf_form').submit(function () {
blockUIForDownload();
});
});
var fileDownloadCheckTimer;
function blockUIForDownload() {
var token = new Date().getTime(); //use the current timestamp as the token value
$('#download_token_value_id').val(token);
$.blockUI();
fileDownloadCheckTimer = window.setInterval(function () {
var cookieValue = $.cookie('fileDownloadToken');
if (cookieValue == token)
finishDownload();
}, 1000);
}
First, we’re using jQuery to hook into the ‘submit’ event of the HTML form. This will get fired just prior to the form data being submitted to the server. When this happens we’re generating a token value using the current timestamp. This token can really be any arbitrary string value, but we do want to try and make it unique per request coming from the same browser, and using the timestamp is a pretty easy way to do this. We take that token and add it to the form within that hidden field that we created. This will ensure that the token value gets submitted up to the server (more on this later).
Next, we’re using the jQuery Block UI plug-in to block the user from submitting the form multiple times. The behavior of the Block UI plug-in is very configurable and I encourage you to go check out the documentation for details on all of the different ways you can customize what message is displayed to the user while the UI is blocked.
With the UI effectively blocked, we use the ‘window.setInterval’ function to create an interval timer. The first argument provided is the function that you want to have executed at each interval and the second argument is how long you want the interval to be in milliseconds. For our purposes we want some code to check for the presence of a cookie value every 1 second. We’re using the jQuery cookies plug-in to examine the value of a cookie named ‘fileDownloadToken’. When the value of that cookie matches the token value that we generated before (based on the current timestamp), we’re going to invoke a method called ‘finishDownload’. We’ll look at the finishDownload method in a bit. First, let’s see what we need to do on the server side.
The Server Side Setup
Again, I’ve adapted this example from an ASP .NET Web Forms application, so the code below is C#, but this approach should work for just about any web platform. I’ve taken out the particulars around how the file to be downloaded is actually created and written to the HTTP response, as that will vary from application to application.
var response = HttpContext.Current.Response;
response.Clear();
response.AppendCookie(new HttpCookie("fileDownloadToken", downloadTokenValue); //downloadTokenValue will have been provided in the form submit via the hidden input field
response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", desiredFileName)); //desiredFileName will be whatever the resutling file name should be when downloaded
//Code to generate file and write file contents to response
response.Flush();
What I like best about this approach is that you were going to have to write 99% of the above code one way or another. The only addition that had to be made to the server side functionality was the single line where we add the file download token cookie to the response. This is the cookie value that our javascript timer is polling for back on the browser while all of the server side code is building the file to be downloaded. Once the response is flushed back to the client, our timer will see that the cookie is present and invoke the ‘finishDownload’ method. Let’s take a look at that.
Finishing Up On The Client Side
Once the expected cookie value appears, all that’s left to do is a little bit of clean up in the ‘finishDownload’ function:
function finishDownload() {
window.clearInterval(fileDownloadCheckTimer);
$.cookie('fileDownloadToken', null); //clears this cookie value
$.unblockUI();
}
All we’re doing here is clearing out the previously set interval timer, removing the cookie, and unblocking the UI so that the user can continue doing their work after they decide what to do with the downloaded file. All in all, we added a line or two to the server side code and a few lines of javascript to accomplish our goal of informing the user that we’re working on their file.