Правильный, идиоматический способ использования настраиваемых шаблонов редактора с моделями IEnumerable в ASP.NET MVC

Этот вопрос является продолжением для Почему мой DisplayFor не зацикливается на моем IEnumerable <DateTime> ?


Быстрое обновление.

Когда:

  • модель имеет свойство типа IEnumerable<T>
  • вы передаете это свойство в Html.EditorFor(), используя перегрузку, которая принимает только выражение лямбда
  • У вас есть шаблон редактора для типа T в разделе Views/Shared/EditorTemplates

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

Например, когда существует класс модели Order со свойством Lines:

public class Order
{
    public IEnumerable<OrderLine> Lines { get; set; }
}

public class OrderLine
{
    public string Prop1 { get; set; }
    public int Prop2 { get; set; }
}

И есть представление Views/Shared/EditorTemplates/OrderLine.cshtml:

@model TestEditorFor.Models.OrderLine

@Html.EditorFor(m => m.Prop1)
@Html.EditorFor(m => m.Prop2)

Затем, когда вы вызываете @Html.EditorFor(m => m.Lines) из представления верхнего уровня, вы получите страницу с текстовыми полями для каждой строки заказа, а не только.


Однако, как вы можете видеть в связанном вопросе, это работает только при использовании этой конкретной перегрузки EditorFor. Если вы укажете имя шаблона (чтобы использовать шаблон, не названный в честь класса OrderLine), тогда автоматическая обработка последовательности не произойдет, и вместо этого произойдет ошибка времени выполнения.

В этот момент вам нужно будет объявить свою собственную модель шаблона как IEnumebrable<OrderLine> и вручную перебрать ее элементы так или иначе, чтобы вывести все из них, например.

@foreach (var line in Model.Lines) {
    @Html.EditorFor(m => line)
}

И вот где начинаются проблемы.

Элементы управления HTML, сгенерированные таким образом, имеют одинаковые идентификаторы и имена. Когда вы позже добавите POST, связующее устройство модели не сможет построить массив OrderLine s, а объект модели, который вы получите в методе HttpPost в контроллере, будет null.
Это имеет смысл, если вы посмотрите на выражение лямбда - это действительно не связывает объект, который строится с местом в модели, из которой он приходит.

Я пробовал различные способы итерации по элементам, и, казалось бы, единственный способ - переопределить модель шаблона как IList<T> и перечислить его с помощью for:

@model IList<OrderLine>

@for (int i = 0; i < Model.Count(); i++)
{ 
    @Html.EditorFor(m => m[i].Prop1)
    @Html.EditorFor(m => m[i].Prop2)
}

Затем в представлении верхнего уровня:

@model TestEditorFor.Models.Order

@using (Html.BeginForm()) {
    @Html.EditorFor(m => m.Lines, "CustomTemplateName")
}

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


Пока это работает, это кажется очень неправильным.

Каков правильный, идиоматический способ использования настраиваемого шаблона редактора с EditorFor, сохраняя при этом все логические ссылки, которые позволяют движку генерировать HTML, подходящий для привязки модели?

Ответ 1

После обсуждения с Erik Funkenbusch, что привело к просмотру исходного кода MVC, казалось бы, есть два более приятных (правильных и идиоматических?) способа сделать это.

Оба включают предоставление правильного префикса имени html для помощника и генерируют HTML, идентичный выходу по умолчанию EditorFor.

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

В следующих примерах предположим, что у вас уже есть два шаблона для класса OrderLine: OrderLine.cshtml и DifferentOrderLine.cshtml.


Способ 1 - Использование промежуточного шаблона для IEnumerable<T>

Создайте шаблон-помощник, сохранив его под любым именем (например, "ManyDifferentOrderLines.cshtml" ):

@model IEnumerable<OrderLine>

@{
    int i = 0;

    foreach (var line in Model)
    { 
        @Html.EditorFor(m => line, "DifferentOrderLine", "[" + i++ + "]")
    }
}

Затем вызовите его из основного шаблона заказа:

@model Order

@Html.EditorFor(m => m.Lines, "ManyDifferentOrderLines")

Метод 2 - Без промежуточного шаблона для IEnumerable<T>

В основном шаблоне заказа:

@model Order

@{
    int i = 0;

    foreach (var line in Model.Lines)
    {
        @Html.EditorFor(m => line, "DifferentOrderLine", "Lines[" + i++ + "]")
    }
}

Ответ 2

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

Сказав, что , самым простым решением является пользовательский DataType. MVC использует DataTypes в дополнение к UIHints и typenames. См:

Пользовательский редакторTemplate не используется в MVC4 для DataType.Date

Итак, вам нужно только сказать:

[DataType("MyCustomType")]
public IEnumerable<MyOtherType> {get;set;}

Затем вы можете использовать MyCustomType.cshtml в своих шаблонах редактора. В отличие от UIHint, это не страдает от отсутствия поддержки IEnuerable. Если ваше использование поддерживает тип по умолчанию (например, "Телефон" или "Электронная почта", вместо этого предпочитает использовать существующую нумерацию типов). Кроме того, вы можете получить свой собственный атрибут DataType и использовать DataType.Custom в качестве базы.

Вы также можете просто обернуть свой тип другим типом, чтобы создать другой шаблон. Например:

public class MyType {...}
public class MyType2 : MyType {}

Тогда вы можете легко создать MyType.cshtml и MyType2.cshtml, и вы всегда можете рассматривать MyType2 как MyType для большинства целей.

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

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

Это позволяет создать шаблон CreateMyType и шаблон EditMyType, отличный от отдельных позиций (которые вы можете комбинировать с предыдущим предложением).

Еще один вариант: если вы не используете DisplayTemplates для этого типа, вы можете использовать DisplayTempates для вашего альтернативного шаблона (при создании настраиваемого шаблона это просто соглашение.. при использовании встроенного шаблона, тогда будет просто создавать версии дисплея). Конечно, это противоречит интуиции, но оно решает проблему, если у вас есть только два шаблона для того же типа, которые вам нужно использовать, без соответствующего шаблона отображения.

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

@model IEnumerable<MyType>
@{ var model = Model.ToArray();}
@for(int i = 0; i < model.Length; i++)
{
    <p>@Html.TextBoxFor(x => model[i].MyProperty)</p>
}

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

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

Итак, чтобы ответить на ваш вопрос. Правильным, идиоматическим способом было бы перепроектировать ваши контроллеры и представления, чтобы эта проблема не существовала. запрещая, чтобы выбирал наименее оскорбительный "взлом" для достижения желаемого.. p >

Ответ 3

Кажется, нет более простого способа достижения этого, чем описанного в ответе @GSerg. Странно, что команда MVC не придумала менее грязный способ сделать это. Я сделал этот метод расширения, чтобы инкапсулировать его как минимум в некоторой степени:

public static MvcHtmlString EditorForEnumerable<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string templateName)
{
    var fieldName = html.NameFor(expression).ToString();
    var items = expression.Compile()(html.ViewData.Model);
    return new MvcHtmlString(string.Concat(items.Select((item, i) => html.EditorFor(m => item, templateName, fieldName + '[' + i + ']'))));
}

Ответ 4

Вы можете использовать атрибут UIHint для прямого просмотра MVC, который вы хотите загрузить для редактора. Таким образом, ваш объект Order будет выглядеть так, используя UIHint

public class Order
{
    [UIHint("Non-Standard-Named-View")]
    public IEnumerable<OrderLine> Lines { get; set; }
}