绑定可编辑的子项列表
TL;DR:在我的 ASP.NET MVC3 应用程序中,我应该如何实现一个视图,该视图允许我同时编辑“父”实体的详细信息和“子”列表的详细信息'实体?
更新:我接受@torm的回答因为他提供了一个链接给出了一些解释,解释为什么我当前的解决方案可能是最好的。 但是,我们很想听听其他人是否有任何替代方案!
我一直在搜索和阅读(请参阅底部的“参考资料”部分,了解迄今为止的一些发现)。 然而,我仍然觉得到目前为止我找到的解决方案有些“臭”。我想知道你们中是否有人有更优雅的答案或建议(或者可以解释为什么这可能是“尽善尽美”)。 提前致谢!
因此,设置如下:
模型:
public class Wishlist
{
public Wishlist() { Wishitems = new List<Wishitem>(); }
public long WishListId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual ICollection<Wishitem> Wishitems { get; set; }
}
public class Wishitem
{
public long WishitemId { get; set; }
public string Name { get; set; }
public int Quantity { get; set; }
}
控制器:
public class WishlistsController : Controller
{
private SandboxDbContext db = new SandboxDbContext();
/* ... */
public ActionResult Edit(long id)
{
Wishlist wishlist = db.Wishlists.Find(id);
return View(wishlist);
}
[HttpPost]
public ActionResult Edit(Wishlist wishlist)
//OR (see below): Edit(Wishlist wishlist, ICollection<Wishitem> wishitems)
{
if (ModelState.IsValid)
{
db.Entry(wishlist).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(wishlist);
}
/* ... */
}
视图:Views\Wishlist\Edit.cshtml
@model Sandbox.Models.Wishlist
<h2>Edit</h2>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm())
{
@Html.ValidationSummary(true)
<fieldset>
<legend>Wishlist</legend>
@Html.HiddenFor(model => model.WishListId)
<div class="editor-label">@Html.LabelFor(model => model.Name)</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
</fieldset>
<table>
<tr>
<th>
Quantity
</th>
<th>
Name
</th>
</tr>
@for (var itemIndex = 0; itemIndex < Model.Wishitems.Count; itemIndex++)
{
@Html.EditorFor(item => Model.Wishitems.ToList()[itemIndex])
}
</table>
<p>
<input type="submit" value="Save" />
</p>
}
编辑器模板:Views\Shared\EditorTemplates\Wishitem.cshtml
@model Sandbox.Models.Wishitem
<tr>
<td>
@Html.HiddenFor(item=>item.WishitemId)
@Html.TextBoxFor(item => item.Quantity)
@Html.ValidationMessageFor(item => item.Quantity)
</td>
<td>
@Html.TextBoxFor(item => item.Name)
@Html.ValidationMessageFor(item => item.Name)
</td>
</tr>
发生了什么?
上面的设置为“父”愿望清单模型生成了一个包含标准输入元素的页面:
<input class="text-box single-line" id="Name" name="Name" type="text" value="MyWishlist" />
对于表中的“子”愿望项目,我们获得索引输入元素:
<input data-val="true" data-val-number="The field Quantity must be a number." data-val-required="The Quantity field is required." name="[0].Quantity" type="text" value="42" />
<input name="[0].Name" type="text" value="Unicorns" />
这导致发布的 Wishlist Wishlist
参数返回一个空的 .Wishitems
属性。
POST 处理程序的替代签名 ([HttpPost] public ActionResult Edit(Wishlist Wishlist, ICollection
) 仍然给我一个空的 wishlist.Wishitems
,但让我访问(可能修改的)wishitems
。
在第二种情况下,我可以做一些自定义绑定。例如(这不是我职业生涯中见过的最优雅的代码):
[HttpPost]
public ActionResult Edit(Wishlist editedList, ICollection<Wishitem> editedItems)
{
var wishlist = db.Wishlists.Find(editedList.WishListId);
if (wishlist == null) { return HttpNotFound(); }
if (ModelState.IsValid)
{
UpdateModel(wishlist);
foreach (var editedItem in editedItems)
{
var wishitem = wishlist.Wishitems.Where(wi => wi.WishitemId == editedItem.WishitemId).Single();
if (wishitem != null)
{
wishitem.Name = editedItem.Name;
wishitem.Quantity = editedItem.Quantity;
}
}
db.SaveChanges();
return View(wishlist);
}
else
{
editedList.Wishitems = editedItems;
return View(editedList);
}
}
我的愿望清单
我希望有一种方法可以让我在单个结构化对象中获取所有已发布的数据,例如:
[HttpPost]
public ActionResult Edit(Wishlist wishlist) { /* ...Save the wishlist... */ }
使用 wishlist.Wishitems
填充(可能修改的)项目
或者如果我的控制器必须单独接收它们,那么我可以用更优雅的方式来处理数据的合并。像这样的东西
[HttpPost]
public ActionResult Edit(Wishlist editedList, ICollection<Wishitem> editedItems)
{
var wishlist = db.Wishlists.Find(editedList.WishListId);
if (wishlist == null) { return HttpNotFound(); }
if (ModelState.IsValid)
{
UpdateModel(wishlist);
/* and now wishlist.Wishitems has been updated with the data from the Form (aka: editedItems) */
db.SaveChanges();
return View(wishlist);
}
/* ...Etc etc... */
}
提示、技巧、想法?
注意:
- 这是一个沙盒示例。我正在开发的实际应用程序完全不同,与沙箱中公开的域无关。
- 我在示例中没有使用“ViewModels”,因为到目前为止,它们似乎不是答案的一部分。如果有必要,我肯定会介绍它们(在我正在开发的实际应用程序中,我们已经在使用它们)。
- 同样,在本示例中,存储库由简单的 SandboxDbContext 类抽象出来,但在实际应用程序中可能会被通用存储库和工作单元模式所取代。
- 沙箱应用程序是使用以下内容构建的:
- 可视化 Web 开发人员 2010 Express
- Microsoft Visual Web Developer 2010 Express 修补程序 - ENU (KB2547352)
- Microsoft Visual Web Developer 2010 Express 修补程序 - ENU (KB2548139)
- Microsoft Visual Web Developer 2010 Express - ENU Service Pack 1 (KB983509)
- .NET Framework 4.0.30319 SP1Rel
- ASP.NET MVC3
- 视图的 Razor 语法
- 代码优先方法
- 实体框架4.2.0.0
- 可视化 Web 开发人员 2010 Express
- Sandbox 是针对 .NET Framework 4 构建的
参考:
"ASP.NET MVC3 入门" 涵盖基础知识,但不处理模型关系
“使用 MVC 开始使用 EF” an-asp-net-mvc-应用程序 特别是 第 6 部分展示了如何处理模型之间的一些关系。 但是,本教程为其 POST 处理程序使用
FormCollection
参数,而不是自动模型绑定。 换句话说: [HttpPost] public ActionResult Edit(int id, FormCollection formCollection) 而不是类似的东西 [HttpPost]公共ActionResult编辑(InstructorAndCoursesViewModel viewModel) 此外,与给定讲师关联的课程列表(在 UI 中)表示为一组具有相同名称的复选框(导致 POST 处理程序的string[]
参数),不完全是与我正在查看的场景相同。“编辑可变长度列表,ASP.NET MVC2 样式” 基于 MVC2(所以我想知道既然我们有了 MVC3,它是否仍然描述了最佳选择)。 不可否认,我(还)还没有处理从列表中插入和/或删除 Children 模型的问题。 另外,这个解决方案:
- 依赖于自定义代码 (BeginCollectionItem) - 如果有必要,这很好(但在 MVC3 中仍然有必要吗?)
- 将列表作为独立集合处理,而不是包装模型的属性 - 换句话说,有周围的“GiftsSet”模型(相当于我的示例中的父 Wishlist 模型),尽管我不这样做知道引入显式父模型是否会使该解决方案无效。
“模型绑定到列表” 另一篇顶级参考文献,作者:Phil Haack。它具有与上面的 Hansleman 帖子相同的一些信息,但也向我们展示了可以在循环中使用 HtmlHelpers (
for (int i = 0; i < 3; i++) { Html.TextBoxFor(m => ; m[i].Title) }
),或在编辑器模板 (Html.EditorFor(m=>m[i])
) 中。 然而,使用这种方法,编辑器模板生成的 HTML 将不包含任何特定的前缀(例如:输入元素的名称和 id 将采用[index].FieldName
的形式,例如:<代码>[0].数量,或[1].名称
)。这在示例中可能很重要,也可能不重要,但在我的实际应用程序中可能会成为一个问题,其中不同的“并行”子列表可能会出现在同一视图中。
TL;DR: In my ASP.NET MVC3 App, how should I implement a View that allows me to edit details of a 'parent' entity at the same time as the details of a list of 'children' entities ?
Update: I'm accepting @torm's answer because he provided a link that gives some explanation as to why my current solution may be as good as it gets. However, we'd love to hear if anyone else have any alternative!
I've been searching and reading (see the 'References' section at the bottom for some of the findings so far).
However, I still feel like there's something 'smelly' with the solutions I found so far. I wonder if any of you have a more elegant answer or suggestion (or can explain why this may be 'as good as it gets'). Thanks in advance!
So, here's the setup:
The Models:
public class Wishlist
{
public Wishlist() { Wishitems = new List<Wishitem>(); }
public long WishListId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual ICollection<Wishitem> Wishitems { get; set; }
}
public class Wishitem
{
public long WishitemId { get; set; }
public string Name { get; set; }
public int Quantity { get; set; }
}
The Controller:
public class WishlistsController : Controller
{
private SandboxDbContext db = new SandboxDbContext();
/* ... */
public ActionResult Edit(long id)
{
Wishlist wishlist = db.Wishlists.Find(id);
return View(wishlist);
}
[HttpPost]
public ActionResult Edit(Wishlist wishlist)
//OR (see below): Edit(Wishlist wishlist, ICollection<Wishitem> wishitems)
{
if (ModelState.IsValid)
{
db.Entry(wishlist).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(wishlist);
}
/* ... */
}
The View: Views\Wishlist\Edit.cshtml
@model Sandbox.Models.Wishlist
<h2>Edit</h2>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm())
{
@Html.ValidationSummary(true)
<fieldset>
<legend>Wishlist</legend>
@Html.HiddenFor(model => model.WishListId)
<div class="editor-label">@Html.LabelFor(model => model.Name)</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
</fieldset>
<table>
<tr>
<th>
Quantity
</th>
<th>
Name
</th>
</tr>
@for (var itemIndex = 0; itemIndex < Model.Wishitems.Count; itemIndex++)
{
@Html.EditorFor(item => Model.Wishitems.ToList()[itemIndex])
}
</table>
<p>
<input type="submit" value="Save" />
</p>
}
The Editor Template: Views\Shared\EditorTemplates\Wishitem.cshtml
@model Sandbox.Models.Wishitem
<tr>
<td>
@Html.HiddenFor(item=>item.WishitemId)
@Html.TextBoxFor(item => item.Quantity)
@Html.ValidationMessageFor(item => item.Quantity)
</td>
<td>
@Html.TextBoxFor(item => item.Name)
@Html.ValidationMessageFor(item => item.Name)
</td>
</tr>
What is Going on?
The setup above generates a page with standard input elements for the 'parent' Wishlist model:
<input class="text-box single-line" id="Name" name="Name" type="text" value="MyWishlist" />
For the 'children' Wishitems in the table, we get indexed input elements:
<input data-val="true" data-val-number="The field Quantity must be a number." data-val-required="The Quantity field is required." name="[0].Quantity" type="text" value="42" />
<input name="[0].Name" type="text" value="Unicorns" />
This leads to a the Wishlist wishlist
argument POSTed back with an empty .Wishitems
property.
The alternative signature for the POST handler ([HttpPost] public ActionResult Edit(Wishlist wishlist, ICollection<Wishitem> wishitems)
) still gets me an empty wishlist.Wishitems
, but lets me access the (potentially modified) wishitems
.
In this second scenario, I can do some for of custom binding. For instance (not the most elegant code I've seen in my career):
[HttpPost]
public ActionResult Edit(Wishlist editedList, ICollection<Wishitem> editedItems)
{
var wishlist = db.Wishlists.Find(editedList.WishListId);
if (wishlist == null) { return HttpNotFound(); }
if (ModelState.IsValid)
{
UpdateModel(wishlist);
foreach (var editedItem in editedItems)
{
var wishitem = wishlist.Wishitems.Where(wi => wi.WishitemId == editedItem.WishitemId).Single();
if (wishitem != null)
{
wishitem.Name = editedItem.Name;
wishitem.Quantity = editedItem.Quantity;
}
}
db.SaveChanges();
return View(wishlist);
}
else
{
editedList.Wishitems = editedItems;
return View(editedList);
}
}
My Wishlist
I wish there was a way for me to get all the POSTed data in a single structured object, eg:
[HttpPost]
public ActionResult Edit(Wishlist wishlist) { /* ...Save the wishlist... */ }
With wishlist.Wishitems
filled with the (potentially modified) items
Or a more elegant way for me to handle the merging of the data, if my controller must receive them separately. Something like
[HttpPost]
public ActionResult Edit(Wishlist editedList, ICollection<Wishitem> editedItems)
{
var wishlist = db.Wishlists.Find(editedList.WishListId);
if (wishlist == null) { return HttpNotFound(); }
if (ModelState.IsValid)
{
UpdateModel(wishlist);
/* and now wishlist.Wishitems has been updated with the data from the Form (aka: editedItems) */
db.SaveChanges();
return View(wishlist);
}
/* ...Etc etc... */
}
Hints, tips, thoughts?
Notes:
- This is a Sandbox example. The actual application I'm working on is quite different, has nothing to do with the domain exposed in Sandbox.
- I'm not using 'ViewModels' in the example, because -so far- they don't seem to be part of the answer. If they are necessary, I would certainly introduce them (and in the real app I'm working on we're already using them).
- Similarly, the repository is abstracted by the simple SandboxDbContext class in this example, but would probably be replaced by a generic Repository and Unit Of Work pattern in the real app.
- The Sandbox app is built using:
- Visual Web Developer 2010 Express
- Hotfix for Microsoft Visual Web Developer 2010 Express - ENU (KB2547352)
- Hotfix for Microsoft Visual Web Developer 2010 Express - ENU (KB2548139)
- Microsoft Visual Web Developer 2010 Express - ENU Service Pack 1 (KB983509)
- .NET Framework 4.0.30319 SP1Rel
- ASP.NET MVC3
- Razor syntax for the Views
- Code-First approach
- Entity Framework 4.2.0.0
- Visual Web Developer 2010 Express
- Sandbox is built targeting .NET Framework 4
References:
"Getting Started with ASP.NET MVC3"
Covers the basics, but does not deal with model relationships"Getting Started with EF using MVC"
an-asp-net-mvc-application
In particular Part 6 shows how to deal with some of the relationships between the models.
However, this tutorial uses aFormCollection
argument for its POST handler, rather than the automatic model binding.
In other words:
[HttpPost] public ActionResult Edit(int id, FormCollection formCollection)
Rather than something along the lines of
[HttpPost] public ActionResult Edit(InstructorAndCoursesViewModel viewModel)
Also, the list of Courses associated with a given Instructor is represented (in the UI) as a set of checkboxes with the same name (leading to astring[]
argument for the POST handler), not quite the same scenario that I am looking at."Editing a variable length list, ASP.NET MVC2-style"
Based on MVC2 (so I'm wondering if it is still describes the best option now that we have MVC3).
Admittedly, I have not (yet) got to dealing with the insertions and/or removal of Children models from the list.
Also, this solution:- relies on custom code (BeginCollectionItem) - which is fine if it is necessary (but is it still necessary in MVC3 ?)
- handles the list as a free-standing collection, rather than a property of a wrapping model - in other words, there is surrounding "GiftsSet" model (equivalent to the parent Wishlist model in my example), although I don't know if introduing an explicit parent model invalidates this solution or not.
"ASP.NET Wire Format for Model Binding to Arrays, Lists, Collections, Dictionaries"
Scott Hanselman's post is one of the most quoted reference son the topic of binding to lists in MVC applications.
However, he is simply describing the naming conventions adopted by the framework and used to generate objects matching your action method (note how the article has no example of generating a page that then submits data to one of the actions described).
This is great information if we have to generate the HTML ourselves. Do we have to?"Model Binding To A List"
Another top reference, by Phil Haack. It has some of the same information as the Hansleman post above, but also shows us we can use HtmlHelpers within a loop (for (int i = 0; i < 3; i++) { Html.TextBoxFor(m => m[i].Title) }
), or in an Editor Template (Html.EditorFor(m=>m[i])
).
However, using this approach, the HTML generated by the Editor Template would not include any specific prefix (eg: the names and ids of the input elements would be in the form[index].FieldName
like:[0].Quantity
, or[1].Name
). This may or may not be critical in the example, but will probably be an issue in my actual application, where different 'parallel' lists of children may appear in the same view.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论