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

## あらまし
筆者は業務中、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>
```

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

[^1]: 特に`id`の被りはhtmlの規則違反のため致命的です。

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

### モデルが`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>`を指定した場合と同じく、`id`や`name`の値に`item`が頭につきます。
直接`Model[i]`でリストを作成した場合、ようやく2列目以降にもデータ属性が追加されますが、`id`が生成されず、`name="[0].Hoge"`となり、すごく怪しい雰囲気です。
モデル自体を`item`という変数に移し替えてみたらうまく行きそうな気がしますが、これも`id="CS___8__locals1_item_0__Hoge"` `name="CS$<>8__locals1.item[0].Hoge"`と変換されてしまう[^2]ので使い物になりません。

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

### こんな`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を使うほうが設定の管理がしやすいですね。