読者です 読者をやめる 読者になる 読者になる

1.21 jigowatts

Great Scott!

ASP.NET MVCはじめました~ajaxメソッドでローディング画像を表示して検索結果を取得しExcelでダウンロードする

概要

Excelダウンロード処理を書いてみましたが、データの取得に時間がかかるとブラウザが固まってしまいます。なので今回は処理中にローディング画像をぐるぐる表示するようにしてみます。

処理の流れも変わりまして、jQueryajaxメソッドで一旦サーバー上にExcelファイルを作成して、コールバックでファイルをダウンロードします。
f:id:sh_yoshida:20150117075225p:plain

環境

Visual Studio 2010
ASP.NET MVC2
SQLServer2008 R2
EPPlus 3.1:EPPlus-Create advanced Excel spreadsheets on the server - Home

今回の要件は

  1. 処理中にローディング画像を表示する
  2. 検索結果がExcelでダウンロードできる

として、サンプルコードを書いてみました。

実装

Ajax.BeginFormの記事で書いたコードに追記する感じでダウンロード機能を実装しました。

JavaScriptファイルの用意

最初はajaxメソッドでダウンロード用のアクションを呼ぶだけでいいかと思っていたのですが、ダウンロードダイアログが表示されず処理が終了してしまいました。調べたところ、ajaxメソッド的にExcelファイルのダウンロードはレスポンスでダメというのを見つけて、じゃあどうするかってことで、また調べてたらstackoverflowに丁度いい感じの質問と回答があったので参考にさせてもらいました(まるっと)。
■Mvc2App/Scripts/Libs/PeopleAjaxSearch.js

$(function () {
    $('#downloading').hide();
    //ダウンロードボタン押下時イベント
    var req;
    $('#btnDownload').click(function () {
        $('#btnDownload').attr("disabled", "disabled");

        $('#downloading').show();
        var dataLength = $("#gridData tbody").children().length;
        if (dataLength <= 1) {
            $('#downloading').hide();
            alert('There is no data.')
            $('#btnDownload').removeAttr("disabled");
            return;
        }
          
        req = $.ajax({
            url: getPath("People/CreateReport"),
            type: 'POST',
            cache: false,
            success: function (returnValue) {
                var rep = getPath("People/DownloadFile");
                window.location = rep + '?file=' + returnValue;
            },
            error: function () { alert('NG...'); },
            complete: function () {
                $('#downloading').hide();
                $('#btnDownload').removeAttr("disabled");
            }
        });
    });

    $('#downloading').click(function () {
        req.abort();
        alert('ダウンロードを中止しました。');
    });
});
コントローラーの用意

アクションメソッドExcelファイルを作成するものと、作成したファイルをダウンロードする2段階です。
■Mvc2App/Controllers/PeopleController.cs

private static readonly string eOutputDir = "~/Report";
[HttpPost]
public string CreateReport()
{
    var condition = Session["Conditions"] as PeopleSearchConditionModel;
    if (condition == null)
    {
        return "ERROR";
    }
    System.Threading.Thread.Sleep(5000);
    var file = _service.CreateReport(condition, Server.MapPath(eOutputDir));
    return file;
}

[HttpGet]
public ActionResult DownloadFile(string file)
{
    if (file == "ERROR")
    {
        return RedirectToAction("Show", "Error", new { code = "NotFound" });
    }
    var contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

    var fullPath = Path.Combine(Server.MapPath(eOutputDir), file);
    if (System.IO.File.Exists(fullPath))
    {
        return File(fullPath, contentType, file);
    }
    return RedirectToAction("Show", "Error", new { code = "NotFound" });
}
サービスインタフェースとサービスクラスの用意

■Mvc2App/Services/PeopleService.cs

string CreateReport(PeopleSearchConditionModel condition, string root);

Excelファイルを作成します。ファイル名がかぶらないようにファイル名に日時を付与していますが、サーバーにファイルがたまり続けてしまうので、前日以前分は削除するようにしました。
■Mvc2App/Services/PeopleService.cs

public string CreateReport(PeopleSearchConditionModel condition, string root)
{
    FileClearner(root);
    var file = CreateFileName();
    try
    {
        var data = _repository.Search(condition);
        var fmt = Mapping<Person>.ToArray<PeopleExcelFormat>(data);

        MakeFile(file, root, fmt);

        return file;
    }
    catch (Exception e)
    {
        Logger.Error(e.Message);
        Logger.Debug(e.StackTrace);

        if (e.InnerException != null)
        {
            Logger.Error(e.InnerException.Message);
            Logger.Debug(e.InnerException.StackTrace);
        }
        throw;
    }
}

