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

1.21 jigowatts

Great Scott!

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

ASP.NET MVC

概要

データの一覧表示画面を作成しましたが、実際は検索画面と一覧表示はセットになることが多いと思います。
今回は検索と簡単なページング機能を実装してみます。

環境

Visual Studio 2010
ASP.NET MVC2
SQLServer2008 R2

今回の要件は

  1. 初期表示時は検索結果を一覧表示しない
  2. 検索ボタンを押下することで条件に応じた検索結果を一覧表示する
  3. 一覧には3件まで表示する
  4. 3件以上データがヒットした場合はページングできるようにする

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

実装

コントローラーの用意
using System;
using System.Data;
using System.Web.Mvc;
using Mvc2App.Common;
using Mvc2App.InputModels.Peple;
using Mvc2App.Models;
using Mvc2App.Services;
using Mvc2App.Services.Abstractions;
using Mvc2App.ViewModels.Peple;

namespace Mvc2App.Controllers
{
    [Authorize]
    public class PepleController : Controller
    {
        IPepleService _service;

        public PepleController() : this(new PepleService())
        { 
            
        }

        public PepleController(IPepleService service)
        {
            _service = service;
        }

        ...

        [HttpPost]
        public ActionResult Search(PepleSearchConditionModel condition)
        {
            Session["condition"] = condition;
            var model = _service.Search(condition);
            return View(model);
        }

        [HttpGet]
        public ActionResult Search(int? pageIndex)
        {
            var condition = (PepleSearchConditionModel)Session["condition"];
            if (pageIndex != null && 
                condition != null)
            {
                condition.Page = pageIndex.ToString();
                var model = _service.Search(condition);
                return View(model);
            }
            else 
            {
                return View();
            }
        }
    }
}
サービスインタフェースとサービスクラスの用意
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Mvc2App.InputModels.Peple;
using Mvc2App.Models;
using Mvc2App.ViewModels.Peple;

namespace Mvc2App.Services.Abstractions
{
    public interface IPepleService
    {
        ...
        PepleSearchViewModel Search(PepleSearchConditionModel condition);
    }
}
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Mvc2App.Common;
using Mvc2App.Framework.Search;
using Mvc2App.InputModels.Peple;
using Mvc2App.Models;
using Mvc2App.Repositories;
using Mvc2App.Repositories.Abstractions;
using Mvc2App.Services.Abstractions;
using Mvc2App.ViewModels.Peple;

namespace Mvc2App.Services
{  
    public class PepleService : IPepleService
    {
        private IPepleRepository _repository;
        public PepleService() : this(new PepleRepository())
        { 
        }

        public PepleService(IPepleRepository repository)
        {
            _repository = repository;
        }

        ...

        public PepleSearchViewModel Search(PepleSearchConditionModel condition)
        {
            var searcher = new PepleSearcher();
            return searcher.Search(condition);
        }
    }
}
検索フレームワークの用意
using Mvc2App.Common;
using Mvc2App.InputModels.Peple;
using Mvc2App.Models;
using Mvc2App.Repositories;
using Mvc2App.Repositories.Abstractions;
using Mvc2App.ViewModels.Peple;

namespace Mvc2App.Framework.Search
{
    public class PepleSearcher
    {
        IPepleRepository _repository;

        public PepleSearcher() : this(new PepleRepository()) { }
        public PepleSearcher(IPepleRepository repository)
        {
            _repository = repository;
        }

        public PepleSearchViewModel Search(PepleSearchConditionModel condition) 
        {
            var data = GetAllData(condition);
            var helper = new SearchHelper<Person>(data, condition);

            var model = new PepleSearchViewModel() {
                data = helper.GetData(),
                page = helper.GetPage(),
                records = helper.GetRecords(),
                total = helper.GetTotal(),
                SysMessage = helper.GetTotalCountMessage()
            };            

            SetConditions(condition, model);           
            
            return model;
        }

        public Person[] GetAllData(PepleSearchConditionModel condition)
        {
            return _repository.Search(condition);
        }

        private static void SetConditions(PepleSearchConditionModel condition, PepleSearchViewModel model)
        {
            model.condition = new PepleSearchConditionModel();

            if (condition.Name != null)
            {
                model.condition.Name = condition.Name;
            }

            if (condition.Address != null)
            {
                model.condition.Address = condition.Address;
            }

            if (condition.PhoneNumber != null)
            {
                model.condition.PhoneNumber = condition.PhoneNumber;
            }

            if (condition.UpdatedBy != null)
            {
                model.condition.UpdatedBy = condition.UpdatedBy;
            }

            if (condition.UpdateDate != null)
            {
                model.condition.UpdateDate = condition.UpdateDate;
            }
        }
    }
}
using System;
using System.Linq;
using Mvc2App.InputModels;
using Mvc2App.ViewModels;
using Mvc2App.Common;

