I have a page that allows users to download dynamically generated files. Generation takes a long time, so I want to display a "wait" indication. The problem is, I don't know how to detect when a browser receives a file, so I can hide the indicator.
I'm making a request in a hidden form, which is published to the server and results in a hidden iframe. This way, I won't replace the entire browser window with the results. I listen for the "load" event on iframe, hoping that it will trigger when the download is complete.
I return a content disposition: attachment header with the file, which causes the browser to display the save dialog box. But the browser does not trigger the load event in iframe.
One way I try is to use multipart responses. So it will send an empty HTML file as well as an additional downloadable file. For example:
Content-type: multipart/x-mixed-replace;boundary="abcde" --abcde Content-type: text/html --abcde Content-type: application/vnd.fdf Content-Disposition: attachment; filename=foo.fdf file-content --abcde
This works in Firefox; it receives an empty HTML file, triggers a load event, and then displays the save dialog for the downloadable file. But it fails on IE and Safari; ie triggers the load event, but does not download the file, and Safari downloads the file (with the wrong name and content type), and does not trigger the load event.
A different approach might be to call start file creation, then poll the server until the server is ready, and then download the created file. But I prefer to avoid creating temporary files on the server.
Does anyone have a better idea?
#1 building
If you download a saved file rather than a document, you cannot determine when the download will complete because it is not within the scope of the current document, but a separate process in the browser.
#2 building
When a user triggers file generation, you simply assign a unique ID to the download and send the user to a page that refreshes (or uses AJAX checking) every few seconds. When the file is complete, save it under the same unique ID and
- If the file is ready, download it.
- If the file is not ready, show progress.
Then, you can skip the whole iframe / waiting / browser window clutter, but there is a very elegant solution.
#3 building
If you do not want to generate and store files on the server, would you like to store the status, such as files in progress, files completed? Your wait page may poll the server for when file generation is complete. You may not be sure that the browser has started downloading, but you will be confident.
#4 building
I just have the same problem. My solution is to use temporary files, because I have generated a bunch of temporary files. The forms submitted include:
var microBox = { show : function(content) { $(document.body).append('<div id="microBox_overlay"></div><div id="microBox_window"><div id="microBox_frame"><div id="microBox">' + content + '</div></div></div>'); return $('#microBox_overlay'); }, close : function() { $('#microBox_overlay').remove(); $('#microBox_window').remove(); } }; $.fn.bgForm = function(content, callback) { // Create an iframe as target of form submit var id = 'bgForm' + (new Date().getTime()); var $iframe = $('<iframe id="' + id + '" name="' + id + '" style="display: none;" src="about:blank"></iframe>') .appendTo(document.body); var $form = this; // Submittal to an iframe target prevents page refresh $form.attr('target', id); // The first load event is called when about:blank is loaded $iframe.one('load', function() { // Attach listener to load events that occur after successful form submittal $iframe.load(function() { microBox.close(); if (typeof(callback) == 'function') { var iframe = $iframe[0]; var doc = iframe.contentWindow.document; var data = doc.body.innerHTML; callback(data); } }); }); this.submit(function() { microBox.show(content); }); return this; }; $('#myForm').bgForm('Please wait...');
At the end of the script that generates the file, I have:
header('Refresh: 0;url=fetch.php?token=' . $token); echo '<html></html>';
This will cause the load event on iframe to be triggered. Then turn off the wait message and file download will begin. Tested on IE7 and Firefox.
#5 building
The problem is that there is a "wait" indication when the file is generated, and then it returns to normal after downloading the file. The way I like to do this is to use the hidden iFrame and hook the framework's onload event so that my page knows when the download starts. BUT onload is not triggered in IE for file download (for example, with an attachment header token). Polling servers work, but I don't like the extra complexity. So this is my job:
- Locate the hidden iFrame as usual.
- Generate content. Cache it with an absolute timeout in 2 minutes.
- Sending the javascript redirection to the callback client is actually the second call to the generator page. Note: this will cause the onload event to be triggered in IE because it works like a regular page.
- Remove content from the cache and send it to the client.
Disclaimer, do not perform this operation on busy sites, as caching may increase. However, in fact, if your site is busy for a long time, it will starve your threads anyway.
This is the code behind it, and this is what you really need.
public partial class Download : System.Web.UI.Page { protected System.Web.UI.HtmlControls.HtmlControl Body; protected void Page_Load( object sender, EventArgs e ) { byte[ ] data; string reportKey = Session.SessionID + "_Report"; // Check is this page request to generate the content // or return the content (data query string defined) if ( Request.QueryString[ "data" ] != null ) { // Get the data and remove the cache data = Cache[ reportKey ] as byte[ ]; Cache.Remove( reportKey ); if ( data == null ) // send the user some information Response.Write( "Javascript to tell user there was a problem." ); else { Response.CacheControl = "no-cache"; Response.AppendHeader( "Pragma", "no-cache" ); Response.Buffer = true; Response.AppendHeader( "content-disposition", "attachment; filename=Report.pdf" ); Response.AppendHeader( "content-size", data.Length.ToString( ) ); Response.BinaryWrite( data ); } Response.End(); } else { // Generate the data here. I am loading a file just for an example using ( System.IO.FileStream stream = new System.IO.FileStream( @"C:\1.pdf", System.IO.FileMode.Open ) ) using ( System.IO.BinaryReader reader = new System.IO.BinaryReader( stream ) ) { data = new byte[ reader.BaseStream.Length ]; reader.Read( data, 0, data.Length ); } // Store the content for retrieval Cache.Insert( reportKey, data, null, DateTime.Now.AddMinutes( 5 ), TimeSpan.Zero ); // This is the key bit that tells the frame to reload this page // and start downloading the content. NOTE: Url has a query string // value, so that the content isn't generated again. Body.Attributes.Add("onload", "window.location = 'binary.aspx?data=t'"); } }