Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 871
Scuba Kay Avatar asked Dec 05 '22 08:12

Scuba Kay


2 Answers

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!

like image 165
Ricardo Reyna Avatar answered Dec 06 '22 22:12

Ricardo Reyna


You will need to control the progress message client-side.

Using XHR (XMLHttpRequest) file download you can monitor the download and show a progress bar if you want to. Or to use the simpler approach of putting up a straightforward message, switch it on before you make the download request, and switch it off again afterwards.

Here's how: How to get progress from XMLHttpRequest.

Code adapted for ASP.NET MVC:

In your controller method, add a Content-Length header to the Response object:

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

    // Add Content-Length header
    FileInfo i = new FileInfo(fileName);
    Response.AddHeader("Content-Length", i.Length.ToString());

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

Then, wire the onclick event of your submit button to the sendreq() function, below. The updateProgress() function is a handler for the XMLHttpRequest object's onprogress event:

function sendreq(evt) 
{  
    var req = new XMLHttpRequest(); 
    $('#progressbar').progressbar();    
    req.onprogress=updateProgress;
    req.open('GET', 'Controller/DownloadExport', true);  
    req.onreadystatechange = function (aEvt) {  
        if (req.readyState == 4) 
        {  
            }  
    };  
    req.send(); 
}

function updateProgress(evt) 
{
    if (evt.lengthComputable) 
    {  //evt.loaded the bytes browser receive
       //evt.total the total bytes seted by the header
       //
       var percentComplete = (evt.loaded / evt.total)*100;  
       $('#progressbar').progressbar( "option", "value", percentComplete );
    } 
}   

EDIT - use a message instead of progress bar

<div>
    <!-- Your other markup -->
    <div id="progressMsg" style="display:none">Please wait...</div>
    <button onclick="sendreq()">Submit</button>
</div>

<script>
function sendreq(evt) 
{  
    var req = new XMLHttpRequest(); 
    req.open('GET', 'Controller/DownloadExport', true);  
    req.onreadystatechange = function (aEvt) {  
        if (req.readyState == 4) {  
            //4 = complete
            $('#progressMsg').hide();    
        }  
    };  
    $('#progressMsg').show();    
    req.send(); 
}
</script>

Note the third argument to the req.open() states that the call is async. The onreadystate event handler hides the message once the call is complete.

like image 30
Paul Taylor Avatar answered Dec 06 '22 21:12

Paul Taylor