興味深い質問です。最初に簡単な例で問題を説明しましょう。他の回答から判断すると、ここでの問題が何であるかを誰もが理解しているかどうかわからないからです。
次のモデルがあるとします。
public class MyViewModel
{
public int Id { get; set; }
public bool Delete { get; set; }
}
次のコントローラー:
public class HomeController : Controller
{
public ActionResult Index()
{
// Initially we have 2 items in the database
var model = new[]
{
new MyViewModel { Id = 2 },
new MyViewModel { Id = 1 }
};
return View(model);
}
[HttpDelete]
public ActionResult Index(MyViewModel[] model)
{
// simulate a validation error
ModelState.AddModelError("", "some error occured");
if (!ModelState.IsValid)
{
// We refetch the items from the database except that
// a new item was added in the beginning by some other user
// in between
var newModel = new[]
{
new MyViewModel { Id = 3 },
new MyViewModel { Id = 2 },
new MyViewModel { Id = 1 }
};
return View(newModel);
}
// TODO: here we do the actual delete
return RedirectToAction("Index");
}
}
とビュー:
@model MyViewModel[]
@Html.ValidationSummary()
@using (Html.BeginForm())
{
@Html.HttpMethodOverride(HttpVerbs.Delete)
for (int i = 0; i < Model.Length; i++)
{
<div>
@Html.HiddenFor(m => m[i].Id)
@Html.CheckBoxFor(m => m[i].Delete)
@Model[i].Id
</div>
}
<button type="submit">Delete</button>
}
何が起こるかは次のとおりです。
ユーザーはIndex
アクションに移動し、削除する最初のアイテムを選択して、[削除] ボタンをクリックします。フォームを送信する前のビューは次のようになります。
削除アクションが呼び出され、ビューが再度レンダリングされると (検証エラーがあったため)、ユーザーには次のように表示されます。
間違ったアイテムが事前に選択されているのがわかりますか?
なぜこれが起こるのですか?HTML ヘルパーは、バインド時にモデル値ではなく ModelState 値を優先的に使用するため、これは設計によるものです。
では、この問題を解決するにはどうすればよいでしょうか。Phil Haack による次のブログ投稿を読むことにより: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
彼のブログ投稿では、ノンシーケンシャル インデックスについて話し、次の例を挙げています。
<form method="post" action="/Home/Create">
<input type="hidden" name="products.Index" value="cold" />
<input type="text" name="products[cold].Name" value="Beer" />
<input type="text" name="products[cold].Price" value="7.32" />
<input type="hidden" name="products.Index" value="123" />
<input type="text" name="products[123].Name" value="Chips" />
<input type="text" name="products[123].Price" value="2.23" />
<input type="hidden" name="products.Index" value="caliente" />
<input type="text" name="products[caliente].Name" value="Salsa" />
<input type="text" name="products[caliente].Price" value="1.23" />
<input type="submit" />
</form>
入力ボタンの名前にインクリメンタル インデックスを使用しなくなったことがわかりますか?
これを例にどのように適用しますか?
このような:
@model MyViewModel[]
@Html.ValidationSummary()
@using (Html.BeginForm())
{
@Html.HttpMethodOverride(HttpVerbs.Delete)
for (int i = 0; i < Model.Length; i++)
{
<div>
@Html.Hidden("index", Model[i].Id)
@Html.Hidden("[" + Model[i].Id + "].Id", Model[i].Id)
@Html.CheckBox("[" + Model[i].Id + "].Delete", Model[i].Delete)
@Model[i].Id
</div>
}
<button type="submit">Delete</button>
}
これで問題は修正されました。またはそれは?ビューが現在表している恐ろしい混乱を見たことがありますか? 1 つの問題を修正しましたが、ビューに絶対に忌まわしいものを導入しました。あなたのことはわかりませんが、これを見ると吐き気がします。
では、何ができるでしょうか?Steven Sanderson のブログ投稿を読む必要があります: http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/Html.BeginCollectionItem
次のように使用されるヘルパー:
<div class="editorRow">
<% using(Html.BeginCollectionItem("gifts")) { %>
Item: <%= Html.TextBoxFor(x => x.Name) %>
Value: $<%= Html.TextBoxFor(x => x.Price, new { size = 4 }) %>
<% } %>
</div>
このヘルパーでフォーム要素がどのようにラップされているかに注目してください。
このヘルパーは何をしますか? 厳密に型指定されたヘルパーによって生成されたシーケンシャル インデックスを Guid に置き換え、追加の非表示フィールドを使用して各反復でこのインデックスを設定します。
そうは言っても、削除アクションでデータベースから新しいデータを取得する必要がある場合にのみ、問題が発生します。モデル バインダーを使用して再水和する場合、問題はまったくありません (ただし、モデル エラーが発生した場合は、古いデータを含むビューが表示されます -> これはおそらくそれほど問題ではありません)。
[HttpDelete]
public ActionResult Index(MyViewModel[] model)
{
// simulate a validation error
ModelState.AddModelError("", "some error occured");
if (!ModelState.IsValid)
{
return View(model);
}
// TODO: here we do the actual delete
return RedirectToAction("Index");
}