ASP.NET MVC 中的奇怪行为:从嵌套结构的列表中删除项目总是删除最后一个项目
场景
我有一个父/子模型(确切地说是一个小调查表和一个或多个联系人)。由于历史原因,所有这些都将在同一个表单上完成,因此用户将拥有一个针对父级和一个子级的表单,并且他们将点击一个按钮来添加更多子级。子级有一些标准字段,与父级相同,没什么花哨的。主要要求是,在所有数据都有效并设置之前,数据不得接触数据库,而我必须返回服务器来添加删除子项。
实现
很快就可以在 ASP.NET MVC 中实现此功能(使用 MVC 2 和 VS 2010)。我有两种模型,一种用于父母,一种用于孩子,并且只有一个控制器。控制器有一个 Create
方法,它是一个 get
方法,并获取一个默认视图,其中包含一个包含一个子级的全新父级视图。我对子模型使用编辑器模板,效果很好。
我有一个 HTML 表单,其中有“保存”和“添加子项”,每个表单都有“删除”按钮。由于这无法存储在数据库中,因此我将临时模型存储在表单本身中,并在浏览器和服务器之间来回传递。性能在这里并不是什么大问题,而是开发成本,因为这些形式有相当多——所以请不要因为建议替代方法而分心,尽管无论如何我很欣赏评论。
为了找出要删除的子项,我创建了临时 GUID Id 并将它们与该子项关联。这将进入删除按钮的 HTML 输入值(当您有多个操作和相同表单时通常使用的技巧)。
我已经禁用了缓存。
问题
请查看下面的代码片段。我已经调试了代码,并且看到始终传递正确的 GUID、从控制器的列表中删除正确的项目以及在模板中呈现正确的项目。但总是最后一个被删除!我通常点击第一个删除,然后可以看到最后一个被删除。我继续,第一个项目是最后一个被删除的项目。
控制器
public ActionResult Create()
{
EntryForm1 entryForm1 = new EntryForm1();
entryForm1.Children.Add(new Child("FILL ME", "FILL ME"){ TempId = Guid.NewGuid()});
return View("EntryForm1View", entryForm1);
}
[HttpPost]
public ActionResult Create(EntryForm1 form1, FormCollection collection, string add)
{
if (add == "add")
form1.Children.Add(new Child("FILL ME", "FILL ME") {TempId = Guid.NewGuid()});
var deletes = collection.AllKeys.Where(s => s.StartsWith("delete_"));
collection.Clear();
if (deletes.Count() > 0)
{
string delete = deletes.FirstOrDefault();
delete = delete.Replace("delete_", "");
Guid g = Guid.Parse(delete);
var Children = form1.Children.Where(x => x.TempId == g).ToArray();
foreach (Child child in Children)
{
form1.Children.Remove(child);
}
// HERE CORRECT ITEM IS DELETED, BELIEVE ME!!
}
if (ModelState.IsValid)
{
return Redirect("/");
}
return View("EntryForm1View", form1);
}
查看代码段
<% for (int i = 0; i < Model.Children.Count;i++ )
{%>
<h4> <%: Html.EditorFor(m=>m.Children[i])%></h4>
<%
}%>
<p>
<input type="submit" value="Create" name="add" />
<input type="submit" value="add" name="add" />
</p>
子编辑器模板代码段
<%: Html.HiddenFor(x=>x.TempId) %>
</span>
<input type="submit" name='delete_<%: Html.DisplayTextFor(m => m.TempId) %>' value="Delete" />
非常感谢您的时间和关注
更新
我被要求提供模型课程,我将按原样分享它们。 Entryform1
是父级,Somesing
是子级。
索辛公开课 {
public Somesing()
{
}
public Somesing(string o, string a) : this()
{
OneSing = o;
AnozerSing = a;
}
[StringLength(2)]
public string OneSing { get; set; }
[StringLength(2)]
public string AnozerSing { get; set; }
public Guid TempId { get; set; }
}
public class EntryForm1
{
public EntryForm1()
{
Sings = new List<Somesing>();
}
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public List<Somesing> Sings { get; set; }
}
Scenario
I have a parent/child model (to be exact a small questionnaire form and a one or more number of contacts). For historic reasons, all of this would have been done on the same form so user would have a form for the parent and one child and they would hit a button to add more children. Child has a few standard fields and the same with the parent, nothing fancy. Main requirement is that the data must not touch the database until all is valid and setup while I would have to go back to server for adding deleting children.
Implementation
It was very quick to get this working in ASP.NET MVC (using MVC 2 with VS 2010). I got two models, one for parent and one for the child and got only one controller. Controller has a Create
Method which is a get
and gets a default view with a fresh brand new parent containing one child. I use editor template for the child model which works nicely.
I have one HTML form which has a "save" and "add child" and I have "delete" button for each form. Since this cannot be stored in database, I store the temp model in the form itself and it goes back and forth between browser and server. Perfromance is not much of an issue here but the cost of development since there are quite a few of these forms - so please do not get distracted too much by suggesting an alternative approach although I appreciate comments anyway.
In order to find out which child to delete, I create temp GUID Ids and associate them with the child. This will go onto the HTML input's value for delete button (usual trick when you have multiple actions and the same form).
I have disabled caching.
Issue
Please have a look at the snippets below. I have debugged the code and I have seen always correct GUID being passed, correct item removed from the list in the controller and correct items being rendered in the template. BUT ALWAYS THE LAST ONE GETS DELETED!! I usually click the first delete and can see that the last gets deleted. I carry on and first item is the last being deleted.
Controller
public ActionResult Create()
{
EntryForm1 entryForm1 = new EntryForm1();
entryForm1.Children.Add(new Child("FILL ME", "FILL ME"){ TempId = Guid.NewGuid()});
return View("EntryForm1View", entryForm1);
}
[HttpPost]
public ActionResult Create(EntryForm1 form1, FormCollection collection, string add)
{
if (add == "add")
form1.Children.Add(new Child("FILL ME", "FILL ME") {TempId = Guid.NewGuid()});
var deletes = collection.AllKeys.Where(s => s.StartsWith("delete_"));
collection.Clear();
if (deletes.Count() > 0)
{
string delete = deletes.FirstOrDefault();
delete = delete.Replace("delete_", "");
Guid g = Guid.Parse(delete);
var Children = form1.Children.Where(x => x.TempId == g).ToArray();
foreach (Child child in Children)
{
form1.Children.Remove(child);
}
// HERE CORRECT ITEM IS DELETED, BELIEVE ME!!
}
if (ModelState.IsValid)
{
return Redirect("/");
}
return View("EntryForm1View", form1);
}
View snippet
<% for (int i = 0; i < Model.Children.Count;i++ )
{%>
<h4> <%: Html.EditorFor(m=>m.Children[i])%></h4>
<%
}%>
<p>
<input type="submit" value="Create" name="add" />
<input type="submit" value="add" name="add" />
</p>
Child Editor template snippet
<%: Html.HiddenFor(x=>x.TempId) %>
</span>
<input type="submit" name='delete_<%: Html.DisplayTextFor(m => m.TempId) %>' value="Delete" />
Many thanks for your time and attention
UPDATE
I was asked for model classes and I am sharing them as exactly as they are.Entryform1
is the parent and Somesing
is the child.
public class Somesing
{
public Somesing()
{
}
public Somesing(string o, string a) : this()
{
OneSing = o;
AnozerSing = a;
}
[StringLength(2)]
public string OneSing { get; set; }
[StringLength(2)]
public string AnozerSing { get; set; }
public Guid TempId { get; set; }
}
public class EntryForm1
{
public EntryForm1()
{
Sings = new List<Somesing>();
}
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public List<Somesing> Sings { get; set; }
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我认为问题出在 ModelState 上。当视图被渲染时(我认为这就是问题所在),在 POST 之后,最后一个值不会显示,即从视图中删除。
问题是
Model.Children.Count
将返回要显示的正确元素数。让我们分解一下...
因此,如果您最初有 5 个,然后根据 Guid 删除了索引为 0 的第一个,那么您现在剩下的项目为 4 个,索引为 1 到 4。
但是,在渲染视图之后post 后,HtmlHelpers 不会查看发布的模型中的值,而是查看 ModelState 中包含的值。因此,在 ModelState 中,索引为 0 的项仍然存在,并且由于循环现在循环到 4,因此不会显示最后一个元素。
解决方案,使用 ModelState.Clear()
I believe that problem lies with ModelState. When the view gets rendered, which I assume is where the issue lies, after the
POST
, the last value is not displayed i.e. removed from the view.The issue is that
Model.Children.Count
will return the correct number of elements to display.Lets break this down...
So if you have initially had 5 then removed the first one which is at index 0 based on the Guid, you now have items 4 items left with indexes 1 to 4.
However, when rendering the view after the post, the HtmlHelpers do not look at the values in model posted, but rather the values contained within the ModelState. So in the ModelState, item with index 0 still exists and since the loop is now looping to 4, the last element will not be displayed.
The solution, use
ModelState.Clear()
好的,正如 Ahmad 指出的那样,ModelState 是问题的关键。它包含这样的集合:
现在如果我从列表中删除了项目 0,现在这些项目将在列表中向上移动,并且 ModelState 中的数据将与模型不同步。我原以为 ASP.NET MVC 能够足够聪明地找出并重新排序,但这要求太多了。
我实际上实现了 PRG(重定向后获取),并通过将模型保持在会话中,我能够显示正确的信息,但同样,这将删除集合中的所有验证,如果模型本身有效,它将愉快地保存并重定向回主页“/”。显然这是不可接受的。
因此,一个解决方案是删除
ModelState
中的所有项目,然后为模型本身添加一个新条目(键为EmptyString
)。如果您用错误“项目已删除”填充它,这实际上可以正常工作,因为这将显示在验证摘要中。另一种解决方案是手动更改模型状态中的项目并根据新索引重新排列它们。这并不容易,但却是可能的。
OK, as Ahmad pointed out,
ModelState
is the key to the issue. It contains the collection as such:Now if I delete item 0 from the list, now the items will move up in the list and the data in the ModelState will go out of sync with the model. I had expected ASP.NET MVC to be clever enough to find out and re-order, but well that is asking for too much.
I actually implemented PRG (post-redirect-get) and by keeping the model in session, I was able to display correct information but again, this will remove all the validation in the collection and if model itself is valid, it will happily save and redirect back to home "/". Clearly this is not acceptable.
So one solution is to remove all items in the
ModelState
and then add a new entry for the model itself (with key ofEmptyString
). This can actually work alright if you populate it with error "Item deleted" as this will be displayed in the validation summary.Another solution is to manually change the items in the model state and re-arrange them based on the new indexes. This is not easy but possible.
ModelState.Clear() 将解决这个问题。
ModelState.Clear() 用于清除错误,但它也用于强制 MVC 引擎重建要传递到视图的模型。
ModelState.Clear() will Solved this problem.
ModelState.Clear() is used to clear errors but it is also used to force the MVC engine to rebuild the model to be passed to your View.