Pull to refresh

ASP.NET MVC в крупных проектах. Введение: Model Binding

Reading time5 min
Views13K

Вместо вступления


Пока готовилась эта статья, вышел замечательный пост 1andy Организуем view models в ASP.NET MVC, в котором разобрано многое из того, что я хотел бы поведать читателю. По этой причине я решил опустить большое вводное вступление и перейти непосредственно к практическим советам.

Однако, для начала придется все-таки немного помучиться с теорией.

Во-первых, определимся с некоторыми терминами:
  • Presenter — слой логики, управляющий представлением. За основу взят "обрезанный" presenter из шаблона MVP. Насколько он обрезан будет понятно позже. Скажу лишь, что часто употребляют термин ViewModel (как, например, в приведенной выше статье), но у нас будет нечто большее, чем типичный ViewModel (и меньшее, чем типичный Presenter), поэтому я отказался от его использования.
  • Объект Верхнего Уровня, ОВУ — что это такое, будет лучше понятно в следующих частях. Вкратце, ОВУ — модель, presenter или view, вызываемые непосредственно из контроллера
  • View, Представление — как ни странно, данный термин тоже иногда приходится уточнять. Под View в ASP.NET MVC я подразумеваю совокупность presenter'а и шаблона, написанного с использованием Razor


Во-вторых, несколько вводных:
  1. В нашем проекте мы не будем упрощать себе жизнь всяческими AutoMapper'ами и ORM'ами.

    Вообще, это хорошая практика, если вы делаете что-то критичное по скорости. Поскольку я работаю над проектами, где время выполнения запроса (как HTTP, так и SQL) является критичным, максимум, что я могу себе позволить в большинстве случаев — это маппинг сущностей из БД на объекты с максимально упрощенными правилами. Для этого я использую собственную обертку над Dapper.NET
  2. По ТЗ у нас есть большое количество модулей с непересекающимся функционалом. Каждый контроллер, каждый компонент приложения сильно отличается от остальных. На практике это означает, что время, выигрываемое при использовании копи-паста и генерации кода, не превышает время, затрачиваемое на доработку результатов этих действий
  3. Наши представления будут содержать достаточно большое количество логики. Большая ее часть, несомненно, будет вынесена из шаблона в presenter, но, к сожалению, совсем от нее избавиться не удастся.
  4. К приложению предъявляются достаточно жесткие требования по поддержке браузеров. Это означает, что мы должны минимизировать использование CSS-селекторов и использовать "Unobtrusive JavaScript", сохраняя работоспособность при отключенном JS


Model Data Binding


Увы, прежде, чем двинуться вперед, нам необходимо в общих чертах понять, как работает механизм привязки данных в ASP.NET MVC и рассмотреть некоторые проблемы, с ним связанные.

Класс HtmlHelper представляет методы, для генерации HTML разметки вашей страницы. Большинство из них связаны с управляющими тегами форм (input, label, и т.п.). Именно они нас и интересуют
Эти методы можно разделить на две категории, назовем их "генераторы по сущности" и "генераторы по выражению". Для примера возьмем метод вывода текстового поля (<input type="text" />).

Генератор по сущности — это метод Html.TextBox(). Фактически, он никакого отношения к привязке данных не имеет — вы сами его заполняете, сами указываете его текущее значение.

Генератор по выражению, Html.TextBoxFor(), уже гораздо интереснее. Первое, что можно заметить — вместо передачи конкретного значения в поле value мы передам функцию, возвращающую это значение из нашей модели (в данном случае это будет presenter, но об этом позже). Если копнуть чуть глубже, то мы увидим, что на самом деле мы передаем не саму функцию, а некий Expression — класс, описывающий нашу функцию.

