I have a page that allows the user to download a dynamically-generated file. It takes a long time to generate, so I\'d like to show a \"waiting\" indicator. The problem is,
"How to detect when browser receives file download?"
I faced the same problem with that config:
struts 1.2.9
jquery-1.3.2.
jquery-ui-1.7.1.custom
IE 11
java 5
My solution with a cookie:
- Client side:
When submitting your form, call your javascript function to hide your page and load your waiting spinner
function loadWaitingSpinner(){
... hide your page and show your spinner ...
}
Then, call a function that will check every 500ms whether a cookie is coming from server.
function checkCookie(){
var verif = setInterval(isWaitingCookie,500,verif);
}
If the cookie is found, stop checking every 500ms, expire the cookie and call your function to come back to your page and remove the waiting spinner (removeWaitingSpinner()). It is important to expire the cookie if you want to be able to download another file again!
function isWaitingCookie(verif){
var loadState = getCookie("waitingCookie");
if (loadState == "done"){
clearInterval(verif);
document.cookie = "attenteCookie=done; expires=Tue, 31 Dec 1985 21:00:00 UTC;";
removeWaitingSpinner();
}
}
function getCookie(cookieName){
var name = cookieName + "=";
var cookies = document.cookie
var cs = cookies.split(';');
for (var i = 0; i < cs.length; i++){
var c = cs[i];
while(c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0){
return c.substring(name.length, c.length);
}
}
return "";
}
function removeWaitingSpinner(){
... come back to your page and remove your spinner ...
}
- Server side:
At the end of your server process, add a cookie to the response. That cookie will be sent to the client when your file will be ready for download.
Cookie waitCookie = new Cookie("waitingCookie", "done");
response.addCookie(waitCookie);
I hope to help someone!
Create an iframe when button/link is clicked and append this to body.
$('<iframe />')
.attr('src', url)
.attr('id','iframe_download_report')
.hide()
.appendTo('body');
Create an iframe with delay and delete it after download.
var triggerDelay = 100;
var cleaningDelay = 20000;
var that = this;
setTimeout(function() {
var frame = $('<iframe style="width:1px; height:1px;" class="multi-download-frame"></iframe>');
frame.attr('src', url+"?"+ "Content-Disposition: attachment ; filename="+that.model.get('fileName'));
$(ev.target).after(frame);
setTimeout(function() {
frame.remove();
}, cleaningDelay);
}, triggerDelay);
One possible solution uses JavaScript on the client.
The client algorithm:
The server algorithm:
Client source code (JavaScript):
function getCookie( name ) {
var parts = document.cookie.split(name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
function expireCookie( cName ) {
document.cookie =
encodeURIComponent(cName) + "=deleted; expires=" + new Date( 0 ).toUTCString();
}
function setCursor( docStyle, buttonStyle ) {
document.getElementById( "doc" ).style.cursor = docStyle;
document.getElementById( "button-id" ).style.cursor = buttonStyle;
}
function setFormToken() {
var downloadToken = new Date().getTime();
document.getElementById( "downloadToken" ).value = downloadToken;
return downloadToken;
}
var downloadTimer;
var attempts = 30;
// Prevents double-submits by waiting for a cookie from the server.
function blockResubmit() {
var downloadToken = setFormToken();
setCursor( "wait", "wait" );
downloadTimer = window.setInterval( function() {
var token = getCookie( "downloadToken" );
if( (token == downloadToken) || (attempts == 0) ) {
unblockSubmit();
}
attempts--;
}, 1000 );
}
function unblockSubmit() {
setCursor( "auto", "pointer" );
window.clearInterval( downloadTimer );
expireCookie( "downloadToken" );
attempts = 30;
}
Example server code (PHP):
$TOKEN = "downloadToken";
// Sets a cookie so that when the download begins the browser can
// unblock the submit button (thus helping to prevent multiple clicks).
// The false parameter allows the cookie to be exposed to JavaScript.
$this->setCookieToken( $TOKEN, $_GET[ $TOKEN ], false );
$result = $this->sendFile();
Where:
public function setCookieToken(
$cookieName, $cookieValue, $httpOnly = true, $secure = false ) {
// See: http://stackoverflow.com/a/1459794/59087
// See: http://shiflett.org/blog/2006/mar/server-name-versus-http-host
// See: http://stackoverflow.com/a/3290474/59087
setcookie(
$cookieName,
$cookieValue,
2147483647, // expires January 1, 2038
"/", // your path
$_SERVER["HTTP_HOST"], // your domain
$secure, // Use true over HTTPS
$httpOnly // Set true for $AUTH_COOKIE_NAME
);
}
I wrote a simple JavaScript class that implements a technique similar to the one described in bulltorious answer. I hope it can be useful to someone here. The GitHub project is called response-monitor.js
By default it uses spin.js as the waiting indicator but it also provides a set of callbacks for implementation of a custom indicator.
JQuery is supported but not required.
Notable features
Example usage
HTML
<!-- the response monitor implementation -->
<script src="response-monitor.js"></script>
<!-- optional JQuery plug-in -->
<script src="response-monitor.jquery.js"></script>
<a class="my_anchors" href="/report?criteria1=a&criteria2=b#30">Link 1 (Timeout: 30s)</a>
<a class="my_anchors" href="/report?criteria1=b&criteria2=d#10">Link 2 (Timeout: 10s)</a>
<form id="my_form" method="POST">
<input type="text" name="criteria1">
<input type="text" name="criteria2">
<input type="submit" value="Download Report">
</form>
Client (plain JavaScript)
//registering multiple anchors at once
var my_anchors = document.getElementsByClassName('my_anchors');
ResponseMonitor.register(my_anchors); //clicking on the links initiates monitoring
//registering a single form
var my_form = document.getElementById('my_form');
ResponseMonitor.register(my_form); //the submit event will be intercepted and monitored
Client (JQuery)
$('.my_anchors').ResponseMonitor();
$('#my_form').ResponseMonitor({timeout: 20});
Client with callbacks (JQuery)
//when options are defined, the default spin.js integration is bypassed
var options = {
onRequest: function(token){
$('#cookie').html(token);
$('#outcome').html('');
$('#duration').html('');
},
onMonitor: function(countdown){
$('#duration').html(countdown);
},
onResponse: function(status){
$('#outcome').html(status==1?'success':'failure');
},
onTimeout: function(){
$('#outcome').html('timeout');
}
};
//monitor all anchors in the document
$('a').ResponseMonitor(options);
Server (PHP)
$cookiePrefix = 'response-monitor'; //must match the one set on the client options
$tokenValue = $_GET[$cookiePrefix];
$cookieName = $cookiePrefix.'_'.$tokenValue; //ex: response-monitor_1419642741528
//this value is passed to the client through the ResponseMonitor.onResponse callback
$cookieValue = 1; //for ex, "1" can interpret as success and "0" as failure
setcookie(
$cookieName,
$cookieValue,
time()+300, // expire in 5 minutes
"/",
$_SERVER["HTTP_HOST"],
true,
false
);
header('Content-Type: text/plain');
header("Content-Disposition: attachment; filename=\"Response.txt\"");
sleep(5); //simulate whatever delays the response
print_r($_REQUEST); //dump the request in the text file
For more examples check the examples folder on the repository.
In my experience, there are two ways to handle this:
The question is to have a ‘waiting’ indicator while a file is generated and then return to normal once the file is downloading. The way I like todo this is using a hidden iFrame and hook the frame’s onload event to let my page know when download starts. BUT onload does not fire in IE for file downloads (like with the attachment header token). Polling the server works, but I dislike the extra complexity. So here is what I do:
Disclaimer, don’t do this on a busy site, because of the caching could add up. But really, if your sites that busy the long running process will starve you of threads anyways.
Here is what the codebehind looks like, which is all 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'");
}
}