it-roy-ru.com

Скачать файл Excel через AJAX MVC

У меня есть большая (ish) форма в MVC.

Мне нужно иметь возможность создать файл Excel, содержащий данные из подмножества этой формы. 

Сложность в том, что это не должно влиять на остальную часть формы, и поэтому я хочу сделать это через AJAX. Я наткнулся на несколько вопросов по SO, которые, похоже, связаны между собой, но я не совсем понимаю, что означают ответы.

Этот, кажется, ближе всего к тому, что мне нужно: asp-net-mvc-download-Excel - но я не уверен, что понимаю ответ, и ему уже пару лет. Я также наткнулся на другую статью (больше не могу ее найти) об использовании iframe для обработки загрузки файла, но я не уверен, как заставить это работать с MVC.

Мой файл Excel возвращается нормально, если я делаю полный пост обратно, но я не могу заставить его работать с AJAX в mvc.

78
Valuk

Вы не можете напрямую вернуть файл для загрузки с помощью вызова AJAX, поэтому альтернативный подход заключается в использовании вызова AJAX для отправки связанных данных на ваш сервер. Затем вы можете использовать серверный код для создания файла Excel (я бы порекомендовал использовать для этого EPPlus или NPOI, хотя звучит так, как будто эта часть работает). 

ОБНОВЛЕНИЕ сентябрь 2016

Моему исходному ответу (ниже) было более 3 лет, поэтому я подумал, что я буду обновляться, поскольку я больше не создаю файлы на сервере при загрузке файлов через AJAX, однако я оставил исходный ответ, поскольку он может иметь некоторые использовать еще в зависимости от ваших конкретных требований. 

Распространенным сценарием в моих приложениях MVC является создание отчетов через веб-страницу, на которой настроены некоторые параметры отчета (диапазоны дат, фильтры и т.д.). Когда пользователь указал параметры, которые он отправляет на сервер, создается отчет (например, файл Excel в качестве вывода), а затем я сохраняю полученный файл в виде байтового массива в корзине TempData с уникальной ссылкой. Эта ссылка передается обратно как результат Json для моей функции AJAX, которая впоследствии перенаправляет на отдельное действие контроллера для извлечения данных из TempData и загрузки в браузер конечных пользователей.

Чтобы дать это более подробно, предполагая, что у вас есть MVC View, который имеет форму, привязанную к классу Model, давайте вызовем Model ReportVM.

Во-первых, для получения опубликованной модели требуется действие контроллера, например:

public ActionResult PostReportPartial(ReportVM model){

   // Validate the Model is correct and contains valid data
   // Generate your report output based on the model parameters
   // This can be an Excel, PDF, Word file - whatever you need.

   // As an example lets assume we've generated an EPPlus ExcelPackage

   ExcelPackage workbook = new ExcelPackage();
   // Do something to populate your workbook

   // Generate a new unique identifier against which the file can be stored
   string handle = Guid.NewGuid().ToString();

   using(MemoryStream memoryStream = new MemoryStream()){
        workbook.SaveAs(memoryStream);
        memoryStream.Position = 0;
        TempData[handle] = memoryStream.ToArray();
   }      

   // Note we are returning a filename as well as the handle
   return new JsonResult() { 
         Data = new { FileGuid = handle, FileName = "TestReportOutput.xlsx" }
   };

}

Вызов AJAX, который отправляет мою форму MVC вышеприведенному контроллеру и получает ответ, выглядит следующим образом:

$ajax({
    cache: false,
    url: '/Report/PostReportPartial',
    data: _form.serialize(), 
    success: function (data){
         var response = JSON.parse(data);
         window.location = '/Report/Download?fileGuid=' + response.FileGuid 
                           + '&filename=' + response.FileName;
    }
})

Действие контроллера для обработки загрузки файла:

[HttpGet]
public virtual ActionResult Download(string fileGuid, string fileName)
{   
   if(TempData[fileGuid] != null){
        byte[] data = TempData[fileGuid] as byte[];
        return File(data, "application/vnd.ms-Excel", fileName);
   }   
   else{
        // Problem - Log the error, generate a blank file,
        //           redirect to another controller action - whatever fits with your application
        return new EmptyResult();
   }
}

Еще одно изменение, которое можно легко приспособить, если это необходимо, - передать MIME-тип файла в качестве третьего параметра, чтобы одно действие контроллера могло корректно обслуживать различные форматы выходных файлов.

Это устраняет необходимость в создании и хранении каких-либо физических файлов на сервере, поэтому не требуется никаких процедур обслуживания, и это опять-таки беспрепятственно для конечного пользователя.

