3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ASP.NET リスト更新でハマった点

Last updated at Posted at 2021-06-28

あらまし

筆者は業務中、ASP.NET MVCを使い、リストをバインドした入力ビューを作成していました。コードとしてはこんなものです。

Edit.cshtml
@model IEnumerable<Product.Models.FooModel>
@using (Html.BeginForm())
{

@{
    ViewBag.Title = "リストバインド入力";
}

<h2>@ViewBag.Title</h2>

 @using (Html.BeginForm())
{
   <table class="table">
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Hoge)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Piyo)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Fuga)
            </th>
        </tr>

        @{ 
            var count = Model.Count();
        }

        @for (var i = 0; i < count; i++)
        {
            var item = Model.ElementAt(i);
            <tr>
                <td>
                    @Html.TextBoxFor(modelItem => item.Hoge)
                </td>
                <td>
                    @Html.TextBoxFor(modelItem => item.Piyo)
                </td>
                <td>
                    @Html.TextBoxFor(modelItem => item.Fuga)
                </td>
            </tr>
        }
    </table>
    <button type="submit">新規作成</button>
}

さて、これを実際に動かしてみると、データは確かに表示されるのですが、HTMLが危険です。香ばしいにおいがプンプンします。

View(抜粋).html
<tr>
    <td>
        <input data-val="true" id="item_Hoge" name="item.Hoge" type="text" value="hoge1">
    </td>
    <td>
        <input data-val="true" id="item_Piyo" name="item.Piyo" type="text" value="Piyo1">
    </td>
    <td>
        <input data-val="true" id="item_Fuga" name="item.Fuga" type="text" value="Fuga1">
    </td>
</tr>
<tr>
    <td>
        <input id="item_Hoge" name="item.Hoge" type="text" value="hoge2">
    </td>
    <td>
        <input id="item_Piyo" name="item.Piyo" type="text" value="Piyo2">
    </td>
    <td>
        <input id="item_Fuga" name="item.Fuga" type="text" value="Fuga2">
    </td>
</tr>

idnameが丸被り1のため、更新のとき、Hoge1の更新なのかHoge2の更新なのか区別がつきません。
また、データ属性が2列目以降省略されてしまうため、このままでは取り返しがつかなくなるでしょう。

ここで、Modelの各要素をitem変数経由ではなく、直接Model.ElementAt(i)経由で読み出しても、idnameHogeとかになるだけでむしろ悪化しています。

モデルがIEnumerable<T>な点に問題があるのでは?

そこで、モデルの型をIList<Product.Models.FooModel>に変えてみたらどうなるでしょうか。まず、この変更の際にしておかなければならないことがあります。それは、@Html.DisplayNameForの引数のラムダ式を(model => model.firstOrDefault().Hoge)と変更することです。なぜIEnumerable<Product.Models.FooModel>だと@Html.DisplayNameForの引数がProduct.Models.FooModelと解釈してくれるのかは不明です。
item変数に値を取る形式の場合、IEnumerable<Product.Models.FooModel>を指定した場合と同じく、idnameの値にitemが頭につきます。
直接Model[i]でリストを作成した場合、ようやく2列目以降にもデータ属性が追加されますが、idが生成されず、name="[0].Hoge"となり、すごく怪しい雰囲気です。
モデル自体をitemという変数に移し替えてみたらうまく行きそうな気がしますが、これもid="CS___8__locals1_item_0__Hoge" name="CS$<>8__locals1.item[0].Hoge"と変換されてしまう2ので使い物になりません。

こんなnameで大丈夫か?

コントローラー「大丈夫だ、問題ない。IList<Product.Models.FooModel>Model[i]を頼む」

Edit(修正版).cshtml
@model IList<Product.Models.FooModel>
@using (Html.BeginForm())
{

@{
    ViewBag.Title = "リストバインド入力";
}

<h2>@ViewBag.Title</h2>

 @using (Html.BeginForm())
{
   <table class="table">
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Hoge)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Piyo)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Fuga)
            </th>
        </tr>

        @{ 
            var count = Model.Count();
        }

        @for (var i = 0; i < count; i++)
        {
            <tr>
                <td>
                    @Html.TextBoxFor(modelItem => Model[i].Hoge)
                </td>
                <td>
                    @Html.TextBoxFor(modelItem => Model[i].Piyo)
                </td>
                <td>
                    @Html.TextBoxFor(modelItem => Model[i].Fuga)
                </td>
            </tr>
        }
    </table>
    <button type="submit">新規作成</button>
}

コントローラー側で次のように設定すれば、nameが少々変でも値を受け取ってもらえます。

BarController.cs
[HttpPost]
public ActionResult Edit(IList<Product.Models.FooModel> model)
{
    if(ModelState.IsValid)
    {
        using(var db = new DefaultDbConnection())
        {
            ForEach (var item in model)
            {
                var foo = new Foo() {
                    Hoge = item.Hoge,
                    Piyo = item.Piyo,
                    Fuga = item.Fuga
                }
                db.Entry(foo).State = EntityState.Modified;
            }
            db.SaveChanges();
        }
    }
    return View(model);
}

ここで、FooクラスはEntity FrameworkのFooModelに対応するDB上のモデルです。name属性が意味不明なものでも、各値の区別さえできればリストを受け取ることができます。

ただ、もしかしたら素直にモデル定義の段階でIList<T>を内部に含むViewModelを作ってしまった方がいろいろと分かりやすい気もしなくは無いです。

余談

何らかの手段(ajaxやhtmlの<template>要素+JavaScript)を使用して動的に要素を追加する場合、name[n].hoge(ここでnは任意の整数)にしましょう。そうしないと追加された要素が同一のモデルであるとASP.NET MVCが認識してくれません。

この辺りの面倒も、独自ViewModelを使うほうが設定の管理がしやすいですね。

  1. 特にidの被りはhtmlの規則違反のため致命的です。

  2. nameがジェネリック型の定義に使われる<>を含んでいることからわかるように、これはC#内部で使われる値と思われるので、環境などによりitem前の名前は変わると思われます。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?