Связанные массивы в ASP.NET MVC без индекса?

У меня есть HTML, который выглядит так:

<input type="text" name="data[]" value="George"/>
<input type="text" name="data[]" value="John"/>
<input type="text" name="data[]" value="Paul"/>
<input type="text" name="data[]" value="Ringo"/>

В PHP я могу получить этот массив, например:

$array = $_POST['name'];
// $array[0] == "George"

В ASP.NET MVC соглашения об объединении моделей заставляют меня помещать индексы в HTML, поэтому контроллер может получать массив.

<!-- HTML for the ASP.NET MVC Version -->
<input type="text" name="data[0]" value="George"/>
<input type="text" name="data[1]" value="John"/>
<input type="text" name="data[2]" value="Paul"/>
<input type="text" name="data[3]" value="Ringo"/>

// C# Controller
public ActionResult SomeAction(string[] data)
{
    // Do stuff
}

Если я отправлю первый HTML, данные будут null в действии.

Ну, я думаю, это отстой.

Если я использую код на стороне клиента для удаления или добавления элементов в массив, я должен написать код для переопределения массива HTML.

Есть ли способ расширить ASP.NET MVC ModelBinder для привязки массивов без индексов или обходного пути для решения этой проблемы?

ИЗМЕНИТЬ

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

Вид

<table>
@for (var i = 0; i < Model.Sections.Count; ++i)
{
    <tr>
        <td><a href="#" class="edit-section"><span class="glyphicon glyphicon-question-sign"></span></a></td>
        <td>@Html.TextBoxFor(m => Model.Sections[i].SectionOrder, new { @class = "form-control" })</td>
        <td>@Html.TextBoxFor(m => Model.Sections[i].Title, new { @class = "form-control" })</td>
        <td>@Html.TextBoxFor(m => Model.Sections[i].SubTitle, new { @class = "form-control" })</td>
        <td>
            @Html.HiddenFor(m => Model.Sections[i].Id)
            <a href="#" class="delete-section"><span class="glyphicon glyphicon-remove"></span></a>
        </td>
    </tr>
}
</table>

Действие

public ActionResult SaveSections(ICollection<SectionModel> sections)
{
    // DO STUFF
}

Я попытался сделать входы HTML вручную, например:

@for (var i = 0; i < Model.Sections.Count; ++i)
{
    <tr>
        <td><a href="#" class="edit-section"><span class="glyphicon glyphicon-question-sign"></span></a></td>
        <td>@Html.TextBox("Sections.SectionOrder", Model.Sections[i].SectionOrder, new { @class = "form-control" })</td>
        <td>@Html.TextBox("Sections.Title", Model.Sections[i].Title, new { @class = "form-control" })</td>
        <td>@Html.TextBox("Sections.SubTitle", Model.Sections[i].SubTitle, new { @class = "form-control" })</td>
        <td>
            @Html.Hidden("Sections.SubTitle", Model.Sections[i].Id)
            <a href="#" class="delete-section"><span class="glyphicon glyphicon-remove"></span></a>
        </td>
    </tr>
}

Но это не сработало...

Ответ 1

Вам не нужно явно индексировать плоские данные. Если у вас есть

<input type='text' name='data' value='George' />
<input type='text' name='data' value='John' />
<input type='text' name='data' value='Paul' />
<input type='text' name='data' value='Ringo' />

Затем в вашем контроллере вы можете использовать

public ActionResult Create(string[] data)
{
    //data should now be a string array of 4 elements
    return RedirectToAction("Index");
}

Чтобы понять связующее, в основном работайте в обратном направлении. Когда вы отправляете свою форму, при условии, что она отправляется на ваш метод Create, связующее устройство моделирует параметры метода. Он увидит, что у вас есть массив строк в качестве параметра и он называется data. Он любит строки, потому что данные формы передаются в виде строк. Здесь нет никакой реальной работы, кроме как смотреть в коллекцию форм для элементов с ключом data. Все элементы, которые соответствуют, добавляются в массив и назначаются вашему параметру.

Это работает, потому что параметр имеет одно имя как элементы формы. Если имена не совпадают, вы получите null, потому что ничего не найдено с этим именем.

Если вы используете сильные представления (представления с явной моделью), вы можете использовать помощники MVC для их создания для ваших, а элементам ввода будет присвоено собственное имя для возврата к вашему объекту.

Например, если у вас была модель:

public class BandMembers
{
    public string[] data {get; set;}
}

