4

On my web page when a user clicks the download button the Exports Action is called from the controller, an Excel file is generated and then returned to the user. Depending on the parameters passed to the action it sometimes takes some time for the file to be generated and returned to the user.

So, I want to show a busy indicator while waiting for the file. I went with Bootstrap spinner.

$('.content').on('click', 'a.export', function (e) {
    e.preventDefault();

    // show busy indicator
    $('#divSpinner').css('display', 'inline-block');

    var idSelect = '';
    $('#ddlSelect option:selected').each(function () {
        idSelect += $('#ddlSelect')[0].value;
    });

    var idSelect2 = '';
    $('#ddlSelect2 option:selected').each(function () {
        idSelect2 += $('#ddlSelect2')[0].value;
    });

    var url = 'Home/Exports?' + 'param1=' + idSelect + '&param2=' + idSelect2;

    //download file
    window.location = url;

    // hide busy indicator
    $('#divSpinner').css('display', 'none');
});

Well this kind of works, but the problem is that $('#divSpinner').css('display', 'none'); gets hit immediately and the spinner hides before the file is actually available to the user for download (browser download/open window).

How can I rewrite the code to wait for window.location=url to finish and then hide the spinner?

EDIT: I also tried wrapping the Download part in an async function like this:

async function Download(url) {

    let promise = new Promise((resolve, reject) => {
        resolve(window.location = url);
    });

        let result = await promise; // wait until the promise resolves (*)
}

and calling it like this:

Download(url)
    .then(function() {
        $('#divSpinner').hide()
});

Download works, but still $('#divSpinner').hide() get's hit before the file download is presented to the user. Isn't the whole point of async/await to wait for the result before continuing code execution??

EDIT2 (with "blob"):

async function Download(url) {
    let promise = new Promise((resolve, reject) => {
        resolve(filedownload(url));
    });
    let result = await promise; 
}

function filedownload(urlToSend) {
    var req = new XMLHttpRequest();
    req.open("GET", urlToSend, true);
    req.responseType = "blob";
    req.onload = function (event) {
        var blob = req.response;
        var fileName = req.getResponseHeader("fileName") 
        var link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = fileName;
        link.click();
    };
    req.send();
}

and calling it like this:

Download(url)
    .then(function() {
        $('#divSpinner').hide()
});

EDIT 3: I used @Nenad's solution from below, but I only changed the handling of the response header (otherwise I was getting NULL for filename at download - I used this solution https://stackoverflow.com/a/40940790/7975627):

function filedownload(urlToSend, resolve) {
    var req = new XMLHttpRequest();
    req.open("GET", urlToSend, true);
    req.responseType = "blob";
    req.onload = function (event) {
        var blob = req.response;
        var filename = "";
        var disposition = req.getResponseHeader('Content-Disposition');
        if (disposition && disposition.indexOf('attachment') !== -1) {
            var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;    
            var matches = filenameRegex.exec(disposition);
            if (matches != null && matches[1]) {
                filename = matches[1].replace(/['"]/g, '');
            }
        }
        var link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = filename;
        link.click();
        resolve(filename); // call resolve after download has finished.
    };
    req.send();
}
TheMixy
  • 1,049
  • 15
  • 36
  • Is this function calling from the same page? from /Home/Exports ? – cbalakus Feb 27 '20 at 10:19
  • By the way, your won't work. Because you are changing the page before it. So I guess you need to make your indicator visible at startup. When excel data came up, you will hide the indicator. – cbalakus Feb 27 '20 at 10:28
  • Yes, the View and Controller are both "Home" (Action Exports is also in Home controller). Also javascript/jquery is part of the "Index.cshtml" of the Home view – TheMixy Feb 27 '20 at 12:29
  • @cbalakus: . The problem is that it get hidden to quickly (practically instantly, because the code doesnt stop at 'window.location' but continues to the end and after the file is ready the user is prompted for save/oen) – TheMixy Feb 27 '20 at 12:31

2 Answers2

1

TL;DR

You are delegating control to the browser, so there is no JavaScript notification on your page of a finished process.

In detail:

The method you use to download file (window.location = url;) tells to the browser to load a new page instead of the current one. You are delegating control of loading of a new page to the browser, same like if url would point to another HTML page.

Only after browser figures that content is not HTML, but something else, based on headers, it downloads the file, instead of throwing current page and displaying new one.

Headers that direct browser to download the content are:

  • content-disposition: attachment
  • content-type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

Different approach would be to use AJAX request to download responseType=blob. Good example is here. Drawbacks of this method are:

  • It's not supported in older browsers.
  • Full file is loaded JavaScript memory, before you save it.

But you have control of what is happening after the blob download has completed.

UPDATE: Looking at your 2nd edit, you are calling resolve to early. Also, you should return your Promise from download method.

async function Download(url) {
    return new Promise((resolve, reject) => {
        filedownload(url, resolve); // pass resolve
    }); 
}

function filedownload(urlToSend, resolve) {
    var req = new XMLHttpRequest();
    req.open("GET", urlToSend, true);
    req.responseType = "blob";
    req.onload = function (event) {
        var blob = req.response;
        var fileName = req.getResponseHeader("fileName") 
        var link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = fileName;
        link.click();
        resolve(fileName); // call resolve after download has finished.
    };
    req.send();
}
Nenad
  • 24,809
  • 11
  • 75
  • 93
  • I tried using the ajax function from your link and putting it in my "resolve(...)", but still there are 2 problems: 1) `$('#divSpinner').css('display', 'none');` still gets hit immediately and 2) my download filename is NULL (from the controller I'm returning file like this: `return File(file.Contents, file.ContentType, file.Title);`) – TheMixy Mar 14 '20 at 19:18
  • ASP.NET MVC part has nothing to do with behavior in browser, unless you are doing something completely wrong. This is JavaScript issue. Post your solution with Blob as new edit to the question. – Nenad Mar 14 '20 at 19:42
  • Hello. Any other ideas? – TheMixy Apr 22 '20 at 06:17
  • You are calling `resolve` way to early. It should be called after `link.click` in the `filedownload` function. Use `filedownload(urlToSend, resolve)` and call `resolve` after `link.click`. – Nenad Apr 22 '20 at 07:08
  • 1
    Thank you! Solved with a minor modification for avoiding NULL filename (see my edit3 above) – TheMixy Apr 27 '20 at 16:34
0
var url = 'Home/Exports?' + 'param1=' + idSelect + '&param2=' + idSelect2;

$(document).load(url, function () {
    // hide busy indicator
    $('#divSpinner').css('display', 'none');
});

I'm not sure but, can you try this?

cbalakus
  • 620
  • 7
  • 15
  • this way file download doesnt work... spinner shows for a few seconds and then disappears, but no file download is offered to the user – TheMixy Feb 27 '20 at 16:59