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

ASP.NET MVCはじめました~検索結果をjqGridに表示する

概要

自前でページング処理を実装してきましたが、jqGridを使って面倒な処理を丸投げしてしまいます。


ASP.NET MVCはじめました~検索画面を作る - 1.21 jigowatts

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

環境

Visual Studio 2010
ASP.NET MVC2
jqGrid

今回の要件は

  1. 検索結果データを非同期で取得する
  2. jqGridに表示する
  3. ページングやソート機能を実装する

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

実装

JavaScriptファイルの用意

検索ボタン押下イベントとjqGridの設定を書きます。
JavaScriptを外出しにしたため、URLを生成する箇所で躓きました(後述)。
■Mvc2App/Scripts/Libs/PeopleJqgridSearch.js

$(function () {

    //検索ボタン押下イベント
    $('#btnSearch').click(function () {
        var $ctrls = $("#searchGrid");
        var dataType = function (postData) {
            var url = getPath("People/GetData");
            var arg = {
                'Name': $('#condition_Name').val(),
                'Address': $('#condition_Address').val(),
                'PhoneNumber': $('#condition_PhoneNumber').val(),
                'UpdatedBy': $('#condition_UpdatedBy').val(),
                'UpdateDate': $('#condition_UpdateDate').val(),
                'sidx': postData.sidx,
                'sord': postData.sord,
                'page': postData.page,
                'rows': postData.rows
            };
            $.post(
                url,
                arg,
                function (data) {
                    $ctrls[0].addJSONData(data);
                },
                "json"
            );
        }
        $ctrls.jqGrid("setGridParam", { datatype: dataType });
        $ctrls.trigger("reloadGrid");   // <=ここで処理が走る
    });


    //*グリッド定義
    $('#searchGrid').jqGrid({
        'loadError': function (xhr, status, error) {
            alert(xhr.statusText)
        },
        datatype: "local",
        width: 1150,
        height: 96,
        rowNum: 3,//表示件数:default20
        //hiddengrid: true,
        hidegrid: true,
        multiselect: false,
        shrinkToFit: false,
        rownumbers: true,
        showPager: true,
        viewrecords: true,
        caption: " Search result",
        pager: '#pager',
        jsonReader: {
            page: "page",
            total: "total",
            records: "records",
            root: "data"
        },
        colNames: ['Name', 'Address', 'PhoneNumber', 'UpdatedBy', 'UpdateDate'],
        colModel: [{ 'name': 'Name' }, { 'name': 'Address' }, { 'name': 'PhoneNumber' }, { 'name': 'UpdatedBy' }, { 'name': 'UpdateDate'}]

    });
    $("#searchGrid").jqGrid('navGrid', "#pager", { add: false, edit: false, del: false, search: false, refresh: false });

});
コントローラーの用意

ビューの初期表示時用アクションメソッドと、検索結果データを取得するAjax呼び出し用アクションメソッドを記述します。検索結果はJsonメソッドJSON形式にシリアル化して返します。
■Mvc2App/Controllers/PeopleController.cs

[HttpGet]
public ActionResult JqgridSearch()
{
    return View();
}

[HttpPost]
public ActionResult GetData(PeopleSearchConditionModel condition)
{
    if (Request.IsAjaxRequest()) 
    {
        var model = _service.JqgridSearch(condition);
        return Json(model);
    }

    return RedirectToAction("Show", "Error");
}
サービスインタフェースとサービスクラスの用意

■Mvc2App/Services/PeopleService.cs

public interface IPeopleService
{
    JqgridPeopleSearchViewModel JqgridSearch(PeopleSearchConditionModel condition);
}

■Mvc2App/Services/PeopleService.cs

public JqgridPeopleSearchViewModel JqgridSearch(PeopleSearchConditionModel condition)
{
    var searcher = new PeopleSearcher();
    return searcher.JqgridSearch(condition);
}
検索フレームワークの用意