И, на ваш взгляд, вы указали это как свою модель и использовали соответствующие HTML-помощники, ваш метод действий мог бы выглядеть следующим образом:

public ActionResult Create(BandMembers band)
{
    //band now has a property called 'data' with 4 elements
    return RedirectToAction("Index");
}

Это должно привести к созданию экземпляра объекта с именем band, который имеет свойство names с 4 значениями. Это работает, потому что модельное связующее видит параметр, называемый диапазоном, который не соответствует каким-либо известным ключам из коллекции форм, реализует его как сложный объект (а не строку, int, string [], int [] и т.д.) И исследует его члены. Он видит, что этот объект имеет строковый массив, называемый данными, и есть ключи в коллекции форм с этим именем. Он собирает значения, присваивает их свойству данных и назначает этот объект вашему параметру диапазона.

Теперь вы понимаете модели просмотра!

* Будьте осторожны, если бы вы использовали класс BandMembers в своем контроллере, но назвали его data, вы получили бы нуль. Это связано с тем, что модельное связующее находит элементы в коллекции форм с ключом data, но не может понять, как отбросить их из строк в объект BandMembers.

ИЗМЕНИТЬ

Что касается вашего редактирования с более глубоким примером, вот что я придумал.

Во-первых, моя модель просто так, что мы на одной странице. Я создал объект FormData, который содержит список разделов, чтобы действовать как совокупность объектов.

public class FormData
{
    public List<Section> Sections { get; set; }

    public FormData()
    {
    }
}

И мой класс Section.cs:

public class Section
{
    public bool IsDeleted { get; set; }
    public bool IsNew { get; set; }
    public int Id { get; set; }
    public int SectionOrder { get; set; }
    public string Title { get; set; }
    public string SubTitle { get; set; }

    public Section()
    {
    }
}

Использование EditorTemplate в вашем разделе упрощает визуализацию содержимого с помощью генерируемых вами индексов. Я издевался над проектом самостоятельно и подтвердил, что это работает. К сожалению, как вы видели, после удаления элемента ваши индексы будут неработоспособными. Итак, как вы это исправите? Конечно, вы можете пойти и прочитать индексы и переписать их, ИЛИ - просто не удаляйте их! То, что я сделал в моем проекте mock, добавляет новое свойство в раздел, называемом IsDeleted, и делает его скрытым. В обработчике JavaScript для удаления нажмите, я скрываю строку и обновляю скрытый ввод для этой строки IsDeleted input в 'true'. Когда я отправлю форму, теперь у меня будет полная коллекция вместе с удобным флагом, который позволит мне узнать, какие строки мне нужно удалить из моей модели.

Я создал тестовое представление, связанное с моделью FormData, которая содержит список.

@model MVCEditorTemplateDemo.Models.FormData
@using (Html.BeginForm())
{
    <table id="section-container">
        @Html.EditorFor(m => m.Sections)
    </table>

    @Ajax.ActionLink("Add Section", "GetNewSection", "Home", new AjaxOptions() { HttpMethod="POST", InsertionMode=InsertionMode.InsertAfter, UpdateTargetId="section-container" })
    <input type="submit" value="Submit" />
}

Да, редактор EditorFor берет коллекцию! Но как он знает, что с ним делать? Я создал папку в моем представлении/доме (может быть в Shared, если вы хотите использовать его через контроллеры), называемый EditorTemplates, в котором я помещаю частичный вид, называемый Section.cshtml. Имя важно - оно должно совпадать с именем объекта, который он будет отображать. Так как моя модель содержит объекты, называемые Section, мой EditorTemplate также следует называть секцией.

Вот как это выглядит (EditorTemplates\Section.cshtml):

@model MVCEditorTemplateDemo.Models.Section
<tr>
    <td><a href="#" class="edit-section"><span class="glyphicon glyphicon-question-sign"></span></a></td>
    <td>@Html.TextBoxFor(m => Model.SectionOrder, new { @class = "form-control" })</td>
    <td>@Html.TextBoxFor(m => Model.Title, new { @class = "form-control" })</td>
    <td>@Html.TextBoxFor(m => Model.SubTitle, new { @class = "form-control" })</td>
    <td>
        @Html.HiddenFor(m => Model.Id)
        @Html.HiddenFor(m => Model.IsNew)
        @Html.HiddenFor(m => Model.IsDeleted)
        <a href="#" class="delete-section"><span class="glyphicon glyphicon-remove"></span></a>
    </td>
</tr>

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

