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

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

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

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

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

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

11 ответов


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

обновление сентябрь 2016

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

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

чтобы дать это более подробно, предполагая, что у вас есть представление MVC, которое имеет форму, привязанную к классу модели, давайте вызовем 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-адресом параметр-это метод контроллера / действия, в котором ваш код создаст файл Excel.
  • сведения параметр содержит данные json, которые будут извлечены из формы.
  • аргумент returnvalue будет имя файла созданный файл Excel.
  • на

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


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

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

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.Таблице) для создания файла Excel, и он имеет опцию Сохранить в поток. То существо существует несколько способов создания файлов 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 браузер запрашивает загрузку файла, и фактическая навигация не происходит.


я использовал решение, опубликованное 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();
   }
}

сначала создайте действие контроллера, которое создаст файл 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 = "" });
}

затем создать скачать action

[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 view

//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;
    }
});

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

javascript использует плагин загрузки файлов JQuery и состоит из 2 последующих вызовов. Один пост (для отправки параметров) и один получить, чтобы восстановить файл.

 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");
    }

ответ 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);
}

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

$("#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]*=((['"]).*?|[^;\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));
});

использование 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);
    }

$.ajax({
                type: "GET",
                url: "/Home/Downloadexcel/",
                contentType: "application/json; charset=utf-8",
                data: null,
                success: function (Rdata) {
                    debugger;
                    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) {

                }

            });

Я использую Asp.Net WebForm и просто я хочу загрузить файл со стороны сервера. Есть много статей, но я не могу найти простой ответ. Теперь, я попробовал простой способ и получил его.

Это моя проблема.

мне нужно создать много кнопок ввода динамически во время выполнения. И я хочу добавить каждую кнопку для загрузки кнопки с предоставлением уникального номера файла.

Я создаю каждую кнопку так:

fragment += "<div><input type=\"button\" value=\"Create Excel\" onclick=\"CreateExcelFile(" + fileNumber + ");\" /></div>";

каждый кнопка вызывает этот метод ajax.

$.ajax({
    type: 'POST',
    url: 'index.aspx/CreateExcelFile',
    data: jsonData,
    contentType: 'application/json; charset=utf-8',
    dataType: 'json',
    success: function (returnValue) {
      window.location = '/Reports/Downloads/' + returnValue.d;
    }
});

затем я написал основной простой метод.

[WebMethod]
public static string CreateExcelFile2(string fileNumber)
{
    string filePath = string.Format(@"Form_{0}.xlsx", fileNumber);
    return filePath;
}

я генерирую этот Form_1, Form_2, Form_3.... И я собираюсь удалить старые файлы с помощью другой программы. Но если есть способ просто отправить массив байтов для загрузки файла, например, с помощью Response. Я хочу им воспользоваться.

Я надеюсь, что это будет полезно для кого-либо.


отправить форму

public ActionResult ExportXls()
{   
 var filePath="";
  CommonHelper.WriteXls(filePath, "Text.xls");
}

 public static void WriteXls(string filePath, string targetFileName)
    {
        if (!String.IsNullOrEmpty(filePath))
        {
            HttpResponse response = HttpContext.Current.Response;
            response.Clear();
            response.Charset = "utf-8";
            response.ContentType = "text/xls";
            response.AddHeader("content-disposition", string.Format("attachment; filename={0}", targetFileName));
            response.BinaryWrite(File.ReadAllBytes(filePath));
            response.End();
        }
    }