jqGridの表示用ビューモデルに詰め替えます。
■Mvc2App/Framework/Search/PeopleSearcher.cs

public class PeopleSearcher
{
    public JqgridPeopleSearchViewModel JqgridSearch(PeopleSearchConditionModel condition)
    {
        var data = GetAllData(condition);
        var list = new List<JqgirdPeopleViewModel>();
        foreach (var item in data)
        {
            var rec = new JqgirdPeopleViewModel();
            rec.Name  = item.Name;
            rec.Address = item.Address;
            rec.PhoneNumber = item.PhoneNumber;
            rec.UpdatedBy = item.UpdatedBy;
            rec.UpdateDate = item.UpdateDate.ToShortDateString();
            list.Add(rec);
        }
        var helper = new SearchHelper<JqgirdPeopleViewModel>(list.ToArray(), condition);
        helper.Order();
        var model = new JqgridPeopleSearchViewModel()
        {
            data = helper.GetData(),
            page = helper.GetPage(),
            records = helper.GetRecords(),
            total = helper.GetTotal(),
            SysMessage = helper.GetTotalCountMessage()
        };

        return model;
    }
}

ソート機能を追加しました。
■Mvc2App/Framework/Search/SearchHelper.cs

public class SearchHelper<T>
{
    private T _instance;
    private T[] _data;
    private SearchConditionModelBase _condition;

    public SearchHelper(T[] data, SearchConditionModelBase condition)
    {
        _instance = Activator.CreateInstance<T>();
        _data = data;
        _condition = condition;
    }

    ...

    public void Order()
    {
        if (!string.IsNullOrEmpty(_condition.Sidx))
        {
            if ("asc".Equals(_condition.Sord))
            {
                _data = OrderBy(_data, _condition.Sidx);
            }
            else if ("desc".Equals(_condition.Sord))
            {
                _data = OrderByDescending(_data, _condition.Sidx);
            }
        }
    }

    private T[] OrderBy(T[] data, string sortCol)
    {
        Type type = _instance.GetType();
        var prop = type.GetProperty(sortCol);

        data = data.OrderBy(x => prop.GetValue(x, null)).ToArray();

        return data;
    }

    private T[] OrderByDescending(T[] data, string sortCol)
    {
        Type type = _instance.GetType();
        var prop = type.GetProperty(sortCol);

        data = data.OrderByDescending(x => prop.GetValue(x, null)).ToArray();

        return data;
    }
}
ビューモデルの用意

■Mvc2App/ViewModels/People/PeopleViewModel.cs

public class JqgirdPeopleViewModel : ViewModelBase 
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string PhoneNumber { get; set; }
    public string UpdatedBy { get; set; }
    public string UpdateDate { get; set; }
}
ビューの用意

アプリケーション名を考慮したURLの生成のため、getPathファンクションを用意しました。
■Mvc2App/Views/Shared/Site.Master

<body>
    <div class="page">
        ...
    </div>
    <script type="text/javascript">
        //参考
        //http: //stackoverflow.com/questions/6796880/how-to-get-asp-net-mvc-3-current-project-name
        var root = '<%: Url.Content("~/") %>';
        function getPath(url) {
            return root + url;
        }
    </script>
</body>

■Mvc2App/Views/People/JqgridSearch.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">
	JqgridSearch
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2>JqgridSearch</h2>
    <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="button" value="Search" />
                </div>                
            </div>
    </fieldset>

    <div>
        <table id="searchGrid"></table>
        <div id="pager"></div>
    </div>
</asp:Content>

<asp:Content ID="ScriptContent" ContentPlaceHolderID="ScriptContent" runat="server">
    <script type="text/javascript" src="<%: Url.Content("~/Scripts/i18n/grid.locale-ja.js")%>" ></script>
    <script type="text/javascript" src="<%: Url.Content("~/Scripts/jquery.jqGrid.src.js") %>"></script>
    <script type="text/javascript" src="<%: Url.Content("~/Scripts/Libs/PeopleJqgridSearch.js") %>"></script>