Обратите внимание, что преимущество использования TempData вместо Session состоит в том, что после считывания TempData данные очищаются, поэтому это будет более эффективным с точки зрения использования памяти, если у вас большой объем запросов к файлам. Смотрите Лучшие практики TempData .

ОРИГИНАЛЬНЫЙ Ответ

Вы не можете напрямую вернуть файл для загрузки с помощью вызова AJAX, поэтому альтернативный подход заключается в использовании вызова AJAX для отправки связанных данных на ваш сервер. Затем вы можете использовать серверный код для создания файла Excel (я бы порекомендовал использовать для этого EPPlus или NPOI, хотя звучит так, как будто эта часть работает).

Как только файл будет создан на сервере, верните путь к файлу (или просто имя файла) в качестве значения, возвращаемого вашим вызовом AJAX, а затем установите JavaScript window.location на этот URL, который предложит браузеру загрузить файл.

С точки зрения конечных пользователей, операция загрузки файла является бесшовной, поскольку они никогда не покидают страницу, на которой происходит запрос.

Ниже приведен простой надуманный пример вызова ajax для достижения этой цели:

$.ajax({
    type: 'POST',
    url: '/Reports/ExportMyData', 
    data: '{ "dataprop1": "test", "dataprop2" : "test2" }',
    contentType: 'application/json; charset=utf-8',
    dataType: 'json',
    success: function (returnValue) {
        window.location = '/Reports/Download?file=' + returnValue;
    }
});
  • Параметр url - это метод Controller/Action, в котором ваш код создает файл Excel.
  • Параметр data содержит данные json, которые будут извлечены из формы.
  • returnValue будет именем вашего недавно созданного файла Excel.
  • Команда window.location перенаправляет на метод Controller/Action, который фактически возвращает ваш файл для загрузки.

Пример метода контроллера для действия Download:

[HttpGet]
public virtual ActionResult Download(string file)
{   
  string fullPath = Path.Combine(Server.MapPath("~/MyFiles"), file);
  return File(fullPath, "application/vnd.ms-Excel", file);
}
171
CSL

Мои 2 цента - вам не нужно хранить Excel как физический файл на сервере - вместо этого сохраните его в (Session) Cache. Используйте уникально сгенерированное имя для вашей переменной Cache (в которой хранится этот файл Excel) - это будет возвращением вашего (начального) вызова ajax. Таким образом, вам не придется сталкиваться с проблемами доступа к файлам, управлять (удалять) файлы, когда они не нужны, и т.д. И, имея файл в кэше, быстрее его извлекать.

19
Luchian

Недавно я смог сделать это в MVC (хотя не было необходимости использовать AJAX) без создания физического файла и решил поделиться своим кодом:

Супер простая функция JavaScript (нажатие кнопки datatables.net вызывает это):

function getWinnersExcel(drawingId) {
    window.location = "/drawing/drawingwinnersexcel?drawingid=" + drawingId;
}

Код контроллера C #:

    public FileResult DrawingWinnersExcel(int drawingId)
    {
        MemoryStream stream = new MemoryStream(); // cleaned up automatically by MVC
        List<DrawingWinner> winnerList = DrawingDataAccess.GetWinners(drawingId); // simple entity framework-based data retrieval
        ExportHelper.GetWinnersAsExcelMemoryStream(stream, winnerList, drawingId);

        string suggestedFilename = string.Format("Drawing_{0}_Winners.xlsx", drawingId);
        return File(stream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", suggestedFilename);
    }

В классе ExportHelper я использую сторонний инструмент ( GemBox.Spreadsheet ) для генерации файла Excel, и у него есть опция Save to Stream. При этом существует несколько способов создания файлов Excel, которые можно легко записать в поток памяти.

public static class ExportHelper
{
    internal static void GetWinnersAsExcelMemoryStream(MemoryStream stream, List<DrawingWinner> winnerList, int drawingId)
    {

        ExcelFile ef = new ExcelFile();

        // lots of Excel worksheet building/formatting code here ...

        ef.SaveXlsx(stream);
        stream.Position = 0; // reset for future read

     }
}

В IE, Chrome и Firefox браузер запрашивает загрузку файла, и никакой реальной навигации не происходит.

11
Andy S

Сначала создайте действие контроллера, которое создаст файл Excel

[HttpPost]
public JsonResult ExportExcel()
{
    DataTable dt = DataService.GetData();
    var fileName = "Excel_" + DateTime.Now.ToString("yyyyMMddHHmm") + ".xls";

    //save the file to server temp folder
    string fullPath = Path.Combine(Server.MapPath("~/temp"), fileName);

    using (var exportData = new MemoryStream())
    {
        //I don't show the detail how to create the Excel, this is not the point of this article,
        //I just use the NPOI for Excel handler
        Utility.WriteDataTableToExcel(dt, ".xls", exportData);

        FileStream file = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
        exportData.WriteTo(file);
        file.Close();
    }

    var errorMessage = "you can return the errors in here!";

    //return the Excel file name
    return Json(new { fileName = fileName, errorMessage = "" });
}

затем создайте действие Download 

[HttpGet]
[DeleteFileAttribute] //Action Filter, it will auto delete the file after download, 
                      //I will explain it later
public ActionResult Download(string file)
{
    //get the temp folder and file path in server
    string fullPath = Path.Combine(Server.MapPath("~/temp"), file);

    //return the file for download, this is an Excel 
    //so I set the file content type to "application/vnd.ms-Excel"
    return File(fullPath, "application/vnd.ms-Excel", file);
}

если вы хотите удалить файл после загрузки, создайте это 

public class DeleteFileAttribute : ActionFilterAttribute
{
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        filterContext.HttpContext.Response.Flush();

        //convert the current filter context to file and get the file path
        string filePath = (filterContext.Result as FilePathResult).FileName;

        //delete the file after download
        System.IO.File.Delete(filePath);
    }
}