private static readonly string prefixFile = "People_";
private void FileClearner(string root)
{
    var today = DateTime.Parse(DateTime.Now.ToString("yyyy/MM/dd"));
    var pattern = prefixFile + "*";
    string[] files = Directory.GetFiles(root, pattern);
    foreach (var item in files)
    {
        var lastWriteTime = File.GetLastWriteTime(item);

        if (lastWriteTime < today)
        {
            File.Delete(item);
        }
    }
}

private static void MakeFile(string file, string root, PeopleExcelFormat[] fmt)
{
    var fullPath = Path.Combine(root, file);
    var newFile = new FileInfo(fullPath);
    using (var excelFile = new ExcelPackage(newFile))
    {
        var worksheet = excelFile.Workbook.Worksheets.Add("Sample");
        var range = worksheet.Cells[1, 1];  //開始位置
        var length = fmt.Length;

        for (int i = 0; i < length; i++)
        {
            var updateDateRow = i + 2;
            var updateDateCol = 6;
            worksheet.Cells[updateDateRow, updateDateCol].Style.Numberformat.Format = "yyyy/mm/dd";
        }
        range.LoadFromCollection(Collection: fmt, PrintHeaders: true);
        excelFile.Save();
    }
}

private static string CreateFileName()
{
    var format = "yyyyMMddHHmmssfff";
    var currentDate = DateTime.Now.ToString(format);
    var file = prefixFile + currentDate + ".xlsx";
    return file;
}
ビューの用意

ダウンロード時のローディング画像だけ追加しました。
■Mvc2App/Views/People/SearchByAjax.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
 Inherits="System.Web.Mvc.ViewPage<Mvc2App.ViewModels.People.PeopleSearchViewModel>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
	AjaxSearch
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>AjaxSearch</h2>

    <% using (Ajax.BeginForm("AjaxSearch",
        new AjaxOptions
        {
            UpdateTargetId = "ajaxGrid",
            LoadingElementId = "loading",
            OnSuccess = "ajaxSearchOnSuccess",
            OnFailure = "ajaxSearchOnFailure"
        }
    ))
        { %>
    <fieldset>
        <legend>Search Conditions</legend>
            <div class="field-box">
                <div class="field">
                    <span class="field-label">Name</span>
                    <%: Html.TextBoxFor(model => model.condition.Name)%>
                </div>
                <div class="field">
                    <span class="field-label">Address</span>
                    <%: Html.TextBoxFor(model => model.condition.Address)%>
                </div>
                <div class="field">
                    <span class="field-label">PhoneNumber</span>
                    <%: Html.TextBoxFor(model => model.condition.PhoneNumber)%>
                </div>
                <div class="field">
                    <span class="field-label">UpdatedBy</span>
                    <%: Html.TextBoxFor(model => model.condition.UpdatedBy)%>
                </div>
                <div class="field">
                    <span class="field-label">UpdateDate</span>
                    <%: Html.TextBoxFor(model => model.condition.UpdateDate)%>
                </div>
            </div>

            <div id="btnBox1">
                <div id="btnBox2">
                    <input id="btnSearch" type="submit" value="Search" />
                    <input id="btnDownload" type="button" value="Download" />
                    <img id="loading" src="<%: Url.Content("~/Content/images/loading.gif") %>" alt="" class="loader" />
                    <img id="downloading" src="<%: Url.Content("~/Content/images/downloading.gif")  %>" alt="" />
                </div>                
            </div>
    </fieldset>
    <%} %>

    <div id="ajaxGrid">
    </div>
</asp:Content>

<asp:Content ID="ScriptContent" ContentPlaceHolderID="ScriptContent" runat="server">
    <script type="text/javascript" src="<%: Url.Content("~/Scripts/Libs/PeopleAjaxSearch.js") %>"></script>
</asp:Content>

実行結果

検索結果を一覧表示し、ダウンロードボタンを押下するとローディング画像がぐるぐるします。
f:id:sh_yoshida:20150117075225p:plain
ダウンロードダイアログが立ち上がるので、ファイルを保存します。
f:id:sh_yoshida:20150117075400p:plain
Excelで開いてデータを確認。ちゃんと書き込まれてますね。
f:id:sh_yoshida:20150117075413p:plain

おまけ

今回はExcelファイルの作成に時間がかかることが前提として、ローディング画像の表示を実装しましたので、abortメソッドAjax通信のキャンセルができるようにしてみました。
ダウンロードボタンを押下し、ぐるぐる回っているローディング画像をクリックするとAjax通信が
キャンセルされます。
f:id:sh_yoshida:20150117080110p:plain

参考

ファイル生成
C# - EPPlusを使ったExcel Hello World - Qiita

ファイルダウンロード
c# - Download Excel file via AJAX MVC - Stack Overflow


ASP.NET MVCはじめました~検索結果をExcelでダウンロードする - 1.21 jigowatts


ASP.NET MVCはじめました~Ajax.BeginFormでデータグリッドとページング - 1.21 jigowatts