绑定可编辑的子项列表

发布于 2024-12-22 07:04:25 字数 9077 浏览 2 评论 0原文

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, ICollectionWishitems)) 仍然给我一个空的 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
  • Sandbox 是针对 .NET Framework 4 构建的

参考:

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
  • 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 a string[] 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文