web-dev-qa-db-ja.com

AJAX MVC経由でExcelファイルをダウンロードします

MVCに大規模な(ish)フォームがあります。

そのフォームのサブセットからのデータを含むExcelファイルを生成できる必要があります。

トリッキーなビットは、これがフォームの他の部分に影響を与えてはならないということです。そのため、AJAX経由でそれを行いたいと思います。 SOに関するいくつかの質問に出くわしましたが、それらは関連しているようですが、答えが何を意味するのかよくわかりません。

これは私が望んでいるものに最も近いようです: asp-net-mvc-downloading-Excel -しかし、私は応答を理解しているかわかりません、そしてそれは数年前です。また、iframeを使用してファイルのダウンロードを処理することに関する別の記事(もう見つかりません)に出会いましたが、MVCでこれを機能させる方法はわかりません。

完全なポストバックを行っている場合、Excelファイルは正常に戻りますが、mvcでAJAXを使用して動作させることはできません。

83
Valuk

AJAX呼び出しを介してダウンロード用のファイルを直接返すことはできません。そのため、別のアプローチとして、AJAX呼び出しを使用して関連データをサーバーにポストすることができます。サーバー側のコードを使用してExcelファイルを作成できます(この部分は動作しているように聞こえますが、これにはEPPlusまたはNPOIを使用することをお勧めします)。

2016年9月更新

私の元の回答(以下)は3歳以上だったので、AJAXを介してファイルをダウンロードするときにサーバー上にファイルを作成しなくなったため、更新すると思っていましたが、元の回答をそのまま残しました特定の要件に応じて、いくつかの用途があります。

私のMVCアプリケーションの一般的なシナリオは、ユーザーが設定したレポートパラメーター(日付範囲、フィルターなど)を持つWebページを介したレポートです。ユーザーがサーバーに送信するパラメーターを指定すると、レポートが生成され(たとえば、出力としてExcelファイルなど)、結果のファイルを一意の参照を使用してTempDataバケットにバイト配列として保存します。この参照は、Json ResultとしてAJAX関数に返され、その後、別のコントローラーアクションにリダイレクトして、TempDataからデータを抽出し、エンドユーザーのブラウザーにダウンロードします。

これをさらに詳しく説明するには、Modelクラスにバインドされたフォームを持つ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" }
   };

}

上記のコントローラーにMVCフォームを送信し、応答を受け取るAJAX呼び出しは次のようになります。

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

必要に応じて簡単に対応できるもう1つの変更は、1つのControllerアクションがさまざまな出力ファイル形式を正しく処理できるように、ファイルのMIMEタイプを3番目のパラメーターとして渡すことです。

これにより、物理ファイルを作成してサーバーに保存する必要がなくなるため、ハウスキーピングルーチンが不要になり、これもエンドユーザーにとってシームレスです。

TempDataではなくSessionを使用する利点は、TempDataが読み取られるとデータが消去されるため、大量のファイル要求がある場合にメモリ使用量の面でより効率的になることです。 TempData Best Practice を参照してください。

オリジナルの回答

AJAX呼び出しを介してダウンロード用のファイルを直接返すことはできません。そのため、別のアプローチとして、AJAX呼び出しを使用して関連データをサーバーにポストすることができます。サーバー側のコードを使用してExcelファイルを作成できます(この部分は動作しているように聞こえますが、これにはEPPlusまたはNPOIを使用することをお勧めします)。

サーバー上でファイルが作成されたら、AJAX呼び出しへの戻り値としてファイルへのパス(またはファイル名のみ)を返し、ブラウザにプロンプ​​トを表示するこのURLにJavaScript window.locationを設定します。ファイルをダウンロードします。

エンドユーザーの観点から見ると、ファイルのダウンロード操作はシームレスです。リクエストが発生したページを離れることはないからです。

以下は、これを達成するための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ファイルを作成するController/Actionメソッドです。
  • dataパラメーターには、フォームから抽出されるJSONデータが含まれます。
  • returnValueは、新しく作成されたExcelファイルのファイル名になります。
  • window.locationコマンドは、ダウンロード用にファイルを実際に返すController/Actionメソッドにリダイレクトします。

ダウンロードアクションのサンプルコントローラーメソッドは次のとおりです。

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

私の2セント-Excelをサーバー上の物理ファイルとして保存する必要はありません-代わりに、(セッション)キャッシュに保存します。 (そのExcelファイルを保存する)Cache変数に一意に生成された名前を使用します-これは(最初の)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ファイルを生成し、[ストリームに保存]オプションがあります。そうは言っても、メモリストリームに簡単に書き込むことができる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では、ブラウザーはファイルのダウンロードを要求するため、実際のナビゲーションは行われません。

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

次に、ダウンロードアクションを作成します

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

最後に、MVC Razorビューからのajax呼び出し

//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が投稿したソリューションを使用しましたが、セッション全体を通してSessionにファイルデータを保存しないことをお勧めします。 TempDataを使用すると、ファイルデータは次の要求(ファイルのGET要求)の後に自動的に削除されます。また、ダウンロードアクションのセッションでファイルデータの削除を管理することもできます。

SessionStateストレージと、セッション中にエクスポートされるファイルの数、およびユーザーが多い場合は、セッションが大量のメモリ/スペースを消費する可能性があります。

代わりにTempDataを使用するようにCSLからサーバー側のコードを更新しました。

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();
   }
}
6
Niclas
 $。ajax({
タイプ: "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(); 
} 、
エラー:関数(err){
 
} 
 
}); 
1
G.V.K.RAO

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);
    }
1
G.V.K.RAO

このスレッドは、ここで共有する独自のソリューションを作成するのに役立ちました。最初は問題なくGET ajaxリクエストを使用していましたが、リクエストURLの長さを超えてしまったため、POSTに切り替える必要がありました。

JavascriptはJQueryファイルダウンロードプラグインを使用し、2つの成功した呼び出しで構成されます。 1つのPOST(パラメーターを送信するため)と1つの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でスケールアウトしていた問題がファイルのダウンロードを中断しました。代わりに、1つの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);
}

CLIENTajaxポストからのファイルダウンロードの処理バージョン

$("#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

すべてがコントローラーから正常に返されているように見えても、ajax呼び出しから502 Bad Gatewayの結果が得られたため、受け入れられた答えは私にとってはうまくいきませんでした。

おそらく、TempDataで制限に達していた-確かではありませんが、TempDataの代わりにIMemoryCacheを使用した場合、正常に機能することがわかりました。受け入れられた答えのコード:

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

}

AJAX呼び出しは、受け入れられた回答と同じままです(変更は行いませんでした)。

$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 (_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
    }
}

...

これで、MemoryCacheをセットアップするためのコードが追加されました...

「_cache」を使用するために、次のようにコントローラーのコンストラクターに注入しました。

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
  }
 }

また、Startup.csのConfigureServicesに次のものがあることを確認します。

services.AddDistributedMemoryCache();
0
egmfrs