Search code examples
asp.net-mvc-4downloadprogress

How to show 'in progress' while a FileResult function is running in MVC4


I have a download button that calls this function:

public FileResult DownloadExport()
{
    string fileName = "example";

    // Activate 'In progress'
    // Call to a function that takes a while
    // Deactivate 'In progress'

    return File(fileName, System.Net.Mime.MediaTypeNames.Application.Octet, Path.GetFileName(fileName));
}

So, I call a function that generates a file for me. This function takes a while and I don't want my users to think the application crashed. That's why I want to show 'In progress' while the user is waiting. How can I implement this?

For clarification: This question is not about the progress of the download, but about the progress of the function generating the file.


Solution

  • I recently had this exact same problem and I believe my solution (based on this answer to a related problem) is what you were looking for with this question. The basic idea is to get a progress spinner (in my case) to show while the file is being generated, hiding the progress spinner when the file is finished generating, and then providing the file to download. To achieve this I needed four things:

    First, the action on the controller needs to store the file in the Session

    [HttpPost]
    public ActionResult RunReport(ReportViewModel viewmodel)
    {
       // Process that generates the object while will become the file here
       // ...
    
       using (var stream = new MemoryStream())
       {
          // Convert the object to a memory stream
          report.Generate(stream);  // Use your object here
    
          string handle = Guid.NewGuid().ToString();
          Session[handle] = stream.ToArray();
          return new JsonResult()
          {
             Data = new
             {
                FileGuid = handle,
                MimeType = "application/pptx",
                FileName = "My File.pptx"
             }
          };
       }
    }
    

    The controller also needs a new action which will provide the actual download file

    public ActionResult Download(string fileGuid, string mimeType, string filename)
    {
       if(Session[fileGuid] != null)
       {
           byte[] data = Session[fileGuid] as byte[];
           Session.Remove(fileGuid);  // Cleanup session data
           return File(data, mimeType, filename);
       }
       else
       {
          // Log the error if you want
          return new EmptyResult();
       }
    }
    

    Next, an AJAX call from the view which shows the progress spinner, calls RunReport (the action that takes a long time), uses the JSON array it returns to return the download file (which is a quick action), and then hides the spinner again.

    <script type="text/javascript">
       function RunReport(reportUrl) {
          $.ajax({
             cache: false,
             url: reportUrl,
             type: "POST",
             success: function(response) {
                window.location = "/Report/Download?fileGuid=" + response.FileGuid +
                   "&mimeType=" + response.MimeType + "&filename=" + response.FileName;
                $("#progress-spinner").hide();
             }
          });
    
          $("#progress-spinner").show();
       }
    </script>
    

    Finally, the link that launches it all and generates the action link for use in the AJAX call

    <a href="javascript: RunReport('@Url.Action("RunReport", "UserReport", new { ReportId = Model.Id })')">Run Report</a>
    

    I hope this helps someone!