Непоследовательные индексы MVC3 и DefaultModelBinder

Верно ли, что связыватель модели по умолчанию в MVC 3.0 способен обрабатывать непоследовательные индексы (как для простых, так и для сложных типов моделей)? Я сталкивался с сообщениями, которые предполагают, что это должно быть сделано, однако в моих тестах оказалось, что это НЕ так.

Приведенные значения обратного сообщения:

items[0].Id = 10
items[0].Name = "Some Item"
items[1].Id = 3
items[1].Name = "Some Item"
items[4].Id = 6
items[4].Name = "Some Item"

И метод контроллера:

public ActionResult(IList<MyItem> items) { ... }

Загружаются только значения 0 и 1; пункт 4 просто игнорируется.

Я видел множество решений для создания пользовательских индексов (Привязка модели к списку), однако все они, по-видимому, ориентированы на предыдущие версии MVC, и большинство из них немного «деспотичные» IMO.

Я что-то упускаю?


person mindlessgoods    schedule 22.12.2011    source источник


Ответы (6)


У меня это работает, вы должны не забыть добавить общий скрытый ввод индексации, как описано в вашей статье, на которую ссылаются:

Скрытый ввод с name = Items.Index является ключевой частью

<input type="hidden" name="Items.Index" value="0" />
<input type="text" name="Items[0].Name" value="someValue1" />

<input type="hidden" name="Items.Index" value="1" />
<input type="text" name="Items[1].Name" value="someValue2" />

<input type="hidden" name="Items.Index" value="3" />
<input type="text" name="Items[3].Name" value="someValue3" />

<input type="hidden" name="Items.Index" value="4" />
<input type="text" name="Items[4].Name" value="someValue4" />

надеюсь это поможет

