发布Collection和ModelState

时间:2012-06-18 19:21:42

标签: c# asp.net-mvc

我的MVC应用程序中存在一个问题,我不确定如何解决,或者我是否以错误的方式处理它。

我有一个控制器/视图,它显示一个带有复选框的网格中的项目列表,当项目发布到我的控制器时,我想根据传入的id从我的数据库中删除行。 / p>

视图看起来像这样:

@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,按最近排序),然后单击Submit。页面上的验证失败,我将视图返回给用户。网格中现在有三行(3,2,1)。 MVC将在FIRST项目上设置复选框(现在ID为3)。如果用户没有检查这些数据,那么他们可能会删除错误的东西。

有关如何修复此方案或我应该做什么的任何想法? 有没有人知道如何

2 个答案:

答案 0 :(得分: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操作,选择要删除的第一个项目,然后单击“删除”按钮。以下是视图在提交表单之前的样子:

enter image description here

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

enter image description here

查看错误的项目是如何预选的?

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

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

那可以做些什么呢?我们应该阅读史蒂芬桑德森的博客文章: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强类型助手生成的顺序索引,并使用额外的隐藏字段在每次迭代时设置此索引。


这就是说,只有当你需要在Delete操作中从数据库中获取新数据时才会出现问题。如果您依赖模型活页夹进行补水,则根本不存在任何问题(除非出现模型错误,否则您将使用旧数据显示视图 - &gt;这毕竟不是问题):

[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");
}

答案 1 :(得分:0)

此问题的常见解决方案是使用Post-Redirect-Get pattern

你可以找到MVC here的代码示例的解释(以及一堆其他好的MVC技巧)。向下滚动到列表中的项目13以获取PRG说明。