Как привязать ModelExpression к ViewComponent в ASP.NET Core?

Я хотел бы связать выражение модели (например, свойство) с компонентом представления — так же, как с помощью вспомогательного HTML (например, @Html.EditorFor()) или вспомогательного тега (например, <partial for />) — и повторно использовать эту модель в представлении с помощью вложенных помощников HTML и/или тегов. Я могу определить ModelExpression как параметр компонента представления и извлечь из него множество полезных метаданных. Помимо этого, я начинаю сталкиваться с блокпостами:

  • Как передать и привязать к базовой исходной модели, например. помощник тега asp-for?
  • Как убедиться, что метаданные свойств (например, атрибуты проверки) из ViewData.ModelMetadata учитываются?
  • Как мне собрать полное HtmlFieldPrefix для атрибута поля name?

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

Сценарий

Значения списка <select> должны быть заполнены через репозиторий данных. Предположим, что нецелесообразно или нежелательно заполнять возможные значения как часть, например. исходная модель представления (см. «Альтернативные параметры» ниже).

Образец кода

/Components/SelectListViewComponent.cs

using system;
using Microsoft.AspNetCore.Mvc.Rendering;

public class SelectViewComponent 
{

  private readonly IRepository _repository;

  public SelectViewComponent(IRepository repository) 
  {
    _repository = repository?? throw new ArgumentNullException(nameof(repository));
  }

  public IViewComponentResult Invoke(ModelExpression aspFor) 
  {
    var sourceList = _repository.Get($"{aspFor.Metadata.Name}Model");
    var model = new SelectViewModel() 
    {
      Options = new SelectList(sourceList, "Id", "Name")
    };
    ViewData.TemplateInfo.HtmlFieldPrefix = ViewData.TemplateInfo.GetFullHtmlFieldName(modelMetadata.Name);
    return View(model);
  }

}

Примечания

  • Использование ModelExpression не только позволяет мне вызывать компонент представления с выражением модели, но также дает мне много полезных метаданных через отражение, таких как параметры проверки.
  • Имя параметра for недопустимо в C#, так как это зарезервированное ключевое слово. Таким образом, вместо этого я использую aspFor, который будет отображаться в вспомогательном формате тега как asp-for. Это немного хак, но дает знакомый интерфейс для разработчиков.
  • Очевидно, что код и логика _repository будут значительно меняться в зависимости от реализации. В моем собственном случае я фактически извлекаю аргументы из некоторых пользовательских атрибутов.
  • GetFullHtmlFieldName() не создает полное имя поля HTML; он всегда возвращает любое значение, которое я ему передаю, которое является просто именем выражения модели. Подробнее об этом в разделе «Проблемы» ниже.

/Models/SelectViewModel.cs

using Microsoft.AspNetCore.Mvc.Rendering;

public class SelectViewModel {
  public SelectList Options { get; set; }
}

Примечания

  • Технически в этом случае я мог просто вернуть SelectList непосредственно в представление, так как оно будет обрабатывать текущее значение. Однако если вы привяжете свою модель к вспомогательному тегу asp-for вашего <select>, то он автоматически активирует multiple, что является поведением по умолчанию при привязке к модели коллекции.

/Views/Shared/Select/Default.cshtml

@model SelectViewModel

<select asp-for=@Model asp-items="Model.Options">
  <option value="">Select one…</option>
</select>

Примечания

  • Технически значение @Model возвращает SelectViewModel. Если бы это был <input />, это было бы очевидно. Эта проблема скрыта из-за того, что SelectList определяет правильное значение, предположительно из ViewData.ModelMetadata.
  • Вместо этого я мог бы установить aspFor.Model, например. свойство UnderlyingModel на SelectViewModel. Это приведет к тому, что имя поля HTML будет {HtmlFieldPrefix}.UnderlyingModel, и все равно не удастся получить какие-либо метаданные (например, атрибуты проверки) из исходного свойства.

Вариации

Если я не устанавливаю HtmlFieldPrefix и помещаю компонент представления в контекст, например. a <partial for /> или @Html.EditorFor(), тогда имена полей будут правильными, так как HtmlFieldPrefix определяется в родительском контексте. Однако, если я размещу его непосредственно в представлении верхнего уровня, я получу следующую ошибку из-за того, что HtmlFieldPrefix не определено:

