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 a
FormCollection
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.
You may want to check this link http://www.codetuning.net/blog/post/Binding-Model-Graphs-with-ASPNETMVC.aspx I had similar problem and the above allowed me to understand it
来源:https://stackoverflow.com/questions/8570388/binding-an-editable-list-of-children