При передаче коллекции в EditorFor() он генерирует недопустимые имена для элементов ввода

У меня есть BookCreateModel, который состоит из информации о книжной плоскости, такой как Title, PublishYear и т.д. плюс коллекция книг Авторы (сложный тип):

public class BookCreateModel
{
    public string Title { get; set; }
    public int Year { get; set; }
    public IList<AuthorEntryModel> Authors { get; set; }
}

public class AuthorEntryModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

в представлении CreateBook Я использовал EditorFor помощник:

@Html.EditorFor(m => m.Authors, "AuthorSelector")

Edit1:

и шаблон AuthorSelector:

<div class="ptr_authors_wrapper">
    @for (int i = 0; i < Model.Count; i++)
    {
       <div class="ptr_author_line" data-line-index="@i">
        @Html.TextBoxFor(o => o[i].FirstName)
        @Html.TextBoxFor(o => o[i].LastName)
       </div>
    }
</div>
<script>
     ...
</script>

В шаблоне AuthorSelector содержатся некоторые разметки-обертки, которые должны быть осведомлены об индексе каждого отображаемого элемента плюс некоторый javascript, который обрабатывает взаимодействия с дочерними входами и должен отображаться один раз (внутри шаблона AuthorSelector), тем самым избавляясь от шаблон for/или шаблон AuthorSelector невозможен.

теперь проблема EditorFor действовать немного странно и генерировать входные имена, как это:

<input id="Authors__0__FirstName" name="Authors.[0].FirstName" type="text" value="" />
<input id="Authors__0__LastName" name="Authors.[0].LastName" type="text" value="" />

как вы можете видеть вместо генерации имен типа Authors[0].FirstName, он добавляет дополнительную точку, которая делает связующее устройство по умолчанию неспособным анализировать опубликованные данные.

любая идея?

Спасибо!

Ответ 1

Я бы рекомендовал вам придерживаться соглашений, то есть заменить:

@Html.EditorFor(m => m.Authors, "AuthorSelector")

с:

@Html.EditorFor(m => m.Authors)

а затем переименуйте ~/Views/Shared/EditorTemplates/AuthorSelector.cshtml в ~/Views/Shared/EditorTemplates/AuthorEntryModel.cshtml и сделайте его строго типизированным для одной модели AuthorEntryModel и избавьтесь от цикла:

@model AuthorEntryModel
@Html.TextBoxFor(o => o.FirstName)
@Html.TextBoxFor(o => o.LastName)

ASP.NET MVC автоматически отобразит шаблон редактора для всех элементов коллекции и сгенерирует собственные имена.


UPDATE:

После просмотра вашего обновления здесь мой ответ:

В вашем основном представлении:

<div class="ptr_authors_wrapper">
    @Html.EditorFor(m => m.Authors)
</div>

В шаблоне редактора:

@model AuthorEntryModel
<div class="ptr_author_line">
    @Html.TextBoxFor(o => o.FirstName)
    @Html.TextBoxFor(o => o.LastName)
</div>

Вы заметите отсутствие script в шаблоне, что совершенно нормально. Скрипты не имеют ничего общего с разметкой. Они входят в отдельные файлы javascript. В этом файле вы можете использовать jQuery, чтобы делать все, что вам нужно, с вашей разметкой. Он дает вам такие методы, как .index(), которые позволяют вам получить индекс элемента в совпадающем селекторе, чтобы вам не нужно было писать какие-либо петли и загрязнять вашу разметку такими вещами, как атрибуты data-line-index.

Ответ 2

Я немного опаздываю на вечеринку, но, надеюсь, это помогает кому-то.

Перекодировав до System.Web.Mvc.Html.DefaultEditorTemplates.CollectionTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper), шаблон по умолчанию в каркасе обрабатывает это, временно установив HtmlFieldPrefix в пустую строку и явно передавая префикс и индекс в вызов EditorFor().

<div class="ptr_authors_wrapper">
@{
    var prefix = ViewData.TemplateInfo.HtmlFieldPrefix;

    ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty;

    for (int i = 0; i < Model.Count; i++)
    {
        <div class="ptr_author_line" data-line-index="@i">
            @* You can also use null instead of "TextBox" to let the framework resolve which editor to use. *@
            @Html.EditorFor(o => o[i].FirstName, "TextBox", String.Format("{0}[{1}].FirstName", prefix, i))
            @Html.EditorFor(o => o[i].LastName, "TextBox", String.Format("{0}[{1}].LastName", prefix, i))
        </div>
    }

    ViewData.TemplateInfo.HtmlFieldPrefix = prefix;
}
</div>
<script>
     ...