и, наконец, AJAX позвонить с вашей точки зрения MVC Razor 

//I use blockUI for loading...
$.blockUI({ message: '<h3>Please wait a moment...</h3>' });    
$.ajax({
    type: "POST",
    url: '@Url.Action("ExportExcel","YourController")', //call your controller and action
    contentType: "application/json; charset=utf-8",
    dataType: "json",
}).done(function (data) {
    //console.log(data.result);
    $.unblockUI();

    //get the file name for download
    if (data.fileName != "") {
        //use window.location.href for redirect to download action for download the file
        window.location.href = "@Url.RouteUrl(new 
            { Controller = "YourController", Action = "Download"})/?file=" + data.fileName;
    }
});
6
Elvin Acevedo

Я использовал решение, опубликованное CSL, но я бы порекомендовал вам не сохранять данные файла в сеансе в течение всего сеанса. При использовании TempData данные файла автоматически удаляются после следующего запроса (который является запросом GET для файла). Вы также можете управлять удалением данных файла в сеансе в действии загрузки.

Сеанс может занимать много памяти/пространства в зависимости от хранилища SessionState и количества файлов, экспортируемых во время сеанса, и если у вас много пользователей.

Я обновил код стороны serer из CSL, чтобы использовать вместо него TempData.

public ActionResult PostReportPartial(ReportVM model){

   // Validate the Model is correct and contains valid data
   // Generate your report output based on the model parameters
   // This can be an Excel, PDF, Word file - whatever you need.

   // As an example lets assume we've generated an EPPlus ExcelPackage

   ExcelPackage workbook = new ExcelPackage();
   // Do something to populate your workbook

   // Generate a new unique identifier against which the file can be stored
   string handle = Guid.NewGuid().ToString()

   using(MemoryStream memoryStream = new MemoryStream()){
        workbook.SaveAs(memoryStream);
        memoryStream.Position = 0;
        TempData[handle] = memoryStream.ToArray();
   }      

   // Note we are returning a filename as well as the handle
   return new JsonResult() { 
         Data = new { FileGuid = handle, FileName = "TestReportOutput.xlsx" }
   };

}

[HttpGet]
public virtual ActionResult Download(string fileGuid, string fileName)
{   
   if(TempData[fileGuid] != null){
        byte[] data = TempData[fileGuid] as byte[];
        return File(data, "application/vnd.ms-Excel", fileName);
   }   
   else{
        // Problem - Log the error, generate a blank file,
        //           redirect to another controller action - whatever fits with your application
        return new EmptyResult();
   }
}
5
Niclas
 $. ajax ({
 type: "GET", 
 url: "/Home/Downloadexcel/",
 contentType:" application/json; charset = utf-8 ", 
 data: null, 
 success: function (Rdata) {
 отладчик; 
 var bytes = new Uint8Array (Rdata.FileContents); 
 var blob = new Blob ([bytes], {type: " application/vnd.openxmlformats-officedocument.spreadsheetml.sheet "}); 
 var link = document.createElement ('a'); 
 link.href = window.URL.createObjectURL (blob); 
 link .download = "myFileName.xlsx"; 
 link.click (); 
}, 
 error: function (err) {

} 

}) ;
0
G.V.K.RAO

Эта тема помогла мне создать собственное решение, которым я поделюсь здесь. Сначала я использовал GET-запрос ajax без проблем, но он дошел до того, что длина URL-адреса запроса была превышена, поэтому мне пришлось перейти на POST.

