1.21 jigowatts

Great Scott!

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