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。我选择第一行(ID 2,按最新排序),然后单击提交。页面验证失败,我将视图返回给用户。现在网格中有三行 (3, 2, 1)。MVC 将在第一项上具有复选框(现在 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操作,选择要删除的第一个项目并单击“删除”按钮。这是他提交表单之前的视图:

在此处输入图像描述

调用 Delete 操作,当再次呈现视图时(因为存在一些验证错误),用户将看到以下内容:

在此处输入图像描述

看看如何预选了错误的项目?

为什么会这样?因为 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>
}

现在问题已解决。或者是吗?您是否看到该视图现在所代表的可怕混乱?我们已经解决了一个问题,但我们在视图中引入了一些绝对可恶的东西。我不了解你,但当我看到这个我想呕吐。

那么可以做些什么呢?我们应该阅读 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>

注意表单元素是如何包装在这个助手中的?

这个助手是做什么的?它用 Guids 替换了强类型助手生成的顺序索引,并使用一个额外的隐藏字段在每次迭代时设置此索引。


话虽如此,仅当您需要在删除操作中从数据库中获取新数据时才会出现问题。如果您依靠模型粘合剂来补充水分,则根本不会有任何问题(除非存在模型错误,您将使用旧数据显示视图->这可能不是那么有问题):

[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 技巧)。向下滚动到列表中的第 13 项以获取 PRG 说明。

于 2012-06-18T19:32:41.680 回答