</script>

Я нашел это особенно полезным, когда фреймворк записывал имена как [0].Children.[0].ChildProperty из-за именованного шаблона для коллекции Children. В моем случае решение должно было вызвать:

@Html.EditorFor(m => m[i], null, String.Format("{0}[{1}]", prefix, i))

вместо простого вызова:

@Html.EditorFor(m => m[i])

Ответ 4

Здесь вы можете использовать метод расширения, который будет отображать частичный вид и использовать правильный префикс поля HTML:

Метод расширения

    /// <summary>
    /// Helper method that renders the specified partial view as a HTML-encoded string using the specified
    /// collection as the model, with the intention that the partial view will use an editor template on the items
    /// in the collection.
    /// </summary>
    /// <typeparam name="TModel">the model type</typeparam>
    /// <typeparam name="TProperty">the property type</typeparam>
    /// <param name="htmlHelper">the <see cref="HtmlHelper"/> instance</param>
    /// <param name="partialViewName">the name of the partial view to render</param>
    /// <param name="collectionExpression">the model collection property expression</param>
    /// <returns>the HTML-encoded string</returns>
    public static MvcHtmlString PartialContainingEditorForCollection<TModel, TProperty>
        (this HtmlHelper<TModel> htmlHelper, string partialViewName,
         Expression<Func<TModel, TProperty>> collectionExpression)
        where TProperty : IEnumerable
    {
        var viewData = htmlHelper.ViewContext.ViewData;
        var model = (TModel) viewData.Model;
        var collection = collectionExpression.Compile().Invoke(model);

        var htmlFieldPrefix = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(
            ExpressionHelper.GetExpressionText(collectionExpression));

        return htmlHelper.Partial(partialViewName, collection,
                                  new ViewDataDictionary
                                      {
                                          TemplateInfo = new TemplateInfo {HtmlFieldPrefix = htmlFieldPrefix}
                                      });
    }

Пример использования

@Html.PartialContainingEditorForCollection("_TableWithSummary", m => Model.FormModel.ItemsToOrder)

Ответ 5

Я еще не нашел решения для этой ошибки, но в качестве обходного пути я изменил свой UpdateModel с помощью специального класса оболочки вместо непосредственного использования коллекции:

public class BookCreateModel
{
    public string Title { get; set; }
    public int Year { get; set; }
    public BookAuthorsList Authors { get; set; }
}

public class BookAuthorsList
{
    public IList<AuthorEntryModel> AuthorsList { get; set; }
}

public class AuthorEntryModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

и, следовательно, сгенерированные входы больше не будут вызывать проблемы с именами:)

<input  id="Authors_AuthorsList_0__FirstName" name="Authors.AuthorsList[0].FirstName" type="text"/>
<input  id="Authors_AuthorsList_0__LastName" name="Authors.AuthorsList[0].LastName" type="text"/>

Ответ 6

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

public class BookCreateModel
{
    public string Title { get; set; }
    public int Year { get; set; }
    public BookAuthorsModel Authors { get; set; }
}

public class BookAuthorsModel
{
    IList<AuthorEntryModel> Items { get; set; }
}

Затем переименуйте свой шаблон редактора в "BookAuthorsModel.cshtml" и сделайте так:

@model BookAuthorsModel
<div class="ptr_authors_wrapper">
    @for (int i = 0; i < Model.Items.Count; i++)
    {
       <div class="ptr_author_line" data-line-index="@i">
        @Html.TextBoxFor(o => Items.o[i].FirstName)
        @Html.TextBoxFor(o => Items.o[i].LastName)
       </div>
    }
</div>
<script>
     ...
</script>

И когда вы хотите использовать его, просто вызовите:

@Html.EditorFor(m => m.Authors)

Затем он должен генерировать поля ввода следующим образом:

<input id="Authors_Items_0__FirstName" name="Authors.Items[0].FirstName" type="text" value="" />
<input id="Authors_Items_0__LastName" name="Authors.Items[0].LastName" type="text" value="" />

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