Хорошо, теперь у вас есть то, что вам нужно, чтобы ASP.NET MVC автоматически отображал ваши объекты и автоматически генерировал индексы. Поэтому давайте посмотрим на удаление этой строки.

В моем тестовом представлении я добавил раздел сценариев следующим образом:

@section scripts
{
    <script type="text/javascript">
        $(function () { 
            $("table").on("click", ".delete-section", function() {
                $(this).closest('tr').hide();
                $(this).prev('input').val('true');
            });
        });
    </script>
}

Это прекрасно работает. Когда пользователь нажимает кнопку "Удалить", они получают немедленную обратную связь от пользователя, что строка не прошла, и когда форма будет отправлена, у меня будет весь набор элементов, которые я предоставил с помощью удобного свойства, позволяющего мне знать, какие элементы мне нужно удалить из мое хранилище данных. Самая лучшая часть - мне никогда не приходилось перебирать мою коллекцию, и все мои индексы генерировались автоматически.

И это заканчивает мой ответ на ваш вопрос.

Но потом мне стало любопытно, что мне нужно сделать, чтобы создавать новые строки. Поэтому давайте взглянем на тестовое представление и на помощник Ajax.Action. Вы сразу заметите, что я поручаю браузеру выполнить запрос POST. Зачем? Поскольку браузеры могут кэшировать запросы GET для оптимизации производительности. Обычно вам небезразлично, поскольку вы обычно возвращаете один и тот же HTML для каждого запроса, но поскольку нам нужно включать специальное именование, наш HTML фактически каждый раз отличается (чтобы включить индекс в наши имена ввода). Остальное самоочевидно. Реальный трюк находится на стороне сервера - как мы можем вернуть часть, чтобы добавить строку в эту таблицу с надлежащей индексацией?

К сожалению, фреймворк, несмотря на то, что он нормально обнаруживает Views, кажется, падает при проверке в папках EditorTemplates и DisplayTemplates. Наше действие для этого немного грязнее, чем мы обычно имели, если бы мы не использовали шаблоны.

public ActionResult GetNewSection()
{
    var section = new Section() { IsNew = true };
    FakedData.Sections.Add(section);
    ViewData.TemplateInfo.HtmlFieldPrefix = string.Format("Sections[{0}]", FakedData.Sections.Count-1);
    return PartialView("~/Views/Home/EditorTemplates/Section.cshtml", section);
}

Хорошо, так что мы видим? Во-первых, я создаю новый объект Section, так как мне понадобится его для рендеринга EditorTemplate. Я добавил второе новое свойство IsNew, но на данный момент я на самом деле ничего не делаю. Мне просто нужен удобный способ увидеть, что было добавлено и удалено в моем методе POST.

Я добавляю этот новый раздел в мое хранилище данных (FakedData). Вместо этого вы можете отслеживать количество новых запросов по-другому - просто убедитесь, что он увеличивается каждый раз, когда вы нажимаете ссылку "Добавить раздел".

Теперь за трюк. Поскольку мы возвращаем частичное представление, у него нет контекста родительской модели. Если мы просто вернем шаблон только с переданным сектором, он не будет знать его часть большей коллекции и не будет соответствующим образом генерировать имена. Поэтому мы рассказываем, где мы находимся в поле HtmlFieldPrefix. Я использую свое хранилище данных для отслеживания нужного индекса, но опять же, это может произойти откуда угодно, и если вы добавите свойство IsNew, вы сможете добавить новые (и не удаленные) записи в свой магазин на submit вместо.

Как я уже сказал, структура имеет немного проблем с поиском шаблона только на основе имени, поэтому нам нужно указать путь к нему, чтобы он мог корректно возвращаться. Не очень много общего, но это немного раздражает.

Теперь я добавил обработчик POST для тестового представления, и я уверенно получаю количество удаленных элементов, их добавление и общее количество. Просто помните, что строки могут быть новыми и удалены!

[HttpPost]
public ActionResult Test(FormData form)
{
    var sectionCount = form.Sections.Count();
    var deletedCount = form.Sections.Count(i => i.IsDeleted);
    var newItemCount = form.Sections.Count(i => i.IsNew);

    form.Sections = form.Sections.Where(s => !s.IsDeleted).ToList();
    FakedData = form;
    return RedirectToAction("Test");
}

И что это. У нас есть полный сквозной вывод вашей коллекции с правильными индексами, "удаление" строк, добавление новых строк, и нам не пришлось взламывать привязку к модели, манипулировать именами или прибегать к хакам JavaScript для повторного набора наши элементы на submit.

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