namespace Mvc2App.Framework.Search
{
    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 SearchResultViewModel<T> GetPageResult(T[] data, SearchConditionModelBase condition)
        {
            var searchResult = new SearchResultViewModel<T>();
            searchResult.data = GetDisplayData(data, condition.Page, condition.Rows);
            searchResult.page = GetPage(condition.Page);
            searchResult.total = GetTotal(data.Count(), condition.Rows);
            searchResult.records = data.Count();
            searchResult.SysMessage = string.Format(Resource.GetValue("MSG_001"), searchResult.records.ToString());

            return searchResult;
        }

        public T[] GetData()
        {
            return GetDisplayData(_data, _condition.Page, _condition.Rows);
        }

        public int GetPage()
        {
            return GetPage(_condition.Page);
        }

        public int GetTotal()
        {
            return GetTotal(_data.Count(), _condition.Rows);
        }

        public int GetRecords()
        {
            return _data.Count();
        }

        public string GetTotalCountMessage()
        {
            return string.Format(Resource.GetValue("MSG_001"), _data.Count().ToString());
        }

        #region helper methids

        private T[] GetDisplayData(T[] data, string page, string rows)
        {
            int p;
            int.TryParse(page, out p);

            int r;
            int.TryParse(rows, out r);

            return data.Skip((p - 1) * r).Take(r).ToArray();
        }

        private int GetPage(string page)
        {
            int p;
            int.TryParse(page, out p);
            return p;
        }

        private int GetTotal(int dataCount, string rows)
        {
            decimal r;
            decimal.TryParse(rows, out r);
            decimal c = Convert.ToDecimal(dataCount);

            if (r > 0)
            {
                return Convert.ToInt32(Math.Ceiling(c / r));
            }
            else
            {
                return 0;
            }
        }

        #endregion

    }
}
リポジトリインタフェースとリポジトリクラスの用意
using System.Collections.Generic;
using Mvc2App.InputModels.Peple;
using Mvc2App.Models;

namespace Mvc2App.Repositories.Abstractions
{
    public interface IPepleRepository
    {
        ...
        Person[] Search(PepleSearchConditionModel condition);
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using Mvc2App.InputModels.Peple;
using Mvc2App.Models;
using Mvc2App.Repositories.Abstractions;

namespace Mvc2App.Repositories
{
    public class PepleRepository : IPepleRepository
    {
        DevEntities dbContext = new DevEntities();

        public Person[] Search(PepleSearchConditionModel condition) 
        {
            var query = from x in dbContext.Peple
                        select x;

            if (!string.IsNullOrEmpty(condition.Name))
            {
                query = query.Where(q => q.Name.Contains(condition.Name));
            }

            if (!string.IsNullOrEmpty(condition.Address))
            {
                query = query.Where(q => q.Address.Contains(condition.Address));
            }

            if (!string.IsNullOrEmpty(condition.PhoneNumber))
            {
                query = query.Where(q => q.PhoneNumber.Contains(condition.PhoneNumber));
            }

            if (!string.IsNullOrEmpty(condition.UpdatedBy))
            {
                query = query.Where(q => q.UpdatedBy.Contains(condition.UpdatedBy));
            }

            if (!string.IsNullOrEmpty(condition.UpdateDate))
            {
                DateTime fromDate;
                DateTime.TryParse(condition.UpdateDate, out fromDate);
                query = query.Where(q => q.UpdateDate >= fromDate);
                DateTime toDate = fromDate.AddDays(1);
                query = query.Where(q => q.UpdateDate <= toDate);
            }

            var data = query.OrderBy(q => q.ID).ToArray();

            return data;
        }
    }
}

ビューモデルの用意

namespace Mvc2App.ViewModels
{
    public class SearchResultViewModel<T> : ViewModelBase
    {
        public int page { get; set; }
        public int total { get; set; }
        public int records { get; set; }
        public T[] data { get; set; }
    }
}
using Mvc2App.InputModels.Peple;
using Mvc2App.Models;

namespace Mvc2App.ViewModels.Peple
{
    public class PepleSearchViewModel : SearchResultViewModel<Person>
    {
        public PepleSearchConditionModel condition { get; set; }
    }
}
入力モデルの用意
namespace Mvc2App.InputModels
{
    public class SearchConditionModelBase
    {
        public SearchConditionModelBase()
        {
            //そのうち使う予定
            Sidx = "";
            //そのうち使う予定
            Sord = "asc";
            Page = "1";
            Rows = "3";
        }