Что же это дает?
Expression позволяет нам получить метаданные вызываемой функции, в частности, выделить ее составляющие части. ASP использует эту информацию для построения полей name и id генерирующегося тега.
Схематично эти поля получаются так:
  1. Из ViewData текущего представления берется префикс этого представления (об этом ниже)
  2. Из Expression'а генерируется строка, которая повторяет код переданной функции без вызова модели.
    Например, из
    Html.TextBoxFor(m => m.SomeArray[someIndexer])
    при someIndexer = 2 мы получим строку "SomeArray[2]".
    Как конкретно это работает объяснять не буду, можете сами посмотреть в исходниках. Но есть один важный момент:
    выражение корректно генерируется только при глубине поля < 3
    Т.е. выражение
    Html.TextBoxFor(m => m.SubModel.SomeArray[someIndexer])
    уже может не сработать
  3. Префикс, полученный на шаге 1, если он есть, объединяется со строкой, полученной в шаге 2. Результат есть значение поля name результирующего тега.
  4. Далее в этом значении все символы '.', '[', ']' заменяются на подчеркивания ('_'). Это есть значение поля id.
Как я и обещал, рассмотрим откуда берется префикс представления.
Когда вы генерируется представление из контроллера вызовом this.View(), создается новый объект класса ViewDataDictionary. В нем есть поле TemplateInfo, в котором хранится строка HtmlFieldPrefix. Это и есть префикс текущего представления. По-умолчанию она пуста.
Далее, в ASP.NET MVC есть два способа вызвать из одного представления другое — либо выполнить его в полностью изолированном контексте (так делает HtmlHelper.Partial()), либо отрендерить как "субпредставление" текущего — это методы HtmlHelper.DisplayFor() и HtmlHelper.EditorFor().
Второй случай интересен. Вызов этих методов очень похож на вызов генератора по выражению для отдельных тегов, но вместо результирующего тега мы получаем представление, отрендеренное с новым префиксом шаблона.

Небольшой пример
Пусть есть представление с шаблоном MainView.cshtml, вызывающееся из контроллера:
@model App.Models.MainModel
<h1>MainView.cshtml</h1>

@using (this.Html.BeginForm())
{
    this.Html.TextBoxFor(m => m.SomeText)
    this.Html.EditorFor(m => m.SubModel, "SubView")
}

И представление с шаблоном SubView.cshtml, лежащим в Views/Shared/EditorTemplates:
@model App.Models.SubModel
<h2>SubView.cshtml</h2>

this.Html.TextBoxFor(m => m.SubText)

В результате рендеринга получились бы следующие теги:
  1. <input type="text" id="SomeText" name="SomeText" />
  2. <input type="text" id="SubModel_SubText" name="SubModel.SubText" />

Как мы видим, представлению SubView передался префикс SubModel, соответствующий выражению, переданному в метод HtmlHelper.EditorFor().


Итак, разметка отрендерена, клиент ввел данные, запрос отправлен на сервер. Теперь рассмотрим, как пришедшие данные обрабатываются и превращаются в модели (будем рассматривать случай, когда модель является пользовательским классом).
  1. Механизм роутинга определяет метод контроллера, наиболее подходящий для обработки входящего запроса.
  2. Анализируется сигнатура выбранного метода. Параметры, совпадающие по имени с переданными данными, заполняются входящими данными в обход привязки.
  3. Из оставшихся параметров вычисляется класс модели, которую ожидает action.
  4. Создается объект класса модели путем вызова конструктора по-умолчанию (с пустыми параметрами).
    Важно: конструктор должен быть именно дефолтным, не содержащим параметров. Опциональные параметры не пройдут
  5. Для каждого входящего поля, не содержащего точку ("."), заполняется одноименное поле в созданной модели
  6. Далее, для всех оставшихся полей ищутся соответствующие по префиксу поля модели. Заполняются они по тем же принципам, начиная с шага 3

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


Заключение

На этом краткая теоретическая часть завершена, мы выучили минимум, необходимый для того, чтобы начать эффективно использовать ASP.NET MVC, и можем приступать к практике
Tags:
Hubs:
+5
Comments1

Articles

Change theme settings