4

MVC アプリケーションに問題があり、解決方法がわからない、または間違った方法で問題を解決しようとしています。

チェックボックス付きのグリッドにアイテムのリストを表示するコントローラー/ビューがあり、アイテムがコントローラーに投稿されると、渡された ID に基づいてデータベースから行を削除したいと考えています。

ビューは次のようになります。

@for(int index = 0; index < Model.Items.Length; index++)
{
    <td>
        @Html.HiddenFor(m => m[index].Id)
        @Html.CheckBoxFor(m => m[index].Delete)
    </td>
}

私のコントローラーは値を受け入れます:

[HttpPost]
public ActionResult Delete(DeleteItemsModel model)
{
    if( !ModelState.IsValid )
    {
        // ...
    }

    foreach( var id in model.Items.Where(i => i.Delete))
        repo.Delete(id);
}

このシナリオは正常に機能します。アイテムは、ID と削除するかどうかのフラグを使用して正しく投稿されており、適切に削除されます。私が抱えている問題は、ページが検証に失敗したときです。データベースからアイテムを再度取得し、データをビューに送り返す必要があります。

if( !ModelState.IsValid )
{
    var order = repo.GetOrder(id);

    // map
    return View(Mapper.Map<Order, OrderModel>(order));
}

ユーザーが削除するアイテムのリストを取得してから [送信] をクリックするまでの間に、新しいアイテムが追加されている可能性があります。データをプルしてビューに送り返すと、リストに新しいアイテムが表示される可能性があります。

問題の例:
ページで HTTP GET を実行すると、ID が 2 と 1 の 2 つのアイテムがグリッドに表示されます。最初の行 (ID 2、最新順に並べ替え) を選択し、[送信] をクリックします。ページの検証が失敗し、ユーザーにビューを返します。グリッドには 3 つの行 (3、2、1) があります。MVC では、FIRST アイテムにチェックボックスが追加されます (ID は現在 3 です)。ユーザーがこのデータをチェックしない場合、間違ったものを削除している可能性があります。

このシナリオを修正する方法、または代わりに何をすべきかについてのアイデアはありますか? 誰でも方法について何か考えがありますか

4

2 に答える 2

3

興味深い質問です。最初に簡単な例で問題を説明しましょう。他の回答から判断すると、ここでの問題が何であるかを誰もが理解しているかどうかわからないからです。

次のモデルがあるとします。

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");
}
于 2012-06-19T16:13:02.957 に答える
0

この問題の一般的な解決策は、Post-Redirect-Getパターンを使用することです。

MVCのコードサンプルの説明はここにあります(他の優れたMVCのヒントもたくさんあります)。PRGの説明については、リストの項目13まで下にスクロールしてください。

于 2012-06-18T19:32:41.680 に答える