</asp:Content>

だいぶソースが散らかってきました!*1

実行結果

初期表示時はグリッドには何も表示されません。
f:id:sh_yoshida:20150114034130p:plain
検索結果を取得しグリッドに表示。
f:id:sh_yoshida:20150114034239p:plain
ページング機能とソート機能を実装しました。
2ページ目を表示したところ。
f:id:sh_yoshida:20150114034250p:plain
項目を押下すると並び替えてくれます。
Name項目を昇順で並び替え。
f:id:sh_yoshida:20150114034301p:plain
検索条件を入れての検索。
f:id:sh_yoshida:20150114034311p:plain


今回はまったポイント

上でも書きましたが、URLの生成にはまりました。
今回の例ではPostメソッドの引数のurlに"/People/GetData"と指定したところ、ローカルでデバッグしているときは問題なかったのですがIISにデプロイしたところ404になってしまいました。
というのも、http://localhost:12345/People/GetDataであればアクセスできたのですが、IIS上ではアプリケーション名が付与される設定だったため、本来http://192.168.11.123:80/Mvc2App_deploy/People/GetDataにアクセスすべきところが、http://192.168.11.123:80/People/GetDataにアクセスしてしまい、404となってしまいました。

いろいろ調べてみた結果、下記を参考にgetPathファンクションを作成して対応しています。このファンクションをSite.Masterに仕込んでおくことで、仮想アプリケーションルートを含めたパスの生成が可能になりました。
javascript - How to get Asp.Net MVC 3 current project name? - Stack Overflow

JavaScriptをビューに書けばいいんでしょうけど、別の方が見やすいので一手間かけました。こんなときどうする??


ASP.NET MVCはじめました~検索画面を作る - 1.21 jigowatts

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

*1:この記事の内容だけじゃ動かないので他の記事と合わせて補完してください。そのうちどうにかしたい!

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

概要

前回の検索ではカスタムHtmlヘルパーを使用したページャー機能を持つデータグリッドを記述しましたが、非同期でグリッドデータのみ更新したかったのでAjax.BeginFormを使用したAjaxベースのデータグリッドを記述してみました。

f:id:sh_yoshida:20141224041854p:plain

環境

Visual Studio 2010
ASP.NET MVC2

今回の要件は

  1. 検索結果データを非同期で取得する
  2. ローディングイメージを表示する
  3. ページングも非同期で行えるようにする

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

実装

コントローラーの用意

ビューの初期表示時用アクションメソッドと、検索結果データを取得し部分ビューを表示するAjax呼び出し用アクションメソッドを記述します。
■Mvc2App/Controllers/PeopleController.cs

//初期表示
public ActionResult SearchByAjax()
{
    return View();
}

[HttpPost]
public ActionResult AjaxSearch(string pageIndex, PeopleSearchConditionModel condition)
{
    if (Request.IsAjaxRequest())
    {
        if (pageIndex != null)
        {
            condition = Session["Conditions"] as PeopleSearchConditionModel;
            condition.Page = pageIndex;
        }
        Session["Conditions"] = condition;
        var model = _service.Search(condition);
        //ローディング画像をテスト表示するために少し処理を止める
        System.Threading.Thread.Sleep(1000);

        return PartialView("_ajaxGrid", model);
    }
    return RedirectToAction("Show", "Error");
}
メインビューの用意

Ajax.BeginFormヘルパーを使用し、AjaxによるForm送信を行います。引数に指定したAjaxOptionsのUpdateTargetIdプロパティに指定されたdivタグの位置にデータグリッド(部分ビュー)が表示され、処理中はLoadingElementIdプロパティに指定したローディングイメージが表示されるようにしました。また、Ajax通信終了時には結果によってJavaScriptに記述したファンクションが呼ばれるよう、OnSuccess、OnFailureプロパティも指定しています。
■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" />
                </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>
部分ビューの用意

データグリッド部分になります。ページング機能はカスタムAjaxヘルパークラスを作成し、こちらに記述しました。
■Mvc2App/Views/People/_ajaxGrid.ascx

