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


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

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

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

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

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

11 73

11 ответов:

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

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

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

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

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

оригинальный ответ

вы не можете напрямую вернуть файл для загрузки через вызов 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.
  • The

мои 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 последующих вызовов. Один пост (для отправки params) и один получить, чтобы вернуть файл.

 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.... И я собираюсь удалить старые файлы с помощью другой программы. Но если есть способ просто отправить массив байтов для загрузки файла, например, с помощью ответа. Я хочу его использовать.

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

в форме отправки

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