person Bassam Mehanni    schedule 22.12.2011
comment
Я надеялся избежать этого подхода. Я скрестил пальцы на том, что связыватель модели по умолчанию просто решит сам отсутствующий индекс. Должна быть причина (возможно, для более сложных ситуаций?) для необходимости явного указания индекса. В любом случае, спасибо за быстрый ответ и пример кода. - person mindlessgoods; 23.12.2011
comment
О МОЙ НУЛЬ! Это просто сделало возврат списков НАМНОГО проще, вместо того, чтобы делать смешно для (i++). Я могу просто использовать первичный ключ или любой другой идентификатор в индексе, и список возвращается таким красивым и строго типизированным. Эти темные, сокрытые тайны. Это сделало мой день! +1 +пиво!!! - person Piotr Kula; 31.03.2015
comment
@Yablargo, наверное, это функция ModelState. Вы используете это в действии после публикации? - person ps2goat; 21.04.2015
comment
@ ps2goat Я не совсем уверен в этом комментарии, я собираюсь его удалить. С тех пор я использую эту функцию повсюду и не могу говорить о проблеме, с которой я столкнулся в то время. - person Yablargo; 06.05.2015
comment
Существует ли клиентская структура (например, Knockout), которая будет генерировать и управлять этими индексаторами для DefaultModelBinder? - person avo; 12.02.2016
comment
Выполняя это в PartialView (т.е. каждый частичный элемент является элементом из списка), я использовал следующее: <input type="hidden" name="@(ViewData.TemplateInfo.HtmlFieldPrefix.Substring(0, ViewData.TemplateInfo.HtmlFieldPrefix.LastIndexOf("["))).Index" value="@ViewData.TemplateInfo.HtmlFieldPrefix.Split("[]".ToCharArray, StringSplitOptions.RemoveEmptyEntries).Last" /> - person apc; 09.08.2016
comment
работает, когда значение атрибута (value=N) скрытого соответствует следующему (name=Items[N].Name) - person Lapenkov Vladimir; 21.08.2018

Этот вспомогательный метод, основанный на подходе Стива Сандерсона, намного проще и может использоваться для привязки любого элемента в коллекции, и, похоже, он работает с привязкой модели MVC.

public static IHtmlString AnchorIndex(this HtmlHelper html)
{
    var htmlFieldPrefix = html.ViewData.TemplateInfo.HtmlFieldPrefix;
    var m = Regex.Match(htmlFieldPrefix, @"([\w]+)\[([\w]*)\]");
    if (m.Success && m.Groups.Count == 3)
        return
            MvcHtmlString.Create(
                string.Format(
                    "<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />",
                    m.Groups[1].Value, m.Groups[2].Value));
    return null;
}

Например. Просто вызовите его в EditorTemplate или в любом другом месте, где вы будете генерировать входные данные, как показано ниже, чтобы сгенерировать скрытую переменную привязки индекса, если она применима.

@model SomeViewModel
@Html.AnchorIndex()
@Html.TextBoxFor(m => m.Name)
... etc.

Я думаю, что у него есть несколько преимуществ по сравнению с подходом Стива Сандерсона.

  1. Он работает с EditorFor и другими встроенными механизмами для обработки перечислимых значений. Итак, если Items является свойством IEnumerable<T> в модели представления, следующее работает, как и ожидалось:

    <ul id="editorRows" class="list-unstyled"> @Html.EditorFor(m => m.Items) @* Each item will correctly anchor allowing for dynamic add/deletion via Javascript *@ </ul>

  2. Это проще и не требует больше магических строк.

  3. У вас может быть один EditorTemplate/DisplayTemplate для типа данных, и он просто не будет работать, если не используется для элемента в списке.

Единственным недостатком является то, что если привязываемая корневая модель является перечислимой (т. е. параметром самого метода Action, а не просто свойством где-то глубже в графе объекта параметра), привязка завершится ошибкой при первом непоследовательном индексе. К сожалению, функциональность .Index DefaultModelBinder работает только для некорневых объектов. В этом сценарии вам остается только использовать описанные выше подходы.

person Phil Degenhardt    schedule 24.01.2014
comment
Большое спасибо за это, Фил. Я твердо привержен использованию EditorFor с перечислениями, и это сработало как шарм! - person Jono; 09.05.2016

Статья, на которую вы ссылаетесь, является старой (MVC2), но, насколько мне известно, это по-прежнему де-факто способ моделирования коллекций привязки с использованием связывателя моделей по умолчанию.

Если вы хотите непоследовательную индексацию, как говорит Бассам, вам нужно будет указать индексатор. Индексатор не обязательно должен быть числовым.

Для этого мы используем помощник HTML BeginCollectionItem Стива Сандерсона. Он автоматически генерирует индексатор как Guid. Я думаю, что это лучший подход, чем использование числовых индексаторов, когда ваш HTML элемент коллекции не является последовательным.

person danludwig    schedule 22.12.2011
comment
Я наткнулся на ту же статью, и она определенно касалась проблемы, которую я описывал. Как упоминалось выше, я надеялся, что связыватель модели по умолчанию справится с этой ситуацией внутри и что помощник BeginCollectionItem на самом деле не нужен. Спасибо за ответ! - person mindlessgoods; 23.12.2011

Я боролся с этим на этой неделе, и ответ Бассама был ключом к тому, чтобы направить меня на правильный путь. У меня есть динамический список элементов инвентаря, который может иметь поле количества. Мне нужно было знать, сколько элементов они выбрали, за исключением того, что список элементов может варьироваться от 1 до n.

В конце концов мое решение было довольно простым. Я создал ViewModel под названием ItemVM с двумя свойствами. ID товара и количество. В почтовом действии я принимаю список из них. При включенном индексировании все элементы передаются... даже с нулевым количеством. Вы должны проверить и обработать его на стороне сервера, но с итерацией обработать этот динамический список тривиально.

В моем представлении я использую что-то вроде этого:

@foreach (Item item in Items)
{
<input type="hidden" name="OrderItems.Index" value="@item.ItemID" />
<input type="hidden" name="OrderItems[@item.ItemID].ItemID" value="@item.ItemID" />
<input type="number" name="OrderItems[@item.ItemID].Quantity" />
}

Это дает мне список с индексом, основанным на 0, но итерация в контроллере извлекает все необходимые данные из новой строго типизированной модели.

public ActionResult Marketing(List<ItemVM> OrderItems)
...
        foreach (ItemVM itemVM in OrderItems)
            {
                OrderItem item = new OrderItem();
                item.ItemID = Convert.ToInt16(itemVM.ItemID);
                item.Quantity = Convert.ToInt16(itemVM.Quantity);
                if (item.Quantity > 0)
                {
                    order.Items.Add(item);
                }
            }

В результате вы получите набор элементов, количество которых больше 0, и идентификатор элемента.

Этот метод работает в MVC 5 с использованием EF 6 в Visual Studio 2015. Возможно, это поможет кому-то найти это решение, как я.

person Jason Conville    schedule 10.10.2015
comment
Не могли бы вы привести полный пример? у меня очень похожая ситуация - person Mr_LinDowsMac; 20.10.2015

Или используйте эту функцию javascript, чтобы исправить индексацию: (очевидно, замените EntityName и FieldName)

function fixIndexing() {
        var tableRows = $('#tblMyEntities tbody tr');

        for (x = 0; x < tableRows.length; x++) {
            tableRows.eq(x).attr('data-index', x);

            tableRows.eq(x).children('td:nth-child(1)').children('input:first').attr('name', 'EntityName[' + x + "].FieldName1");

            tableRows.eq(x).children('td:nth-child(2)').children('input:first').attr('name', 'EntityName[' + x + "].FieldName2");

            tableRows.eq(x).children('td:nth-child(3)').children('input:first').attr('name', 'EntityName[' + x + "].FieldName3");
        }

        return true; //- Submit Form -
    }
person Pinpoint Solutions    schedule 14.01.2015

Я закончил тем, что сделал более общий помощник HTML: -

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;

namespace Wallboards.Web.Helpers
{
    /// <summary>
    /// Hidden Index Html Helper
    /// </summary>
    public static class HiddenIndexHtmlHelper
    {
        /// <summary>
        /// Hiddens the index for.
        /// </summary>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="htmlHelper">The HTML helper.</param>
        /// <param name="expression">The expression.</param>
        /// <param name="index">The Index</param>
        /// <returns>Returns Hidden Index For</returns>
        public static MvcHtmlString HiddenIndexFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, int index)
        {
            var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            var propName = metadata.PropertyName;

            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("<input type=\"hidden\" name=\"{0}.Index\" autocomplete=\"off\" value=\"{1}\" />", propName, index);

            return MvcHtmlString.Create(sb.ToString());
        }
    }
}

А затем включите его в каждую итерацию элемента списка в представлении Razor: -

@Html.HiddenIndexFor(m => m.ExistingWallboardMessages, i)
person Stephen Garside    schedule 10.03.2017