我的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)。如果用户没有检查这些数据,那么他们可能会删除错误的东西。
有关如何修复此方案或我应该做什么的任何想法? 有没有人知道如何
答案 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
操作,选择要删除的第一个项目,然后单击“删除”按钮。以下是视图在提交表单之前的样子:
调用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操作中从数据库中获取新数据时才会出现问题。如果您依赖模型活页夹进行补水,则根本不存在任何问题(除非出现模型错误,否则您将使用旧数据显示视图 - &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说明。