2012/09/26

【ASP.Net】MVC4で何か作ってみる その6(最終回)【MVC】

 前回の続きです。

 前回はjqueryのDataTablesプラグインで簡単に並べ替えとフィルターを実装することができました。今回はリストをドラッグアンドドロップで並べ替えしてみたいと思います。

 いやぁー、ぶっちゃけ今回が一番苦労しました。ドラッグアンドドロップで並べ替えたまでは良かったんですが、その後のデータベースへの保存にかなり手こずりました。正直、諦めようかと思ったくらいでしたが、何とか出来てよかったです。

 では早速、最初のモデル作成とコントローラー作成は今まで同じなのでちゃちゃっとやっちゃいます。

社員順序モデル
public class EmployeeSort
{
 
    /// <summary>
    /// 社員ID
    /// </summary>
    public int Id { get; set; }
 
    /// <summary>
    /// ソート順
    /// </summary>
    public int SortNumber { get; set; }
 
    /// <summary>
    /// 社員名
    /// </summary>
    public string EmployeeName { get; set; }
 
}

Indexメソッド(コントローラー)
public ActionResult Index()
{
    var resultList = new List<EmployeeSort>();
 
    using (var context = new MvcTestContext())
    {
        var items = from p in context.Employees
                    orderby p.SortNumber
                    select p;
 
        foreach (var item in items)
        {
            resultList.Add(new EmployeeSort
                {
                    Id= item.EmployeeId,
                    SortNumber = (int)item.SortNumber,
                    EmployeeName = item.EmployeeName
                });
        }
    }
 
    return View(resultList);
}

 ここまでは特に問題ないでしょう。次にビューですが、今回はリスト上でのドラッグアンドドロップで並べ替えを実現するためにjqueryUIの「Sortableプラグイン」を利用します。細かな使い方はこちらを見てもらうとして、ビューで使用する準備をしましょう。

Sortableプラグイン使用準備(ビューに追加)
<script src="~/Scripts/jquery-ui-1.8.23.min.js"></script>
<link href="~/Content/themes/smoothness/jquery-ui-1.8.23.custom.css" rel="stylesheet" />
 
<script type="text/javascript">
    $(function () {
        $('#sortable').sortable();
 
    });
</script>

 ついでだったんでthemeも突っ込んどきました。これで<li>タグをドラッグアンドドロップで並べ替えできます。後は並べ替えた後、どう保存するかってとこなんですが、とりあえず単純に保存ボタンを押したら並べ替えた順に社員テーブルのIdを受け取ってテーブルを更新しちゃえばよいかと考えました。というわけで一端ビューは置いといて、Post時のIndexメソッドです。

Post時のIndexメソッド
[HttpPost]
public ActionResult Index(int[] id)
{
    using (var context = new MvcTestContext())
    {
        for (var i = 0; i < id.Length; i++)
        {
            var overlap = context.Employees.Find(id[i]);
 
            if (overlap != null)
            {
                overlap.SortNumber = i + 1;
            }
        }
 
        context.SaveChanges();
 
        return RedirectToAction("Index");
    }
}

 コードはいたってシンプルです。これで行けるはず。…とこの時点では余裕で考えてて、何気なしにビューの続きも作成しました。

ビュー全景
@model IEnumerable<MVCTestProject.Models.EmployeeSort>
 
@{
    ViewBag.Title = "Sort";
    Layout = "~/Views/Shared/_LayoutPage.cshtml";
}
 
<script src="~/Scripts/jquery-ui-1.8.23.min.js"></script>
<link href="~/Content/themes/smoothness/jquery-ui-1.8.23.custom.css" rel="stylesheet" />
 
<script type="text/javascript">
    $(function () {
        $('#sortable').sortable();
 
    });
</script>
 
<h2>並べ替え</h2>
 
<ul id="sortable">
@foreach (var item in Model)
{
    <li class="ui-state-default" style="padding: 5px; width: 200px">
        @Html.HiddenFor(model => item.Id)
        @Html.DisplayFor(model => item.SortNumber)&nbsp;:&nbsp;@Html.DisplayFor(model=>item.EmployeeName)
    </li>
}
</ul>
 
@using (Html.BeginForm()) {
        <input type="submit" value="保存" />
}

 見ての通りモデルをリスト化し、最後の<input>タグでPostが発生するようにしています。これで「おぉー、出来た出来た」と早速、実行してみました。

実行結果(並び替え)
image

 ちょっと地味ですがプラグインは正常に動作し、ドラッグアンドドロップで並べ替えが効いています。次は保存ボタンをクリックしてみます。

