Главная страница   /   3.3. Использование строго типизированных шаблонов (ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

3.3. Использование строго типизированных шаблонов

По мере того, как вы будете продвигаться вперед к использованию строго типизированных представлений, в основе которых лежит модель представления, вы будете встречаться все с большим количеством возникающих паттернов. Если объект модели представления обладает булевым свойством, то вам необходимо отобразить на форме элемент типа CheckBox. E-mail адреса всегда должны отображаться так же, как и поля ввода пароля, и т.д. Очень редко элемент ввода данных включает в себя соответствующее сообщение валидации данных.

Методы расширений класса HtmlHelper хороши для отдельных фрагментов HTML-элементов, но, как правило, они не подходят в тех случаях, когда сгенерированная HTML-разметка начинает все более усложняться и содержит все большее количество разнообразных элементов. ASP.NET MVC дает нам возможность принимать решения о способе отображения представлений на основании метаданных модели. Примером этого служит наделение нашей модели представления классом RequiredAttribute для обеспечения автоматической валидации ее данных. Фреймворк также предоставляет способы генерации фрагментов HTML-разметки, основанные на свойствах нашей модели представления.

Начиная с релиза ASP.NET MVC 2 команда разработчиков MVC паттерна реализовала такую возможность представления, которая находится где-то посередине между методами расширений HtmlHelper и полномасштабными partial классами. Этой возможностью являются шаблонизированные вспомогательные методы, которые созданы для того, чтобы содействовать генерации HTML-разметки, основанной на строго типизированных представлениях. Шаблонизированные вспомогательные методы могут использоваться для генерации HTML-разметки всей модели или конкретных составляющих модели.

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

Шаблоны EditorFor и DisplayFor

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

  • Html.Display("Message")
  • Html.DisplayFor(m => m.Message)
  • Html.DisplayForModel()
  • Html.Editor("UserName")
  • Html.EditorFor(m => m.UserName)
  • Html.EditorForModel()

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

Поскольку страница "Change Password" (Смена пароля) является простой, для генерации формы редактирования мы можем воспользоваться методом EditorForModel.

Листинг 3-10: Использование метода EditorForModel для простой модели
@using (Html.BeginForm()) { %>
<div>
	<fieldset>
		<legend>Account Information</legend>
		@Html.EditorForModel()
		<p>
			<input type="submit" value="Change Password" />
		</p>
	</fieldset>
</div>
}

Строка 5: Генерирует интерфейс редактирования для модели

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

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

Листинг 3-11: Модель для Change Password
public class ChangePasswordModel
{
	[Required]
	[DataType(DataType.Password)]
	[Display(Name = "Current password")]
	public string OldPassword { get; set; }
	[Required]
	[ValidatePasswordLength]
	[DataType(DataType.Password)]
	[Display(Name = "New password")]
	public string NewPassword { get; set; }
	[DataType(DataType.Password)]
	[Display(Name = "Confirm new password")]
	[Compare("NewPassword", ErrorMessage = "The new password" +
	" and confirmation password do not match.")]
	public string ConfirmPassword { get; set; }
}

Строка 3: Требует, чтобы пользователь задал значение

Строка 5: Элементы управления отображают методы полей

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

Но в нашем классе модели представления нам может понадобиться еще больший контроль над HTML-разметкой, нежели предоставляемый при помощи метаданных. Например, нам, возможно, потребуется заключить некоторые элементы в теги абзаца <p>. Для такого уровня индивидуального контроля, при котором нам необходимо компоновать отдельные элементы, мы можем использовать метод EditorFor.

Листинг 3-12: Использование метода EditorFor для дополнительного контроля внешнего вида
<p>
	@Html.EditorFor(m => m.OldPassword)
</p>
<p>
	@Html.EditorFor(m => m.NewPassword)
</p>
<p>
	@Html.EditorFor(m => m.ConfirmPassword)
</p>

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

Встроенные шаблоны

В комплект ASP.NET MVC входит набор встроенных шаблонов, как для шаблонов редактирования, так и для шаблонов отображения. Встроенные шаблоны отображения представлены в таблице 3-2.

Таблица 3-2: Шаблоны отображения в ASP.NET MVC
Шаблон отображения Описание
EmailAddress Выводит гиперссылку, добавляя mailto
HiddenInput Скрывает отображаемое значение
Html Выводит отформатированное значение модели
Text Выводит необработанное содержимое (использует шаблон String)
Url Для вывода гиперссылки сочетает модель и отформатированное значение модели
Collection Выполняет цикл по IEnumerable и выводит шаблон для каждого элемента
Boolean Выводит чекбокс для обычных булевых значений и выпадающий список для значений bool?
Decimal Форматирует значение, добавляя к нему два знака после запятой
String Выводит необработанное содержимое
Object Выполняет цикл по всем свойствам объекта и выводит шаблон отображения для каждого свойства

Каждый шаблон, за исключением шаблонов Collection и Object, выводит единичное значение. Шаблон Object выполняет цикл по каждому элементу коллекции ModelMetadata.Properties (которая, в свою очередь, наполняется посредством проверки открытых свойств по типу элемента) и выводит соответствующий шаблон отображения для каждого элемента. Шаблон Collection выполняет цикл по каждому элементу объекта модели и выводит корректный шаблон отображения для каждого элемента списка.

Шаблоны отображения, как и предполагалось, выводят такие элементы отображения в веб-браузере, как необработанный текст и теги якоря (anchor), тогда как шаблоны редактирования выводят элементы формы. Шаблоны редактирования, которые по умолчанию содержатся в ASP.NET MVC, приведены в таблице 3-3.

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

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

Таблица 3-3: Шаблоны редактирования в ASP.NET MVC
Шаблон отображения Описание
HiddenInput Использует метод расширения HtmlHelper.Hidden для отображения элемента <input type="hidden" />
MultilineText Использует метод расширения HtmlHelper.TextArea для отображения многострочного элемента ввода данных
Password Использует метод расширения HtmlHelper.Password для отображения элемента ввода пароля
Text Использует метод расширения HtmlHelper.TextBox для отображения элемента ввода текстовых данных
Collection Выполняет цикл по IEnumerable и выводит шаблон для каждого элемента с корректным индексом
Boolean Выводит чекбокс для обычных булевых значений и выпадающий список для значений bool?
Decimal Форматирует значение внутри текстового поля, добавляя к нему два знака после запятой
String Использует метод расширения HtmlHelper.TextBox для отображения элемента ввода текстовых данных
Object Выполняет цикл по всем свойствам объекта и выводит шаблон отображения для каждого свойства

Выбор шаблона

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

Шаблонизированные вспомогательные методы выполняют поиск шаблона в конкретных местах перед тем, как начать поиск шаблона по другому имени. Места, в которых выполняется поиск шаблонов, – это папки EditorTemplates и DisplayTemplates. Как и в случае с именами компонентов и представлений шаблонизированные методы вначале будут просматривать папку конкретного контроллера представления (или папки конкретной области и конкретного контроллера представления), а затем уже переходить к поиску в папке Views/Shared. Если шаблонизированный вспомогательный метод используется внутри конкретной области представления, то к таким папкам поиска относятся следующие папки:

  • <Area>/<ControllerName>/EditorTemplates/<TemplateName>.cshtml (или .vbhtml)
  • <Area>/Shared/EditorTemplates/<TemplateName>.cshtml (или .vbhtml)

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

  • <ControllerName>/EditorTemplates/<TemplateName>.cshtml (или .vbhtml)
  • Shared/EditorTemplates/<TemplateName>.cshtml (или .vbhtml)

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

Шаг Место поиска
1 Имя шаблона, передаваемое посредством шаблонизированных вспомогательных методов отображения и редактирования (по умолчанию значение null)
2 Значение ModelMetadata.TemplateHint (заполненное из атрибута [UIHint])
3 Значение ModelMetadata.DataTypeName (заполненное из атрибута [DataType])
4 Тип модели (в случае типа допускающего значение null используется один из указанных ниже типов)
Несложный тип (существует конвертер типа модели в тип String): String
Интерфейс IEnumerable: Collection
Любой другой интерфейс: Object
6 Рекурсивный поиск базовых типов, одного за другим, поиск Type.Name. Если элементом является IEnumerable, то выполняется поиск имени "Collection", в противном случае – имени "Object"

Например, предположим, что нам необходимо отобразить пользовательский шаблон ChangePasswordModel нашей модели для страницы "Change Password". У нас уже есть созданный объект модели, поэтому мы можем определить шаблон, совпадающий с названием типа модели, т.е. ChangePasswordModel. В связи с тем, что данный шаблон специфичен для нашего контроллера Account, мы поместим шаблон в папку EditorTemplates, находящуюся под папкой Account, как это показано на рисунке 3-3.

Рисунок 3-3: Шаблон ChangePasswordModel в папке EditorTemplates

Если нам необходимо, чтобы наш шаблон был виден для всех контроллеров, то нам необходимо поместить его в папку EditorTemplates, находящуюся в папке Shared, как это показано на рисунке 3-4.

Рисунок 3-4: Создание глобального шаблона редактирования Object в папке Shared

Несмотря на то, что шаблоны Razor наследуются от класса WebViewPage (файлы с расширением .cshtml), они не используют тот же файл _ViewStart.cshtml, который наследуют обычные представления конкретной страницы. Вместо этого вам приходится вручную устанавливать желаемый макет. В следующем разделе мы изучим то, как создавать пользовательские шаблоны и переопределять существующие шаблоны.

Настройка шаблонов

В основном существует две причины создания пользовательского шаблона:

  • Создание нового шаблона
  • Переопределение существующего шаблона

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

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

Листинг 3-13: Разметка шаблона ChangePasswordModel
model Guestbook.Models.ChangePasswordModel
<p>
	@Html.EditorFor(m => m.OldPassword)
</p>
<p>
	@Html.EditorFor(m => m.NewPassword)
</p>
<p>
	@Html.EditorFor(m => m.ConfirmPassword)
</p>

Строка 3: Генерирует шаблон редактирования для каждого свойства

Строка 5: Заключает шаблон редактирования в теги абзаца

Наш новый шаблон Object.cshtml просто использует существующие шаблоны EditorFor для каждого элемента, но при этом заключает каждый шаблон в теги абзаца. Но каково преимущество такой модели над использованием частичного представления?

Например, частичное представление должно выбираться по имени. Шаблоны выбираются исходя из метаданных модели посредством передачи в представление необходимых сведений для точного определения того, какой шаблон необходимо использовать. Кроме того, во ViewDataDictionary шаблонам предоставляется дополнительная информация, которую не получают частичные представления и другие страницы, и которая содержится в свойстве ViewData.ModelMetadata. Только шаблоны обладают свойством ModelMetadata, заполняемым ASP.NET MVC, для частичных представлений и представлений это свойство имеет значение null.

При помощи свойства ModelMetadata вы можете получить доступ ко всем метаданным, генерируемым из провайдера метаданных модели. К этой информации относится информация о типе модели и ее свойствах.

К информации о типе модели относятся свойства, перечисленные в таблице 3-4.

Таблица 3-4: Свойства класса ModelMetadata, которые предоставляются посредством отображения
Свойства класса ModelMetadata Описание
Model Значение модели
ModelType Тип модели
ContainerType Тип контейнера модели, если свойство Model является свойством родительского типа
PropertyName Имя свойства, представленное значением Model
Properties Коллекция объектов метаданных модели, которая описывает свойства этой модели
IsComplexType Свойство, определяющее является ли данная модель сложной
IsNullableValueType Свойство, определяющее допускает ли тип значение null

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

Таблица 3-5: Свойства класса ModelMetadata, которые предоставляются посредством DataAnnotations
Свойства класса ModelMetadata Источник данных
ConvertEmptyStringToNull System.ComponentModel.DataAnnotations.DisplayFormatAttribute
DataTypeName System.ComponentModel.DataAnnotations.DataTypeAttribute
DisplayFormatString System.ComponentModel.DataAnnotations.DisplayFormatAttribute
DisplayName System.ComponentModel.DataAnnotations.DisplayAttribute или System.ComponentModel.DisplayNameAttribute
EditFormatString System.ComponentModel.DataAnnotations.DisplayFormatAttribute
HideSurroundingHtml System.Web.Mvc.HiddenInputAttribute
IsReadOnly System.ComponentModel.ReadOnlyAttribute или System.ComponentModel.DataAnnotations.EditableAttribute
IsRequired System.ComponentModel.DataAnnotations.RequiredAttribute
NullDisplayText System.ComponentModel.DataAnnotations.DisplayFormatAttribute
TemplateHint System.ComponentModel.DataAnnotations.UIHintAttribute
ShowForDisplay System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute
ShowForEdit System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute
Description System.ComponentModel.DataAnnotations.DisplayAttribute
ShortDisplayName System.ComponentModel.DataAnnotations.DisplayAttribute
Watermark System.ComponentModel.DataAnnotations.DisplayAttribute
Order System.ComponentModel.DataAnnotations.DisplayAttribute

В нашем пользовательском шаблоне мы можем проанализировать эти метаданные модели для того, чтобы выполнить настройку вывода HTML-страницы. Помимо свойств, перечисленных в таблицах 3-4 и 3-5, объект класса ModelMetadata обладает свойством AdditionalValues типа IDictionary<string, object>, которое может содержать дополнительные метаданные, заполненные с помощью пользовательского провайдера метаданных модели. Например, если нам нужно отобразить символ * для обязательных полей, то нам необходимо просто проверить свойство IsRequired в нашем пользовательском шаблоне. Или же мы могли бы дополнить нашу модель атрибутом DataType, который имеет значение DataType.DateTime, и тем самым создать пользовательский шаблон, который выводит даты в виджете выбора даты.

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

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

Листинг 3-14: Создание пользовательского шаблона Object
@foreach (var prop in ViewData.ModelMetadata.Properties
	.Where(pm => pm.ShowForEdit && !ViewData.TemplateInfo.Visited(pm))) {
<div class="editor-field-container">
	@if (!String.IsNullOrEmpty(Html.Label(prop.PropertyName).ToHtmlString())) {
	<div class="editor-label">
		@Html.Label(prop.PropertyName):
	</div>
	}
	<div class="editor-field">
		@Html.Editor(prop.PropertyName)
		@Html.ValidationMessage(prop.PropertyName, "*")
	</div>
	<div class="cleaner"></div>
</div>
}

Строка 6: Отображает надпись для свойства

Строка 10: Отображает шаблон редактирования

Строка 11: Отображает сообщение валидации

Мы создаем цикл for для последовательного прохождения по ModelMetadata.Properties, которые должны выводиться для редактирования и не отображались на экране до этого момента, путем отображения надписи, шаблона редактирования и сообщения валидации для каждого свойства добавлением в блок тега div. Наконец, мы добавляем пустой тег div, который сбрасывает float стиль, применяемый для достижения стиля столбца. Окончательный внешний вид страницы показан на рисунке 3-5.

Рисунок 3-5: Внешний вид страницы, основанной на плавающем стиле, порожденный пользовательским шаблоном Object

Путем помещения логики отображения в глобальные шаблоны мы можем с легкостью стандартизировать внешний вид режимов отображения и редактирования наших представлений в рамках всего сайта. Для областей, которым необходима пользовательская настройка, мы можем выборочно переопределить или предоставить новые шаблоны. Благодаря стандартизации и инкапсуляции логики отображения в одном месте нам нужно будет писать меньшее количество кода и для того, чтобы повлиять на весь наш сайт, нам потребуется выполнить доработки всего лишь в одном месте. Если мы захотим изменить наш виджет выбора даты, то мы можем просто перейти к шаблону date-time и легко изменить внешний вид и поведение нашего сайта.