J'ai un grand (ish) forme dans MVC.
Je dois être capable de générer un fichier Excel contenant des données d'un sous-ensemble de ce formulaire.
La difficulté réside dans le fait que cela ne devrait pas affecter le reste du formulaire et que je souhaite le faire via AJAX. J'ai rencontré quelques questions sur SO qui semblent liées, mais je ne peux pas tout à fait comprendre ce que les réponses signifient.
Celui-ci semble le plus proche de ce que je suis après: asp-net-mvc-download-Excel - mais je ne suis pas sûr de comprendre la réponse, et il a maintenant quelques années. Je suis également tombé sur un autre article (je ne le trouve plus) sur l'utilisation d'un iframe pour gérer le téléchargement du fichier, mais je ne sais pas comment le faire fonctionner avec MVC.
Mon fichier Excel retourne bien si je fais un post complet mais je ne peux pas le faire fonctionner avec AJAX dans mvc.
Vous ne pouvez pas retourner directement un fichier au téléchargement via un appel AJAX. Une autre solution consiste à utiliser un appel AJAX pour publier les données connexes sur votre serveur. Vous pouvez ensuite utiliser le code côté serveur pour créer le fichier Excel (je vous recommande d'utiliser EPPlus ou NPOI pour cela bien que cela puisse sembler comme si cette partie fonctionnait).
MISE À JOUR Septembre 2016
Ma réponse originale (ci-dessous) datait de plus de 3 ans. Je pensais donc que je mettrais à jour, car je ne crée plus de fichiers sur le serveur lors du téléchargement de fichiers via AJAX. utilisez toujours en fonction de vos besoins spécifiques.
Un scénario courant dans mes applications MVC consiste à créer des rapports via une page Web contenant certains paramètres de rapport configurés par l'utilisateur (plages de dates, filtres, etc.). Lorsque l'utilisateur a spécifié les paramètres qu'il envoie au serveur, le rapport est généré (par exemple, un fichier Excel en sortie), puis je stocke le fichier résultant sous forme de tableau d'octets dans le compartiment TempData
avec une référence unique. Cette référence est renvoyée en tant que résultat Json à ma fonction AJAX qui redirige ensuite vers une action distincte du contrôleur pour extraire les données de TempData
et les télécharger dans le navigateur de l'utilisateur final.
Pour vous donner plus de détails, en supposant que votre vue MVC ait un formulaire lié à une classe Model, appelons Model ReportVM
.
Tout d'abord, une action du contrôleur est requise pour recevoir le modèle posté. Voici un exemple:
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" }
};
}
L'appel AJAX qui poste mon formulaire MVC sur le contrôleur ci-dessus et reçoit la réponse ressemble à ceci:
$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;
}
})
L'action du contrôleur pour gérer le téléchargement du fichier:
[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();
}
}
Un autre changement qui pourrait facilement être adapté si nécessaire consiste à passer le type MIME du fichier en tant que troisième paramètre afin que l'action d'un contrôleur puisse correctement servir une variété de formats de fichier de sortie.
Cela supprime la nécessité de créer et de stocker des fichiers physiques sur le serveur. Ainsi, aucune routine de maintenance n’est requise, ce qui, une fois de plus, est transparent pour l’utilisateur final.
Notez que l'avantage d'utiliser TempData
plutôt que Session
est qu'une fois que TempData
est lu, les données sont effacées, ce qui rendra l'utilisation plus efficace de la mémoire si le volume de demandes de fichiers est important. Voir Meilleures pratiques TempData .
Réponse ORIGINALE
Vous ne pouvez pas retourner directement un fichier au téléchargement via un appel AJAX. Une autre solution consiste à utiliser un appel AJAX pour publier les données connexes sur votre serveur. Vous pouvez ensuite utiliser le code côté serveur pour créer le fichier Excel (je vous recommande d'utiliser EPPlus ou NPOI pour cela bien que cela puisse sembler comme si cette partie fonctionnait).
Une fois le fichier créé sur le serveur, renvoyez le chemin d'accès au fichier (ou simplement le nom du fichier) comme valeur de retour à votre appel AJAX, puis définissez JavaScript window.location
sur cette URL, qui invite le navigateur à télécharger. le fichier.
Du point de vue de l'utilisateur final, l'opération de téléchargement de fichier est transparente, car ils ne quittent jamais la page sur laquelle la demande provient.
Vous trouverez ci-dessous un exemple simple et artificiel d’appel ajax permettant d’atteindre cet objectif:
$.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;
}
});
Un exemple de méthode de contrôleur pour l'action de téléchargement serait:
[HttpGet]
public virtual ActionResult Download(string file)
{
string fullPath = Path.Combine(Server.MapPath("~/MyFiles"), file);
return File(fullPath, "application/vnd.ms-Excel", file);
}
Mes 2 centimes - vous n'avez pas besoin de stocker Excel sous forme de fichier physique sur le serveur - mais de le stocker dans le cache (de session). Utilisez un nom généré de manière unique pour votre variable de cache (qui stocke ce fichier Excel) - il s'agira du retour de votre appel ajax (initial). De cette façon, vous n'avez pas à traiter les problèmes d'accès aux fichiers, à gérer (supprimer) les fichiers lorsque vous n'en avez pas besoin, etc.
J'ai récemment pu accomplir cela dans MVC (bien qu'il ne soit pas nécessaire d'utiliser AJAX) sans créer un fichier physique et je pensais partager mon code:
Fonction JavaScript très simple (un clic sur le bouton datatables.net déclenche cette opération):
function getWinnersExcel(drawingId) {
window.location = "/drawing/drawingwinnersexcel?drawingid=" + drawingId;
}
Code du contrôleur 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);
}
Dans la classe ExportHelper, j'utilise un outil tiers ( GemBox.Spreadsheet ) pour générer le fichier Excel doté d'une option Enregistrer dans le flux. Cela étant dit, il existe plusieurs façons de créer des fichiers Excel faciles à écrire dans un flux de mémoire.
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
}
}
Dans IE, Chrome et Firefox, le navigateur vous invite à télécharger le fichier. Aucune navigation réelle ne se produit.
Créez d'abord l'action du contrôleur qui créera le fichier 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 = "" });
}
puis créez l'action Télécharger
[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);
}
si vous voulez supprimer le fichier une fois téléchargé, créez ceci
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);
}
}
et enfin appel ajax de votre vue 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;
}
});
J'ai utilisé la solution publiée par CSL mais je vous recommande de ne pas stocker les données du fichier dans Session pendant toute la session. En utilisant TempData, les données du fichier sont automatiquement supprimées après la demande suivante (c'est-à-dire la demande GET du fichier). Vous pouvez également gérer la suppression des données du fichier dans Session dans l'action de téléchargement.
La session peut utiliser beaucoup de mémoire/espace en fonction du stockage SessionState et du nombre de fichiers exportés au cours de la session et si vous avez plusieurs utilisateurs.
J'ai mis à jour le code côté sereur à partir de CSL pour utiliser TempData à la place.
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();
}
}
$. ajax ({ type: "GET", url: "/Home/Downloadexcel/", contentType:" application/json; charset = utf-8 ", data: null, success: function (Rdata) { débogueur; var bytes = new Uint8Array (Rdata.FileContents); var blob = new Blob ([octets], {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) { } .______.}) ;
Ce fil m'a aidé à créer ma propre solution que je partagerai ici. Au début, j’utilisais une requête GET ajax sans problème, mais la longueur de l’URL de la requête a été dépassée;.
Le javascript utilise le plugin de téléchargement de fichier JQuery et consiste en 2 appels successifs. Un POST (Pour envoyer des paramètres) et un GET pour récupérer le fichier.
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)
});
Du côté serveur
[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");
}
La réponse de CSL a été implémentée dans un projet sur lequel je travaille, mais le problème que j'ai rencontré était celui de la montée en charge sur Azure, nos téléchargements de fichiers ont été interrompus. Au lieu de cela, j'ai pu le faire avec un appel AJAX:
SERVEUR
[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 (version modifiée de Gestion du téléchargement de fichier depuis 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]*=((['"]).*?\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));
});
La réponse acceptée ne fonctionnait pas très bien pour moi, car j’obtenais un résultat = 502 Bad Gateway de l’appel ajax, même si tout semblait revenir correctement du contrôleur.
J'avais peut-être atteint une limite avec TempData - pas sûr, mais j'ai découvert que si j'utilisais IMemoryCache au lieu de TempData, cela fonctionnait bien. Voici donc ma version adaptée code dans la réponse acceptée:
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();
//This is an equivalent to tempdata, but requires manual cleanup
_cache.Set(handle, memoryStream.ToArray(),
new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(10)));
//(I'd recommend you revise the expiration specifics to suit your application)
}
// Note we are returning a filename as well as the handle
return new JsonResult() {
Data = new { FileGuid = handle, FileName = "TestReportOutput.xlsx" }
};
}
L'appel AJAX reste identique à la réponse acceptée (je n'ai apporté aucune modification):
$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;
}
})
L'action du contrôleur pour gérer le téléchargement du fichier:
[HttpGet]
public virtual ActionResult Download(string fileGuid, string fileName)
{
if (_cache.Get<byte[]>(fileGuid) != null)
{
byte[] data = _cache.Get<byte[]>(fileGuid);
_cache.Remove(fileGuid); //cleanup here as we don't need it in cache anymore
return File(data, "application/vnd.ms-Excel", fileName);
}
else
{
// Something has gone wrong...
return View("Error"); // or whatever/wherever you want to return the user
}
}
...
Maintenant, il y a du code supplémentaire pour configurer MemoryCache ...
Pour utiliser "_cache", j'ai injecté dans le constructeur du contrôleur de la manière suivante:
using Microsoft.Extensions.Caching.Memory;
namespace MySolution.Project.Controllers
{
public class MyController : Controller
{
private readonly IMemoryCache _cache;
public LogController(IMemoryCache cache)
{
_cache = cache;
}
//rest of controller code here
}
}
Et assurez-vous que vous avez les éléments suivants dans ConfigureServices dans Startup.cs:
services.AddDistributedMemoryCache();
using 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);
}