実行結果(保存)
image

 はい、こけました。お久しぶりの「NullReferenceException」さんです。引数の「id」がNullだとおっしゃってます。どうやらビューからコントローラーに引数が渡ってない模様。…ここから長い旅路が始まりました。

 何故でしょう?「配列は渡せません」なんてことはないはずなんですが…。基本的にMVCではコントローラに引数を渡す際に、自動的に引数と同名のデータを収集・変換してくれてるはずです。それが渡ってないってことは収集されてないのか?ってな具合に試行錯誤しました。まぁ、1から試したこと書いてたらキリが無いので、途中経過はすっとばしますが、1つ目の間違いは「Html.BeginForm()」の位置でした。この辺、今までPHPとか全然構ってない人間なのでピンときてませんでしたが、対象となるデータが<form>タグに入ってなかったら送れないみたいです。ってこう書くと当たり前じゃねーかって気もしますね…。では修正です。

ビューを修正(部分抜粋)
@using (Html.BeginForm()) {
<ul id="sortable">
@foreach (var item in Model)
{
    <li class="ui-state-default" style="padding: 5px; width: 200px">
        @Html.HiddenFor(model => item.Id)
        @Html.DisplayFor(model => item.SortNumber)&nbsp;:&nbsp;@Html.DisplayFor(model=>item.EmployeeName)
    </li>
}
</ul>
 
    <input type="submit" value="保存" />
}

 「Html.BigenForm()」を<ul>タグの前に持ってきました。これでどうだって感じで実行してみます。

実行結果
image

 …駄目でした。でも「Html.BeginForm()」位置の修正に関しては間違ってないはずなのでこのままにしときます。それにしても何で「id」が収集されんのだろうか…とPage Inspectorの<li>タグをじっくり見直してみました。

Page Inspector(部分抜粋)
<li class="ui-state-default" style="padding: 5px; width: 200px;">
<input name="item.Id" id="item_Id" type="hidden" data-val-required="Id フィールドが必要です。" data-val-number="フィールド Id には数字を指定してください。" data-val="true" value="83"></input>
</input>
        1 : 0子
</li>

 「!!」っと、ここでようやく気がつきました。…今まで自分は何を見てたんでしょう。<input>タグのname属性にご注目。「name=”item.Id”」となってるんですよ。上にも書きましたがMVCはコントローラーの引数名でデータを収集するってことは「item.Id ≠ id」ってなわけで…。自分、name属性なんて指定せんでも勝手にやってくれるだろうと思ってたんです(ビュー側のモデルがIEnumerableやIListでなく単体の場合はそのままでOKっぽいです)…。これに気がつくのに数時間…。

 何はともあれ原因っぽいのは分かったんで、ビューをまたもや修正します。

ビューの再修正(部分抜粋)
@using (Html.BeginForm()) {
<ul id="sortable">
@foreach (var item in Model)
{
    <li class="ui-state-default" style="padding: 5px; width: 200px">
        @Html.Hidden("id", item.Id)
        @Html.DisplayFor(model => item.SortNumber)&nbsp;:&nbsp;@Html.DisplayFor(model=>item.EmployeeName)
    </li>
}
</ul>
 
    <input type="submit" value="保存" />
}

 ちょっと違いが分かりにくいですが、「@Html.HiddenFor」から「@Html.Hidden」に変更し、「id」という名前をつけてます。最初はname属性の付け方が分からなくてまたもや時間を喰いました。「@Html.HIddenFor」の方、MSDN(InputExtensions.HiddenFor(TModel, TProperty) メソッド (HtmlHelper(TModel), Expression(Func(TModel, TProperty)), IDictionary(String, Object)) (System.Web.Mvc.Html))にはHtml属性を定義できるとありますが、自分はどうしてもname属性をつけることができませんでした(他の属性はできました)。

実行結果
image

 ちょっと分かりにくいですが、並べ替えを保存に成功した後です。結果自体は地味でしたがようやくドラッグアンドドロップで並べ替えを実装することができました。…わりとまじめにしんどかったです。

 今回で「MVC4で何か作ってみる」は終了です。作ってみた感想としてはやっぱりまだまだ関係資料が少ないなぁと感じました。MSDNもまだほとんどがMVC3のヘルプを見ろって感じです(ぶっちゃけ見ても分かりづらいですが)。それでも個人的にはRazorの方がASPXより好みだし、jqueryとの連携もやっぱり面白いです。今回は見た目の方はやっつけで済ましましたが、もっとリッチなインターフェースを作成できそうな気がします(jqueryの勉強も必要ですが…)。








デル株式会社
pagetop