ArgumentException: имя поля HTML не может быть нулевым или пустым. Вместо этого используйте методы Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor или Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper``1.EditorFor с непустым значением аргумента htmlFieldName. (Параметр «выражение»)

Проблемы

  • HtmlFieldPrefix не заполняется должным образом полным значением. Например, если имя свойства модели — Country, оно всегда будет возвращать Country, даже если фактический путь к модели, скажем, ShippingAddress.Country или Addresses[2].Country.
  • Функция Ненавязчивая проверка jQuery не срабатывает. Например, если свойство, к которому оно привязано, помечено как [Required], то оно здесь не помечается. Предположительно, это связано с тем, что оно привязано к SelectViewModel, а не к родительскому свойству.
  • Исходная модель никак не передается в представление компонента представления; SelectList может вывести исходное значение из ViewData, но оно потеряно для просмотра. Я мог бы передать aspFor.Model через модель представления, но у него не будет доступа к исходным метаданным (таким как атрибуты проверки).

Альтернативные варианты

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

  • Вспомогательные функции тегов. Этого легко добиться с помощью вспомогательных функций тегов. Внедрение зависимостей, таких как репозиторий, в помощник тега менее элегантно, поскольку нет способа создать экземпляр помощника тега через корень композиции, как это можно сделать, например. IViewComponentActivator.
  • Контроллеры. В этом упрощенном примере также можно определить исходную коллекцию в модели представления верхнего уровня рядом с фактическим свойством (например, Country для значения, CountryList для параметров). Это может быть непрактично или элегантно в более сложных примерах.
  • AJAX: значения можно получить с помощью вызова JavaScript к веб-службе, привязав выходные данные JSON к элементу <select> на клиенте. Я использую этот подход в других приложениях, но здесь он нежелателен, так как я не хочу раскрывать весь спектр потенциальной логики запросов в общедоступном интерфейсе.
  • Явные значения. Я мог бы явно передать модель parent вместе с ModelExpression, чтобы воссоздать родительский контекст в компоненте представления. Это немного глупо, поэтому я хотел бы сначала обыграть подход ModelExpression.

Прошлое исследование

Этот вопрос задавали (и отвечали) раньше:

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

Я гоняюсь за кроликом в норе? Или есть варианты, которые может решить более глубокое понимание выражения модели сообществом?


person Jeremy Caney    schedule 20.11.2019    source источник


Ответы (1)


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

Даже если вы решите техническую проблему путем извлечения полностью определенного HtmlFieldPrefix из ModelExpression, более глубокая проблема будет концептуальной. Предположительно, компонент представления соберет дополнительные данные и передаст их в представление через новую модель представления — например, SelectViewModel, предложенную в вопросе. В противном случае нет никакой реальной пользы от использования компонента представления. Однако в представлении компонента представления нет логического способа сопоставить свойства дочерней модели представления обратно с родительской моделью представления.

Так, например, предположим, что в вашем родительском представлении вы привязываете компонент представления к свойству UserViewModel.Country:

@model UserViewModel

<vc:select asp-for="Country" />

Затем, к каким свойствам вы привязываетесь в дочернем представлении?

@model SelectViewModel

<select asp-for=@??? asp-items="Model.Options">
  <option value="">Select one…</option>
</select>

В моем первоначальном вопросе я предложил @Model, что похоже на то, что вы сделали бы, например, в шаблон редактора, вызываемый через @Html.EditorFor():

<select asp-for=@Model asp-items="Model.Options">
  <option value="">Select one…</option>
</select>

Это может вернуть правильные атрибуты id и name, поскольку он возвращается к HtmlFieldPrefix из ViewData. Но у него не будет доступа к каким-либо, например. атрибуты проверки данных, поскольку они привязаны к SelectViewModel, а не к исходному свойству UserViewModel.Country, как в шаблоне редактора.

Точно так же вы можете передать ModelExpression.Model вниз, например. собственность SelectViewModel.Model

<select asp-for=@Model asp-items="Model.Options">
  <option value="">Select one…</option>
</select>

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

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

Очевидно, можно представить, что Microsoft встраивает инструменты более низкого уровня в ModelExpression и базовые реализации вспомогательных функций тегов asp-for, которые позволяют ретранслировать объекты ModelExpression по всей линии. В качестве альтернативы они могут установить ключевое слово, например @ParentModel, которое позволяет ссылаться на модель из родительского представления. Однако при его отсутствии это не представляется возможным.

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

person Jeremy Caney    schedule 28.12.2019