1

I am working on a single-page application using ASP.Net Core MVC and JQuery (doing a full-page refresh resets some long-running processes, so I must use AJAX for all navigation to avoid this.)

I want to allow the user to download a PDF, which can be handled simply enough using my PdfController:

public async Task<IActionResult> GetFile(int FileID)
{
    byte[] fileBytes = await _myService.GetFile(FileID);
    return File(fileBytes, "DesiredMimeType", "DesiredFileName");
}

In this simple case, I can use something like <a href="/Pdf/GetFile?FileID=123">Get File 123</a> and the file downloads without screwing anything up. The problem is that sometimes my service throws an exception if the parameters are incorrect. I would like to be able to display a custom error page (using AJAX), so now my controller action becomes:

public async Task<IActionResult> Index(int FileID)
{
    try
    {
        byte[] fileBytes = await _myService.GetFile(FileID);
        return File(fileBytes, "DesiredMimeType", "DesiredFileName");
    }
    catch (MyCustomException ex)
    {
        return PartialView("MyErrorPage", new MyErrorViewModel(ex));
    }
}

This works fine on the server side, but I don't know how to handle it on the client side. The JavaScript would need to look something like this:

function getFile(fileID) {
    $.get('/Pdf/GetFile?FileID=' + fileID, function (data) {
        // pseudo code - not sure what to do here
        if (isFile(data)) {
            // then prompt a download and leave the DOM alone
        } else {
            // display error page
            $('#ajax-container').html(data);
        }
    }).fail(function (jqXHR, textStatus, errorThrown) {
        notify('Web service responded with ' + textStatus);
    });
}

So my question is: how can I use JQuery or JavaScript to detect whether data is a file or a partial view, and how can I trigger a file download if it is a file? If there is a better approach, I am open to recommendations also.

Dave Smash
  • 2,941
  • 1
  • 18
  • 38

2 Answers2

2

You can try returning an additional flag along with your response.

    public async Task<IActionResult> Index(int FileID)
    {
        try
        {
            string filePath = await _myService.GetFilePath(FileID);
            return  Json (new { status = "valid" , file = Url.Action("Download", new {FilePath = filepath})});
        }
        catch (MyCustomException ex)
        {
            return Json(new { status = "Invalid", pv = PartialView("MyErrorPage", new MyErrorViewModel(ex)) });
        }
    }
    
    public async Task<IActionResult> Download(string FilePath )
    {
       string tempFilePath = System.IO.Path.Combine(_hostingEnvironment.WebRootPath, "FILE-DIR", FilePath);
       byte[] fileBytes = await System.IO.File.ReadAllBytesAsync(tempFilePath);
      
     //Clean up the temp file 

       return file = File(fileBytes, "MIME-TYPE", "FILE-NAME");
    }

Then your js code would be:

        // pseudo code - not sure what to do here
        if (data.status == "valid")) {
            // then prompt a download and leave the DOM alone
            window.location.replace(data.file);
        } else {
            // display error page
            $('#ajax-container').html(data.pv);
        }
Captain Red
  • 1,171
  • 2
  • 16
  • 27
  • 1
    I like this approach! I am still struggling with how to prompt the browser to download the file when the status is valid. I'm finding a little bit more info on that part of the solution on Google, though, so I am working on it... If you have any insight, though, that would be helpful. Thanks. – Dave Smash Dec 17 '18 at 18:54
  • Probably this? https://stackoverflow.com/questions/45727856/how-to-download-a-file-in-asp-net-core – Captain Red Dec 17 '18 at 19:20
  • 1
    Thanks, this got me close enough! I was trying to create a byte[] in memory and avoid saving a temp file, but using a temp file seems to be the best approach. If I do that, I don't need to jump through hoops about how to get JQuery to trick the browser into downloading a file - I can simply return json containing the file path of the temp file and treat it like a redirect (`window.location.replace(data.redirectUrl)`). If `data.redirectUrl` doesn't exist, then I can assume it's a PartialView and replace the page contents using AJAX. – Dave Smash Dec 17 '18 at 20:33
1

Credit goes to Captain Red for getting me to the answer. Here is a full solution for those having the same problem. In PdfController.cs:

public async Task<IActionResult> Generate(string Foo, int Bar)
{
    try
    {
        // TODO: Include a randomized directory in the path so that a malicious user is less likely to guess a file path
        // TODO: Set appropriate permissions on the root pdf folder for IIS App Pool User (if using IIS)
        string relativePath = await _myService.CreateTempFile(Foo, Bar);
        return Json(new { redirect = Url.Action("Download", new { FilePath = relativePath }) });
    }
    catch (MyCustomException ex)
    {
        return PartialView("MyErrorPage", new MyErrorViewModel(ex, Foo, Bar));
    }
}

public async Task<IActionResult> Download(string FilePath)
{
    string tempFilePath = System.IO.Path.Combine(_hostingEnvironment.WebRootPath, "myPdfDir", FilePath);
    byte[] fileBytes = await System.IO.File.ReadAllBytesAsync(tempFilePath);

    // clean up the temp file
    System.IO.File.Delete(tempFilePath);
    Directory.Delete(Path.GetDirectoryName(tempFilePath));

    return File(fileBytes, "MyMimeType", "MyFileName");
}

In JavaSript:

function getPDF(foo, bar) {
    $.get('/Pdf/Generate?Foo=' + foo + '&Bar=' + bar, function (data) {
        if (data.redirect) {
            // we got a url for the pdf file
            window.location.replace(data.redirect); // downloads without affecting DOM
        } else {
            // we got a PartialView with the error page markup
            $('#ajax-container').html(data);
        }
    }).fail(function (jqXHR, textStatus, errorThrown) {
        // error handling
    });
}
Dave Smash
  • 2,941
  • 1
  • 18
  • 38