        public string Sidx { get; set; }
        public string Sord { get; set; }
        public string Page { get; set; }
        public string Rows { get; set; }
    }
}
namespace Mvc2App.InputModels.Peple
{
    public class PepleSearchConditionModel : SearchConditionModelBase
    {
        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; }
    }
}
ビューとCSSの用意
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
Inherits="System.Web.Mvc.ViewPage<Mvc2App.ViewModels.Peple.PepleSearchViewModel>" %>

<%@ Import Namespace="Mvc2App.Extensions.Html"  %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
	Search
</asp:Content>

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

    <h2>Search</h2>
    <div id="sysMessage"><%: Html.DisplayFor(model => model.SysMessage)%></div>
    <% using (Html.BeginForm()) {%>
    <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 class="field-box">
            <p>
                <input type="submit" value="Search" />
            </p>
        </div>
    </fieldset>
    <% } %>
    <div class="grid">
    <table>
        <tr>
            <th></th>
            <th>ID</th>
            <th>Name</th>
            <th>Address</th>
            <th>PhoneNumber</th>
            <th>UpdatedBy</th>
            <th>UpdateDate</th>
        </tr>
    <% if (Model != null) {  %>
        <% 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>
    <% if (Model != null) { %>
       <%: Html.Pager("pager", Model.page, Model.total, Model.records, "/Peple/Search")%>
     <%  }%>
    </div>
    
    <p>
        <%: Html.ActionLink("Create New", "Create") %>
    </p>

</asp:Content>

CSS苦手(/ω\)

div.field-box:last-child 
{
    border-right: none;
}

div.field-box
{
    display: inline-block;
    border-right: 1px solid #eee;
    vertical-align: top;
    margin-right: 8px;
    width: 400px;
}

div.field > span.field-label
{
    display: inline-block;
    width: 128px;
    color: #696969;    
}

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

.pager a:link,
.pager a:active,
.pager a:visited
{
    text-decoration:none;
}

.grid table
{
    margin:auto;
}

.grid
{
    width:700px;    
}
HTMLヘルパーの用意

今までは標準で組み込まれているHTMLヘルパーのみを使用してきましたが、ページングのコードをビューに書き込むと読みにくくなるためHTMLヘルパーを作成しました。

using System.Text;
using System.Web.Mvc;

namespace Mvc2App.Extensions.Html
{
    public static class PagerHelper
    {

        public static MvcHtmlString Pager(this HtmlHelper helper, string cls, int page, int total, int record, string baseUrl)
        {
            var pager = new StringBuilder();
            pager.AppendFormat("<div>");
            pager.AppendFormat("<table class={0}>", cls);
            pager.AppendFormat("<tr>");

            pager.AppendFormat("<td>");

            string prevDisabled = "";
            string prevPage = (page - 1).ToString();
            if (page == 1) 
            {
                prevDisabled = "disabled=disabled";
                prevPage = page.ToString() + "#";
            }
            pager.AppendFormat("<a href='{0}?pageIndex={1}' {3}>{2} |</a>", baseUrl, prevPage, "≪ PREV", prevDisabled);

            for (int i = 0; i < total; i++)
            {
                int urlPage = i + 1;
                if (page == urlPage) {
                    pager.AppendFormat("<b>");
                    pager.AppendFormat("<a href='{0}?pageIndex={1}'> {1} </a>", baseUrl, (i + 1).ToString());
                    pager.AppendFormat("</b>");
                } else {
                    pager.AppendFormat("<a href='{0}?pageIndex={1}'> {1} </a>", baseUrl, (i + 1).ToString());
                }

                pager.AppendFormat("|");
            }

            string nextDisabled = "";
            string nextPage = (page + 1).ToString();
            if (page == total)
            {
                nextDisabled = "disabled=disabled";
                nextPage = page.ToString() + "#";
            }
            pager.AppendFormat("<a href='{0}?pageIndex={1}' {3}> {2}</a>", baseUrl, nextPage, "NEXT ≫", nextDisabled);

            pager.AppendFormat("</td>");

            pager.AppendFormat("</tr>");
            pager.AppendFormat("</table>");
            pager.AppendFormat("</div>");
            return MvcHtmlString.Create(pager.ToString());

        }

    }
}

実行結果

初期表示は一覧表示しません。


f:id:sh_yoshida:20141118200250p:plain

検索ボタンを押下すると一覧表示します。
条件欄が空の場合は全件検索。


f:id:sh_yoshida:20141118200308p:plain

検索条件による絞り込みとページング機能*1もあるよ(・ω・)


f:id:sh_yoshida:20141118200318p:plain

*1:ページ数分リンクが出来ちゃうのが玉に瑕