Introduction
In a web application you often want to download a file that is created on the fly on the server. If this takes more than a few seconds you want to show some kind of feedback to the user, like a "Please wait..." message and a nice loading animation:
It is easy to show such an animation using JavaScript when a download link or button is clicked and the request is sent back to the server. However when the server answers directly with the file to download, there is no way to hook into that event and hide the animation.
I will show you a nice trick to hide the animation as soon as the download is ready and the browser shows the download dialog.
Background
The trick I use makes use of a cookie: Just before the server starts writing the generated file into the response stream, it sets a cookie. The cookie is sent to the browser as part of the HTTP header of the response and is immediately available as soon as the first bytes of the response are received by the browser.
Cookies can also be read from JavaScript. So what we do is starting a timer that polls for the existence of this cookie when the download link/button is pressed. This timer continues to work while the request is running. As soon as we have found our cookie, we know that the download is ready and can hide the animation.
Using the code
Let's take a look at the client code first. This is the MVC view:
@using (Html.BeginForm("Download", "Home"))
{
@Html.Hidden("cookieValue")
<input type="submit" id="btnDownload" value="Download!" />
}
We have a form that will issue an HTTP POST and call an action "Download" in a controller named "Home". Within the form we have a hidden field and a button that starts our download.
<script>
var _tmr;
$(function () {
$('#btnDownload').click(function () {
$('#cookieValue').val(Date.now().toString());
$('body').addClass("loading");
_tmr = window.setInterval(function () {
var _str = 'dlc=' + $('#cookieValue').val();
if (document.cookie.indexOf(_str) !== -1) {
$('body').removeClass("loading");
}
}, 100);
});
});
</script>
Using jQuery, we attach a click event to the download button. If this button is clicked, we generate a unique key from the current time and store this key into our hidden field.
Then we show the loading animation by adding the CSS class "loading" to the HTML body (this is out of scope of this article as there are 100 ways to show a loading animation).
Next we start a timer which fires an event every 100ms. In that event we read all cookies of the current URL. "document.cookie" returns a string with all cookie names and values, separated with a semicolon character. We check if this string contains a cookie with the name "dlc" and a values that matches the unique key that we stored in our hidden field before.
If we have found such a cookie, the loading animation is hidden again.
Now let's look at the server side code. This is the Download
method of our HomeController
class:
[HttpPost]
public FileResult Download(string cookieValue)
{
System.Threading.Thread.Sleep(10000);
StringBuilder sb = new StringBuilder();
sb.AppendLine("This is a demo file that has been generated on the server.");
sb.AppendLine();
for (int i = 1; i <= 1000; i++)
{
sb.AppendLine("Line " + i.ToString() + " " + DateTime.Now.ToString());
}
ControllerContext.HttpContext.Response.Cookies.Add(new HttpCookie("dlc", cookieValue));
return File(Encoding.ASCII.GetBytes(sb.ToString()), "text/plain", "data.txt");
}
The value of the hidden field is automatically passed in the cookieValue
parameter of the method.
To simulate a time consuming file generation I use a Thread.Sleep
to add a delay of 10 seconds. Also I generate a dummy text-file that contains 1'000 lines of text.
The most important part is adding a cookie to the response. I used "dlc" as the name of the cookie, but any name will be ok as long as you use the same name in the JavaScript code. The cookie value will be set to the cookieValue
parameter.
After setting the cookie, the dummy file is returned as a FileResult
.
Points of Interest
It is important to use another unique value for the cookie each time we start a download. Otherwise it would not work if someone starts the same download again because the cookie is already existing!
Although my example is using ASP.NET MVC, the principle can easily be transferred to any other programming language.
History
- 2016-04-23 - first release