Привязка редактируемого списка детей

TL; DR: В моем приложении ASP.NET MVC3, как мне реализовать представление, которое позволяет мне редактировать детали родительского объекта одновременно с деталями списка дочерних объектов?

Обновить: я принимаю @torm answer, потому что он предоставил ссылка, которая дает некоторое объяснение, почему мое текущее решение может быть таким же хорошим, как и оно. Однако нам бы хотелось услышать, есть ли у кого-нибудь альтернатива!

Я искал и читал (см. раздел "Ссылки" внизу для некоторых из полученных до сих пор результатов). Тем не менее, я все еще чувствую, что есть что-то "вонючее" с решениями, которые я нашел до сих пор. Интересно, есть ли у кого-нибудь из вас более элегантный ответ или предложение (или можно объяснить, почему это может быть "так хорошо, как это получается" ). Спасибо заранее!

Итак, здесь настройка:

Модели:

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);
    }
    /* ... */
}

Вид: Представления \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>

Что происходит?

В приведенной выше настройке создается страница со стандартными элементами ввода для "родительской" модели Wishlist:

<input class="text-box single-line" id="Name" name="Name" type="text" value="MyWishlist" />  

Для "детей" Wishitems в таблице мы получаем индексированные элементы ввода:

<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 POSTed назад с пустым свойством .Wishitems.

Альтернативная сигнатура для обработчика POST ([HttpPost] public ActionResult Edit(Wishlist wishlist, ICollection<Wishitem> wishitems)) по-прежнему получает пустой 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);
    }
}

Мой список пожеланий

Я хочу, чтобы у меня был способ получить все данные POSTed в одном структурированном объекте, например:

[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... */
}

Советы, подсказки, мысли?

Примечания:

  • Это пример Sandbox. Фактическое приложение, над которым я работаю, совсем другое, не имеет ничего общего с доменом, открытым в Sandbox.
  • Я не использую "ViewModels" в этом примере, потому что, да еще, они, похоже, не являются частью ответа. Если они понадобятся, я бы обязательно их представил (и в реальном приложении, над которым я работаю, мы уже используем их).
  • Аналогично, репозиторий абстрагируется простым классом SandboxDbContext в этом примере, но, вероятно, будет заменен общим шаблоном Repository и Unit Of Work в реальном приложении.
  • Приложение Sandbox построено с использованием:
    • Visual Web Developer 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 с пакетом обновления 1 (KB983509)
    • .NET Framework 4.0.30319 SP1Rel
    • ASP.NET MVC3
      • Синтаксис Razor для представлений
      • Метод Code-First
    • Entity Framework 4.2.0.0
  • Песочница построена с таргетингом на .NET Framework 4

Литература:

  • "Начало работы с ASP.NET MVC3" Обнаруживает основы, но не имеет отношения к отношениям модели.

  • "Начало работы с EF с помощью MVC" ан-Asp-нетто-MVC-приложения В частности, Часть 6 показывает, как бороться с некоторыми из отношений между моделями. Однако этот учебник использует аргумент FormCollection для своего обработчика POST, а не для автоматической привязки модели. Другими словами: [HttpPost] public ActionResult Edit (int id, FormCollection formCollection) Вместо того, [HttpPost] public ActionResult Edit (InstructorAndCoursesViewModel viewModel) Кроме того, список курсов, связанных с данным инструктором, представлен (в пользовательском интерфейсе) как набор флажков с тем же именем (что приводит к аргументу string[] для обработчика POST), не совсем тот же сценарий, который я ищу в.

  • "Редактирование списка длины переменной, ASP.NET MVC2-style" На основе MVC2 (так что мне интересно, если он все еще описывает лучший вариант теперь, когда у нас есть MVC3). По общему признанию, мне еще не удалось разобраться с вставками и/или удалением моделей "Дети" из списка. Кроме того, это решение:

    • полагается на пользовательский код (BeginCollectionItem) - это нормально, если это необходимо (но все же это необходимо в MVC3?)
    • обрабатывает список как самостоятельную коллекцию, а не свойство модели обертывания - другими словами, существует окружающая модель "GiftsSet" (эквивалентная исходной модели Wishlist в моем примере), хотя я не знаете, если введение явной родительской модели недействительно это решение или нет.
  • Формат проводки ASP.NET для привязки к массивам, спискам, коллекциям, словарям Сообщение Scott Hanselman является одной из наиболее цитируемых ссылок на тему привязки к спискам в приложениях MVC. Однако он просто описывает соглашения об именах, принятые каркасом, и используется для создания объектов, соответствующих вашему методу действий (обратите внимание, как в статье нет примера создания страницы, которая затем передает данные в одно из описанных действий). Это отличная информация, если мы должны сами создавать HTML. Мы должны?

  • "привязка модели к списку" Еще одна главная ссылка, Фил Хаак. Он содержит некоторую информацию, такую ​​как сообщение Hansleman выше, но также показывает, что мы можем использовать HtmlHelpers в цикле (for (int i = 0; i < 3; i++) { Html.TextBoxFor(m => m[i].Title) }) или в шаблоне редактора (Html.EditorFor(m=>m[i])). Однако, используя этот подход, HTML, созданный шаблоном редактора, не будет содержать какого-либо конкретного префикса (например: имена и идентификаторы входных элементов будут в форме [index].FieldName, например: [0].Quantity или [1].Name). Это может быть или не быть критичным в этом примере, но, вероятно, будет проблемой в моем реальном приложении, где в этом же представлении могут отображаться разные "параллельные" списки детей.