あらまし
筆者は業務中、ASP.NET MVCを使い、リストをバインドした入力ビューを作成していました。コードとしてはこんなものです。
@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が危険です。香ばしいにおいがプンプンします。
<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列目以降省略されてしまうため、このままでは取り返しがつかなくなるでしょう。
ここで、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ので使い物になりません。
こんなname
で大丈夫か?
コントローラー「大丈夫だ、問題ない。IList<Product.Models.FooModel>
でModel[i]
を頼む」
@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
が少々変でも値を受け取ってもらえます。
[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を使うほうが設定の管理がしやすいですね。