105 votes

Télécharger un fichier Excel via AJAX MVC

J'ai un grand formulaire en MVC.

Je dois être en mesure de générer un fichier Excel contenant les données d'un sous-ensemble de ce formulaire.

Le problème est que cela ne doit pas affecter le reste du formulaire et je veux donc le faire via AJAX. J'ai trouvé quelques questions sur SO qui semblent être liées, mais je n'arrive pas à comprendre la signification des réponses.

Celui-ci semble le plus proche de ce que je recherche : asp-net-mvc-downloading-excel - mais je ne suis pas sûr de comprendre la réponse, et elle date de quelques années maintenant. Je suis également tombé sur un autre article (je ne le trouve plus) qui proposait d'utiliser un iframe pour gérer le téléchargement du fichier, mais je ne suis pas sûr de savoir comment le faire fonctionner avec MVC.

Mon fichier excel renvoie bien si je fais un post back complet mais je n'arrive pas à le faire fonctionner avec AJAX dans mvc.

231voto

CSL Points 2466

Vous ne pouvez pas renvoyer directement un fichier à télécharger via un appel AJAX. Une approche alternative consiste donc à utiliser un appel AJAX pour envoyer les données correspondantes à votre serveur. Vous pouvez ensuite utiliser un code côté serveur pour créer le fichier Excel (je recommanderais d'utiliser EPPlus ou NPOI pour cela, même si vous semblez avoir déjà travaillé cette partie).

MISE À JOUR septembre 2016

Ma réponse originale (ci-dessous) datant de plus de 3 ans, j'ai pensé que je pourrais la mettre à jour car je ne crée plus de fichiers sur le serveur lors du téléchargement de fichiers via AJAX ; cependant, j'ai laissé la réponse originale car elle peut encore être utile en fonction de vos besoins spécifiques.

Un scénario courant dans mes applications MVC est la création de rapports via une page web qui a des paramètres de rapport configurés par l'utilisateur (plages de dates, filtres, etc.). Lorsque l'utilisateur a spécifié les paramètres, il les envoie au serveur, le rapport est généré (disons par exemple un fichier Excel comme sortie) et ensuite je stocke le fichier résultant comme un tableau d'octets dans le fichier TempData avec une référence unique. Cette référence est renvoyée sous la forme d'un résultat Json à ma fonction AJAX qui redirige ensuite vers une action de contrôleur distincte pour extraire les données de la base de données. TempData et le télécharger dans le navigateur de l'utilisateur final.

Pour donner plus de détails, supposons que vous avez une vue MVC qui a un formulaire lié à une classe Modèle, appelons le Modèle ReportVM .

Tout d'abord, une action du contrôleur est nécessaire pour recevoir le modèle affiché, un exemple serait :

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 envoie mon formulaire MVC au 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();
   }
}

Une autre modification, qui pourrait facilement être prise en compte si nécessaire, consisterait à transmettre le type MIME du fichier en tant que troisième paramètre, de sorte que l'action du contrôleur puisse servir correctement une variété de formats de fichiers de sortie.

Ainsi, il n'est plus nécessaire de créer et de stocker des fichiers physiques sur le serveur. Aucune routine de gestion n'est donc requise et, une fois encore, l'utilisateur final bénéficie d'une transparence totale.

Notez que l'avantage d'utiliser TempData plutôt que Session est qu'une fois TempData est lue, les données sont effacées. Cette méthode sera donc plus efficace en termes d'utilisation de la mémoire si vous avez un volume élevé de demandes de fichiers. Voir Meilleure pratique TempData .

Réponse ORIGINALE

Vous ne pouvez pas renvoyer directement un fichier à télécharger via un appel AJAX. Une approche alternative consiste donc à utiliser un appel AJAX pour envoyer les données correspondantes à votre serveur. Vous pouvez ensuite utiliser un code côté serveur pour créer le fichier Excel (je recommanderais d'utiliser EPPlus ou NPOI pour cela, même si vous semblez avoir déjà travaillé cette partie).

Une fois que le fichier a été créé sur le serveur, renvoyez le chemin d'accès au fichier (ou simplement le nom du fichier) comme valeur de retour de votre appel AJAX, puis définissez les paramètres JavaScript suivants window.location à cette URL qui invitera le navigateur à télécharger le fichier.

Du point de vue de l'utilisateur final, l'opération de téléchargement du fichier est transparente car il ne quitte jamais la page d'origine de la demande.

Vous trouverez ci-dessous un exemple simple et artificiel d'un appel ajax pour y parvenir :

$.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 est la méthode du contrôleur/action où votre code créera le fichier Excel.
  • données contient les données json qui seront extraites du formulaire.
  • Valeur de retour serait le nom de fichier de votre fichier Excel nouvellement créé.
  • Le site emplacement de la fenêtre redirige vers la méthode Contrôleur/Action qui renvoie effectivement votre fichier à télécharger.

Un exemple de méthode de contrôle pour l'action Télécharger serait :

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

19voto

Luchian Points 217

Mon avis : il n'est pas nécessaire de stocker l'excel dans un fichier physique sur le serveur, mais plutôt dans le cache (de session). Utilisez un nom généré de manière unique pour votre variable Cache (qui stocke le fichier Excel) - ce sera le retour de votre appel ajax (initial). De cette façon, vous n'avez pas à vous occuper des problèmes d'accès aux fichiers, de la gestion (suppression) des fichiers lorsqu'ils ne sont pas nécessaires, etc. et, comme le fichier est dans le cache, il est plus rapide de le récupérer.

18voto

Andy S Points 181

J'ai récemment réussi à accomplir ceci en MVC (bien qu'il n'y ait pas eu besoin d'utiliser AJAX) sans créer un fichier physique et j'ai pensé partager mon code :

Fonction JavaScript très simple (un clic sur le bouton de datatables.net la déclenche) :

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 et il dispose d'une option Enregistrer dans le flux. Cela dit, il existe plusieurs façons de créer des fichiers Excel qui peuvent facilement être écrits 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 invite à télécharger le fichier et aucune navigation ne se produit.

8voto

Elvin Acevedo Points 99

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 de téléchargement

[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 après qu'il ait été 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 un appel ajax depuis 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;
    }
});

7voto

Niclas Points 207

J'ai utilisé la solution proposée par CSL mais je vous recommande de ne pas stocker les données du fichier dans la session pendant toute la durée de la session. En utilisant TempData, les données du fichier sont automatiquement supprimées après la prochaine requête (qui est la requête GET pour le fichier). Vous pouvez également gérer la suppression des données du fichier dans la session dans l'action de téléchargement.

La session peut consommer beaucoup de mémoire/espace en fonction du stockage de SessionState et du nombre de fichiers exportés pendant la session et si vous avez beaucoup d'utilisateurs.

J'ai mis à jour le code côté sérère 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();
   }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X