0

This may be a duplicate, but I haven't found a solution yet.

In an app I'm working on, there are two export buttons that should download an Excel file to the user's computer.

One export button performs a POST form method, routing parameters to an action:

<input type="submit" value="EXPORT" id="exportButton" asp-action="ExportResultsData" 
asp-route-ClientID="@listItem.ClientID" asp-route-ProcessingType="@listItem.ProcessingType" 
asp-route-ProgressType="@Model.ProgressType" asp-route-FileName="@listItem.FileName" />

When clicked, the values are routed to the controller action and they are used to create and return the file to the browser where it is downloaded. All good.

As I've said, I have another button that needs to download another Excel file. However, this one is different because the view model does not contain all the values needed to route to its controller action. Each time I click the button, I need to call a JavaScript function and get a JSON object which has this structure:

{
  "draw": 1,
  "columns": [
    {
      "data": "Column1",
      "name": "Column1",
      "searchable": true,
      "orderable": true,
      "search": {
        "value": "",
        "regex": false
      }
    },
    {
      "data": "Column2",
      "name": "Column2",
      "searchable": true,
      "orderable": true,
      "search": {
        "value": "",
        "regex": false
      }
    },
    {
      "data": "Column3",
      "name": "Column3",
      "searchable": true,
      "orderable": true,
      "search": {
        "value": "",
        "regex": false
      }
    },
    {
      "data": "Column4",
      "name": "Column4",
      "searchable": true,
      "orderable": true,
      "search": {
        "value": "",
        "regex": false
      }
    }
  ],
  "order": [
    {
      "column": 0,
      "dir": "asc"
    }
  ],
  "start": 0,
  "length": 15,
  "search": {
    "value": "",
    "regex": false
  },
  "ClientID": 8,
  "FeedbackID": "8",
  "ProcessingType": "Import"
}

Each time I get these parameters, some of the values may change, and the number of columns may increase/decrease.

I can reach the controller action, bind the parameters to the model, and generate an Excel file successfully but the file does not download and just returns back to the AJAX success section:

function ExportFeedbackData(clientID, feedbackID, processingType) {
    var jqueryParams = GetJqueryParams();
    $.ajax({
        url: '/Client/ExportFeedbackData',
        type: 'POST',
        data: {
            DatatablesParameters: jqueryParams,
            ClientID: clientID,
            FeedbackID: feedbackID,
            ProcessingType: processingType,
        },
        success: function (exportedFile) {
            console.log(exportedFile);
            //file data just returns here and doesn't download
        },
        error: function (errormsg) {
            console.log(errormsg);
        }
    });
}

Controller action:

//"DatatablesParameters" refers to parameters sent from a JS plugin called Datatables https://datatables.net/
//https://datatables.net/manual/server-side
public IActionResult ExporFeedbackData(DataTablesParameters DatatablesParameters, int ClientID, int FeedbackID, string ProcessingType)
{
    DataTable dt = _processingService.ReturnDataTable(DatatablesParameters, ClientID, FeedbackID, ProcessingType);

    using (ExcelPackage package = new ExcelPackage())
    {
        ExcelWorksheet worksheet = package.Workbook.Worksheets.Add(ProcessingType);
        worksheet.Cells["A1"].LoadFromDataTable(dt, PrintHeaders: true);
        for (var i = 0; i < dt.Columns.Count; i++)
        {
            worksheet.Cells.AutoFitColumns();
        }
        return File(package.GetAsByteArray(), XlsxContentType, $"{ProcessingType}.xlsx"); //Excel file returned
    }
}

So, what I want is to be able to download the file with AJAX, but I can't figure out how to do it. The file can potentially be large as well. I'm also tempted to somehow eliminate AJAX and dynamically generate an <input> tag, but each time I'd export the values in the jquery object may be different. I'm also open to other solutions.

I'm mainly worried about cleanliness and reliability. With form submission you just click the button and it's downloaded. But with AJAX it looks to be messier.

Lukas
  • 1,699
  • 1
  • 16
  • 49
  • This may work, I will try. The post says "This removes any need for any physical files to created and stored on the server", but I looked up TempData and its contents stored on the server. Do you know anything about this? – Lukas Apr 15 '20 at 16:12
  • 1
    TempData is managed by the framework. As a result it would also be removed by the frameork. The post talks about your code - it does not need to create a physical file and then sending it for download. The file is created in memory and then downloaded. – Rahatur Apr 16 '20 at 10:00
  • I got it working with the solution that was linked. Thank you! – Lukas Apr 16 '20 at 13:48

1 Answers1

0

An AJAX request is a thin-client request. Nothing happens automatically with the response; it's on you, the programmer, to do whatever is required with the response. Since you want to prompt a file download, you will need to dynamically create an anchor link with the file data as an object URL in the href, and then "click" it.

let a = document.createElement('a');
let url = window.URL.createObjectURL(exportedFile);
a.href = url;
a.download = 'myfile.xlsx';
document.body.append(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • Thank you for the response. At the moment I can't get it to work because it looks like `createObjectURL()` method does not work anymore because browsers are disabling it. – Lukas Apr 15 '20 at 16:18