发布Collection和ModelState [英] Posting a Collection and ModelState

查看:49
本文介绍了发布Collection和ModelState的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的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,按最新顺序排序),然后单击提交".页面上的验证失败,我将视图返回给用户.现在,网格中有三行(3、2、1). MVC将在FIRST项上选中该复选框(现在的ID为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>
}

现在,此问题已解决.还是吗?您是否看到了视图现在所代表的可怕的混乱局面?我们已经解决了一个问题,但是我们引入了一些绝对可恶的观点.我不认识你,但是当我看着这个我想呕吐.

那该怎么办?我们应该阅读史蒂文·桑德森的博客文章: 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操作从数据库中获取新数据时,问题才会出现.如果您依靠模型绑定器进行补水,则根本不会有任何问题(除非存在模型错误,否则您将使用旧数据显示视图->毕竟这可能不是问题):

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

I have a problem in my MVC application that I am not sure how to solve or if I am going about it in the wrong way.

I have a controller/view that displays a list of items in a grid with a checkbox and when the items are posted to my controller, I would like to delete rows from my database based on the id's that were passed in.

The view looks something like this:

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

My controller accepts the values:

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

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

This scenario works fine. Items are posting correctly with the id and a flag to delete or not and they are deleted properly. The problem I am having is when my page fails validation. I need to get the items from the database again and send the data back to the view:

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

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

In the time between the user getting a list of items to delete and them clicking Submit, it is possible that new items could have been added. Now when I pull the data and send it back to the view, there could be new items in the list.

Example of the problem:
I do an HTTP GET on my page and there are two items in my grid with Id of 2 and 1. I select the first row (Id 2, sorted by most recent) and then I click Submit. Validation on the page fails and I return the view to the user. There are now three rows in the grid (3, 2, 1). MVC will have the checkbox on the FIRST item (with an id of 3 now). If the user doesn't check this data then they could be potentially deleting the wrong thing.

Any ideas on how to fix this scenario or what should I do instead? Does anyone have any idea on how to

解决方案

Interesting question. Let's first illustrate the problem with a simple example because judging from the other answers I am not sure that everyone understood what the problem is here.

Suppose the following model:

public class MyViewModel
{
    public int Id { get; set; }
    public bool Delete { get; set; }
}

the following controller:

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

and a view:

@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>
}

Here's what will happen:

The user navigates to the Index action, selects the first item to delete and clicks on the Delete button. Here's how the view looks like before he submits the form:

The Delete action is invoked and when the view is rendered once again (because there was some validation error) the user is presented with the following:

See how the wrong item is preselected?

Why does this happen? Because HTML helpers use the ModelState value in priority when binding instead of the model value and this is by design.

So how to solve this problem? By reading the following blog post by Phil Haack: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx

In his blog post he talks about Non-Sequential Indices and gives the following example:

<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>

See how we are no longer using incremental indexes for the names of the input buttons?

How do we apply this to our example?

Like this:

@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>
}

Now the problem is fixed. Or is it? Have you seen the horrible mess that the view now represents? We have fixed one problem but we have introduced something absolutely abominable in the view. I don't know about you but when I look at this I want to vomit.

So what could be done? We should read Steven Sanderson's blog post: http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/ in which he presents a very interesting custom Html.BeginCollectionItem helper which is used like this:

<div class="editorRow">
    <% using(Html.BeginCollectionItem("gifts")) { %>
        Item: <%= Html.TextBoxFor(x => x.Name) %>
        Value: $<%= Html.TextBoxFor(x => x.Price, new { size = 4 }) %>
    <% } %>
</div>

Notice how the form elements are wrapped in this helper?

What does this helper do? It replaces the sequential indexes generated by the strongly typed helpers by Guids and uses an additional hidden field to set this index at each iteration.


This being said, the problem exhibits only if you need to get fresh data from your database in the Delete action. If you rely on the model binder to rehydrate there won't be any issues at all (except that if there is a model error you will show the view with old data -> which probably is not that problematic after all):

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

这篇关于发布Collection和ModelState的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