<%@ Control Language="C#" 
Inherits="System.Web.Mvc.ViewUserControl<Mvc2App.ViewModels.People.PeopleSearchViewModel>" %>
<%@ Import Namespace="Mvc2App.Extensions.Html"  %>
    <% if (Model != null)
       { %>

        <% if (Model.records == 0) { %>
        <div class="systemMessage">
            <span><%: Model.SysMessage %></span>
        </div>
        <% }
           else
        { %>
        <div id="grid">
            <table id="gridData">
                <tr>
                    <th></th>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Address</th>
                    <th>PhoneNumber</th>
                    <th>UpdatedBy</th>
                    <th>UpdateDate</th>
                </tr>
                <% foreach (var item in Model.data) { %>    
                <tr>
                    <td>
                        <%: Html.ActionLink("Edit", "Edit", new { id=item.ID}) %> |
                        <%: Html.ActionLink("Details", "Details", new { id = item.ID })%> |
                        <%: Html.ActionLink("Delete", "Delete", new { id = item.ID })%>
                    </td>
                    <td>
                        <%: item.ID %>
                    </td>
                    <td>
                        <%: item.Name %>
                    </td>
                    <td>
                        <%: item.Address %>
                    </td>
                    <td>
                        <%: item.PhoneNumber %>
                    </td>
                    <td>
                        <%: item.UpdatedBy %>
                    </td>
                    <td>
                        <%: String.Format("{0:yyyy-MM-dd HH:mm:ss.fff}", item.UpdateDate) %>
                    </td>
                </tr>
                <% } %>
            </table> 
        </div>
        <div>
            <%: Ajax.AjaxPager("pager",
                                        Model.page,
                                        Model.total,
                                        Model.records,
                                        "AjaxSearch"
                                        ,new AjaxOptions {UpdateTargetId = "ajaxGrid",
                                                                LoadingElementId = "loading",
                                                                OnSuccess = "ajaxSearchOnSuccess",
                                                                OnFailure = "ajaxSearchOnFailure"}
            )%>
        </div>
    <%} %>
 <%  }%>
JavaScriptファイルの用意

Ajax通信終了時のファンクションを記述します。今回はアラートを表示するだけです。
■Mvc2App/Scripts/Libs/PeopleAjaxSearch.js

function ajaxSearchOnSuccess() { alert('Success'); }
function ajaxSearchOnFailure() { alert('fail'); }
カスタムAjaxヘルパーの用意

ビューに記述すると読みにくくなるので、ヘルパークラスを用意しHTML文字列を生成します。
今回はAjax.ActionLinkを使用して非同期用リンクを作成し、このリンクからアクションメソッドが呼び出されるようにしました。
■Mvc2App/Extensions/Html/AjaxPagerHelper.cs

using System.Web.Mvc.Ajax;