Javascript использует плагин загрузки файлов JQuery и состоит из 2 последовательных вызовов. Один POST (для отправки параметров) и один GET для получения файла.

 function download(result) {
        $.fileDownload(uri + "?guid=" + result,
        {
            successCallback: onSuccess.bind(this),
            failCallback: onFail.bind(this)
        });
    }

    var uri = BASE_EXPORT_METADATA_URL;
    var data = createExportationData.call(this);

    $.ajax({
        url: uri,
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify(data),
        success: download.bind(this),
        fail: onFail.bind(this)
    });

Серверная сторона

    [HttpPost]
    public string MassExportDocuments(MassExportDocumentsInput input)
    {
        // Save query for file download use
        var guid = Guid.NewGuid();
        HttpContext.Current.Cache.Insert(guid.ToString(), input, null, DateTime.Now.AddMinutes(5), Cache.NoSlidingExpiration);
        return guid.ToString();
    }

   [HttpGet]
    public async Task<HttpResponseMessage> MassExportDocuments([FromUri] Guid guid)
    {
        //Get params from cache, generate and return
        var model = (MassExportDocumentsInput)HttpContext.Current.Cache[guid.ToString()];
          ..... // Document generation

        // to determine when file is downloaded
        HttpContext.Current
                   .Response
                   .SetCookie(new HttpCookie("fileDownload", "true") { Path = "/" });

        return FileResult(memoryStream, "documents.Zip", "application/Zip");
    }
0
Machinegon

Ответ CSL был реализован в проекте, над которым я работаю, но проблема, с которой я столкнулся, заключалась в том, что расширение Azure прервало загрузку наших файлов. Вместо этого я смог сделать это одним вызовом AJAX:

СЕРВЕР

[HttpPost]
public FileResult DownloadInvoice(int id1, int id2)
{
    //necessary to get the filename in the success of the ajax callback
    HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");

    byte[] fileBytes = _service.GetInvoice(id1, id2);
    string fileName = "Invoice.xlsx";
    return File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
}

CLIENT (измененная версия Обработка загрузки файла из поста ajax )

$("#downloadInvoice").on("click", function() {
    $("#loaderInvoice").removeClass("d-none");

    var xhr = new XMLHttpRequest();
    var params = [];
    xhr.open('POST', "@Html.Raw(Url.Action("DownloadInvoice", "Controller", new { id1 = Model.Id1, id2 = Model.Id2 }))", true);
    xhr.responseType = 'arraybuffer';
    xhr.onload = function () {
        if (this.status === 200) {
            var filename = "";
            var disposition = xhr.getResponseHeader('Content-Disposition');
            if (disposition && disposition.indexOf('attachment') !== -1) {
                var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
                var matches = filenameRegex.exec(disposition);
                if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
            }
            var type = xhr.getResponseHeader('Content-Type');

            var blob = typeof File === 'function'
                ? new File([this.response], filename, { type: type })
                : new Blob([this.response], { type: type });
            if (typeof window.navigator.msSaveBlob !== 'undefined') {
                // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
                window.navigator.msSaveBlob(blob, filename);
            } else {
                var URL = window.URL || window.webkitURL;
                var downloadUrl = URL.createObjectURL(blob);

                if (filename) {
                    // use HTML5 a[download] attribute to specify filename
                    var a = document.createElement("a");
                    // safari doesn't support this yet
                    if (typeof a.download === 'undefined') {
                        window.location = downloadUrl;
                    } else {
                        a.href = downloadUrl;
                        a.download = filename;
                        document.body.appendChild(a);
                        a.click();
                    }
                } else {
                    window.location = downloadUrl;

                }

                setTimeout(function() {
                        URL.revokeObjectURL(downloadUrl);
                    $("#loaderInvoice").addClass("d-none");
                }, 100); // cleanup
            }
        }
    };
    xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    xhr.send($.param(params));
});
0
wilsjd

используя ClosedXML.Excel;

   public ActionResult Downloadexcel()
    {   
        var Emplist = JsonConvert.SerializeObject(dbcontext.Employees.ToList());
        DataTable dt11 = (DataTable)JsonConvert.DeserializeObject(Emplist, (typeof(DataTable)));
        dt11.TableName = "Emptbl";
        FileContentResult robj;
        using (XLWorkbook wb = new XLWorkbook())
        {
            wb.Worksheets.Add(dt11);
            using (MemoryStream stream = new MemoryStream())
            {
                wb.SaveAs(stream);
                var bytesdata = File(stream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "myFileName.xlsx");
                robj = bytesdata;
            }
        }


        return Json(robj, JsonRequestBehavior.AllowGet);
    }
0
G.V.K.RAO