public static class AjaxPagerHelper
{
    public static MvcHtmlString AjaxPager(this AjaxHelper helper,
                                                string cls,
                                                int page,
                                                int total,
                                                int record,
                                                string actionName,
                                                AjaxOptions ajaxOptions
                                                )
    {

        var pager = new StringBuilder();
        pager.AppendFormat("<div class={0}>", "pagerDataBox");
        pager.AppendFormat("{0}/{1} Page : ", page.ToString(), total.ToString());
        pager.AppendFormat("{0} Record", record.ToString());
        pager.AppendFormat("</div>");
        pager.AppendFormat("<div class={0}>", "pagerBox");
        pager.AppendFormat("<table class={0}>", cls);
        pager.AppendFormat("<tr>");
        pager.AppendFormat("<td>");
        var htmlAttributes = new { disabled = "disabled" };

        string prevPage = (page - 1).ToString();
        string linkTextPrev = "≪ PREV";
        if (page == 1)
        {
            prevPage = page.ToString() + "#";
            pager.Append(AjaxActionLinkPageIndex(helper, linkTextPrev, actionName, prevPage, ajaxOptions, htmlAttributes));
        }
        else
        {
            pager.Append(AjaxActionLinkPageIndex(helper, linkTextPrev, actionName, prevPage, ajaxOptions));
        }
        pager.AppendFormat("|");

        for (int i = 0; i < total; i++)
        {
            int pageIndex = i + 1;
            if (page == pageIndex)
            {
                pager.AppendFormat("<b>");
                pager.Append(AjaxActionLinkPageIndex(helper, pageIndex.ToString(), actionName, pageIndex.ToString(), ajaxOptions));
                pager.AppendFormat("</b>");
            }
            else
            {
                pager.Append(AjaxActionLinkPageIndex(helper, pageIndex.ToString(), actionName, pageIndex.ToString(), ajaxOptions));
            }
            pager.AppendFormat("|");
        }

        string nextPage = (page + 1).ToString();
        var linkTextNext = "NEXT ≫";
        if (page == total || total == 0)
        {
            nextPage = page.ToString() + "#";
            pager.Append(AjaxActionLinkPageIndex(helper, linkTextNext, actionName, nextPage, ajaxOptions, htmlAttributes));
        }
        else
        {
            pager.Append(AjaxActionLinkPageIndex(helper, linkTextNext, actionName, nextPage.ToString(), ajaxOptions));
        }

        pager.AppendFormat("</td>");
        pager.AppendFormat("</tr>");
        pager.AppendFormat("</table>");
        pager.AppendFormat("</div>");

        return MvcHtmlString.Create(pager.ToString());
    }

    private static string AjaxActionLinkPageIndex(AjaxHelper helper, string linkText, string actionName, string pageIndex, AjaxOptions ajaxOptions, object htmlAttributes = null) 
    {
        return helper.ActionLink(linkText, actionName, new { pageIndex = pageIndex }, ajaxOptions, htmlAttributes).ToHtmlString();
    }
}
CSSの用意

ついでにメモ程度に。
■Mvc2App/Content/Site.css

#grid > table
{
    width:3000px;    
}
#grid
{
    height:200px;
    overflow:auto;
}

#orderTypeGrid
{
    height:242px;
    overflow:auto;
}

#btnBox1
{
    display:table;
    height:45px;
    margin-top:20px;
}
#btnBox2
{
    display:table-cell;
    vertical-align:middle;
    height:45px
}

.loader
{
    vertical-align:middle;
    display:none;  
}

.pager,
.pager td
{
    border:none;
    border-spacing:0 px;  
    margin:auto;
}

.pager a:link,
.pager a:active,
.pager a:visited
{
    color: #004e79;
    text-decoration:none;
    margin:0px 4px 0px 4px;
}

.pagerDataBox
{
    float:right;  
    margin-top:5px;
}

.pagerBox
{
}
    
.systemMessage > span
{
    font-size: 1em;
    font-weight:bold;
    color:Red;
}

実行結果

検索ボタンを押下するとローディングイメージがぐるぐる回って、データグリッドが表示されました。
Ajax通信が正常に終了するとSuccessアラートも表示されています。
f:id:sh_yoshida:20141224041854p:plain
また、ページング時もローディングイメージがぐるぐる回って、2ページ目のデータが表示されました。
f:id:sh_yoshida:20141224041911p:plain


今回はまったポイント

Ajax.BeginFormによる部分ビューの表示はできたのですが、グリッドのページャーが通常のリンクだったため、クエリパラメータでページを渡し、GETメソッドでなんとか非同期で部分ビューを表示しようとしていましたが部分ビューだけ表示されてしまったりしてうまくいきませんでした。
いろいろググっているうちにAjax.ActionLinkにたどり着き、Ajaxヘルパーをカスタマイズすることでなんとか動く形になりました。
うーん、これでよかったのだろうか。
もっと他にもいい方法があるかもしれません。


ASP.NET MVCはじめました~検索画面を作る - 1.21 jigowatts