ASP.NET MVC 4
Адам Фриман
Использование метаданных модели
Как вы уже убедились, шаблонные вспомогательные методы ничего не знают о нашем приложении и его типах моделей, и поэтому мы в конечном итоге получаем не тот HTML, который требуется. Мы хотим сохранить преимущества простых представлений, но мы должны улучшить качество вывода вспомогательных методов, прежде чем начнем использовать их всерьез.
Нельзя взваливать вину за такой результат на шаблонные вспомогательные методы; они генерируют HTML, основываясь на наиболее точных предположениях относительно того, что мы ожидаем. К счастью, с помощью метаданных модели мы можем предоставить вспомогательным методам информацию о том, как обрабатывать наши типы моделей. Метаданные записываются с помощью атрибутов C#, где значения атрибутов и параметров предоставляют различные инструкции вспомогательным методам представлений. Метаданные применяются к классу модели, к которому обращаются вспомогательные методы, когда генерируют элементы HTML. В следующих разделах мы продемонстрируем, как с помощью метаданных можно предоставлять инструкции вспомогательным методам для создания элементов label
, display
и editor
.
Контролируем редактируемость и видимость свойств с помощью метаданных
Мы не хотим, чтобы пользователи могли видеть или редактировать свойство PersonId
класса Person
. В большинстве классов моделей есть по крайней мере одно такое свойство, которое часто связано с механизмом хранения – например, первичный ключ, который находится под контролем реляционной базы данных (что мы продемонстрировали, когда создавали приложение SportsStore).
Можно использовать атрибут HiddenInput
, который сообщит вспомогательному методу, что необходимо отобразить как скрытое поле. В листинге 20-11 показано, как мы применили HiddenInputAttribute
к классу Person
.
Листинг 20-11: Используем атрибут HiddenInput
using System;
using System.Web.Mvc;
namespace HelperMethods.Models
{
public class Person
{
[HiddenInput]
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
// ...other types omitted from Listing 20-for brevity...
}
Когда к свойству применен этот атрибут, вспомогательные методы Html.EditorFor
и Html.EditorForModel
будут визуализировать для него нередактируемый элемент. На рисунке 20-6 показан результат запуска приложения и перехода по ссылке к /Home/CreatePerson
.
Рисунок 20-6: Визуализация нередактируемого элемента для свойства
Значение свойства PersonId
выводится, но пользователь не может его редактировать. Для этого свойства генерируется следующий HTML:
<div class="editor-field">
0
<input id="PersonId" name="PersonId" type="hidden" value="0" />
</div>
Значение свойства (в данном случае 0
) просто визуализируется, но вспомогательный метод также создает для него скрытый элемент input
; это полезный элемент в формах HTML, потому что он гарантирует, что при отправке формы мы предоставляем значение для данного свойства (мы вернемся к этой теме, когда будем рассматривать связывание данных в главе 22 и валидацию в главе 23). Если мы хотим полностью скрыть свойство, можно установить свойству DisplayValue
в атрибуте HiddenInput
значение false
, как показано в листинге 20-12.
Листинг 20-12: Скрываем свойство с помощью атрибута HiddenInput
public class Person
{
[HiddenInput(DisplayValue = false)]
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
Когда мы используем вспомогательный метод Html.EditorForModel
для объекте Person
, он создает скрытое поле ввода, чтобы при отправке формы значение свойства PersonId
также было отправлено, но метка и числовое значение были опущены. Это имеет такой же эффект, как и скрытие свойства PersonId
от пользователя, как показано на рисунке 20-7.
Рисунок 20-7: Скрытие свойств объекта модели от пользователя
Если вы хотите визуализировать HTML для отдельных свойств, можно создать скрытый элемент input
для свойства PersonId
с помощью вспомогательного метода Html.EditorFor
:
@Html.EditorFor(m => m.PersonId)
Свойство HiddenInput
обнаруживается, и если DisplayValue
имеет значение true
, то будет сгенерирован следующий код HTML:
<input id="PersonId" name="PersonId" type="hidden" value="1" />
Исключаем свойство из формирования шаблонов
Если вы не хотите создавать HTML для свойства, можно использовать атрибут
ScaffoldColumn
. В то время как атрибутHiddenInput
включает значение свойства в скрытый элемент ввода,ScaffoldColumn
означает, что свойство не будет использоваться при формировании шаблона. Вот пример использования атрибута:[ScaffoldColumn(false)] public int PersonId { get; set; }
Когда вспомогательный метод для модели увидит атрибут
ScaffoldColumn
, он полностью пропустит это свойство; для него не будет создан скрытый элемент ввода, и информация из этого свойства не будет включена в HTML. Сгенерированный HTML будет таким же, как если бы мы использовали атрибутHiddenInput
, но при отправки формы значение для этого свойства отправляться не будет (это может оказывать влияние на связывание данных, которое мы обсудим позже в этой главе). АтрибутScaffoldColumn
не влияет на вспомогательные методы, работающие с одним свойством, такие какEditorFor
. Если мы вызовем в представлении@Html.EditorFor(m => m.PersonId)
, для свойстваPersonId
будет создан элементeditor
, даже если к нему применен атрибутScaffoldColumn
.
Используем метаданные для создания элементов label
По умолчанию вспомогательные методы Label
, LabelFor
, LabelForModel
и EditorForModel
используют имена свойств как содержимое для элементов label
, которые они генерируют. Например, если мы визуализируем метку таким образом:
@Html.LabelFor(m => m.BirthDate)
генерируется следующий элемент HTML:
<label for="BirthDate">BirthDate</label>
Конечно, мы не всегда хотим отображать пользователю названия свойств.Для этого можно применить атрибут DisplayName
из пространства имен System.ComponentModel.DataAnnotations
и передать в него значение для свойства Name
. В листинге 20-13 показано, как мы применили этот атрибут к классу Person
.
Листинг 20-13: Используем атрибут DisplayName
для создания метки
using System;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
namespace HelperMethods.Models
{
[DisplayName("New Person")]
public class Person
{
[HiddenInput(DisplayValue = false)]
public int PersonId { get; set; }
[Display(Name = "First")]
public string FirstName { get; set; }
[Display(Name = "Last")]
public string LastName { get; set; }
[Display(Name = "Birth Date")]
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
[Display(Name = "Approved")]
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
// ...other types omitted from Listing 20-for brevity...
}
Когда вспомогательный метод будет визуализировать элемент label
для свойства BirthDate
, он обнаружит атрибут Display
и будет использовать значение параметра Name
в качестве текста метки, например:
<label for="BirthDate">Birth Date</label>
Вспомогательные методы также распознают атрибут DisplayName
, который можно найти в пространстве имен System.ComponentModel
. Этот атрибут можно применять к классам, что позволяет нам использовать вспомогательный метод Html.LabelForModel
– в листинге показано, как мы применили этот атрибут к классу Person
. (Атрибут DisplayName
можно применить и к свойствам, но мы, как правило, используем его только для классов моделей – просто по привычке). Результат применения атрибутов Display
и DisplayName
показан на рисунке 20-8.
Рисунок 20-8: Используем атрибутыDisplay
иDisplayName
для создания меток
Используем метаданные для значений данных
С помощью метаданных можно предоставить инструкции относительно того, как должно отображаться свойство модели. Для нашего свойства BirthDate
отображается время, хотя мы хотели бы отображать для него только дату, и, чтобы решить эту проблему, мы будем использовать метаданные. Отображаемые значения данных контролируются атрибутом DataType
. В листинге 20-14 вы можете увидеть, как мы применили его к классу Person
.
Листинг 20-14: Применяем атрибутDataType
к классуPerson
[DisplayName("New Person")]
public class Person
{
[HiddenInput(DisplayValue = false)]
public int PersonId { get; set; }
[Display(Name = "First")]
public string FirstName { get; set; }
[Display(Name = "Last")]
public string LastName { get; set; }
[Display(Name = "Birth Date")]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
[Display(Name = "Approved")]
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
Атрибут DataType
принимает в качестве параметра значение из перечисления DataType
. В данном примере мы указали значение DataType.Date
, которое сообщает шаблонным вспомогательным методам, что необходимо визуализировать значение свойства BirthDate
в формате даты без времени, как показано на рисунке 20-9.
Подсказка
Изменения будут более заметны, если вы просмотрите приложение в браузере с лучшей поддержкой типов элементов
input
HTML5.
Рисунок 20-9: Изменяем способ отображения значенияDateTime
с помощью атрибутаDataType
В таблице 20-3 описаны наиболее полезные значения из перечисления DataType
.
Таблица 20-3: Значения из перечисления DataType
Значение | Описание |
DateTime |
Отображает дату и время (это поведение по умолчанию для значений System.DateTime ) |
Date |
Отображает дату из DateTime |
Time |
Отображает время из DateTime |
Text |
Отображает одну строку текста |
PhoneNumber |
Отображает номер телефона |
MultilineText |
Визуализирует значение в элементе textarea |
Password |
Отображает замаскированные символы |
Url |
Отображает данные в виде URL (используя элемент HTML a ) |
EmailAddress |
Отображает данные как адрес электронной почты (используя элемент a с mailto href ) |
Результат применения этих значений зависит от типа свойства, с которым они связаны, и вспомогательного метода, который мы используем. Например, значение MultilineText
сообщит вспомогательному методу, который создает элементы editor
для свойств, сгенерировать элемент HTML textarea
, но он будет проигнорирован вспомогательными методами для элементов display
. Все логично - элемент textarea
позволяет пользователю редактировать значение, и атрибут никак не повлияет на данные, которые мы отображаем в формате read-only. Равным образом, значение URL будет влиять только на вспомогательные методы для элементов display
, которые будут визуализировать элемент HTML a
для создания ссылки.
Используем метаданные для выбора шаблона отображения
Как следует из их названия, шаблонные вспомогательные методы используют шаблоны отображения (display templates) для создания HTML. Шаблон выбирается на основании типа обрабатываемого свойства и самого вспомогательного метода. С помощью атрибута UIHint
можно указать шаблон, который мы хотим использовать для работы с определенным свойством, как показано в листинге 20-15.
Листинг 20-15: Используем атрибут UIHint
[DisplayName("New Person")]
public class Person
{
[HiddenInput(DisplayValue = false)]
public int PersonId { get; set; }
[Display(Name = "First")]
[UIHint("MultilineText")]
public string FirstName { get; set; }
[Display(Name = "Last")]
public string LastName { get; set; }
[Display(Name = "Birth Date")]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
[Display(Name = "Approved")]
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
В листинге мы указали шаблон MultilineText
, который создаст элемент HTML textarea
для свойства FirstName
; он используется с вспомогательными методами для элементов editor
, такими как EditorFor
или EditorForModel
. В таблице 20-4 показан набор встроенных шаблонов MVC Framework.
Таблица 20-4: Встроенные шаблоны MVC Framework
Название | Эффект (Editor ) |
Эффект (Display ) |
Boolean |
Отображает чекбокс для свойств bool . Для свойств bool , поддерживающих значение null , создается элемент select с опциями True , False и Not Set . |
Как и для элементов editor , но добавляется атрибут disabled , который визуализирует элементы управления HTML в формате read-only.
|
Collection |
Отображает соответствующий шаблон для каждого элемента в последовательности IEnumerable . Элементы в последовательности не обязательно должны быть одного типа. |
Как и для элементов editor .
|
Decimal |
Отображает однострочное текстовое поле и приводит значения данных к формату двух десятичных разрядов. | Отображает значения данных в формате двух десятичных разрядов. |
DateTime |
Визуализирует элемент input , атрибут type которого содержит datetime (позволяет ввести дату и время). |
Визуализирует значение переменной DateTime .
|
Date |
Визуализирует элемент input , атрибут type которого содержит date (позволяет ввести дату, но не время). |
Визуализирует дату из переменной DateTime
|
EmailAddress |
Визуализирует значение в однострочном элементе ввода textbox . |
Отображает ссылку с помощью элемента HTML а с атрибутом href mailto .
|
HiddenInput |
Создает скрытый элемент ввода. | Отображает значение данных и создает скрытый элемент ввода. |
Html |
Отображает значение значение в однострочном элементе ввода textbox . |
Отображает ссылку с помощью элемента HTML а .
|
MultilineText |
Отображает элемент HTML textarea , который содержит значения данных. |
Отображает значения данных. |
Number |
Отображает элемент ввода, атрибут type которого содержит number . |
Отображает значения данных. |
Object |
Объяснение дано после этой таблицы. | Объяснение дано после этой таблицы. |
Password |
Отображает значение в элементе ввода textbox таким образом, что символы замаскированы, но могут быть отредактированы. |
Отображает значения данных, символы не замаскированы. |
String |
Отображает значение в однострочном элементе ввода textbox . |
Отображает значения данных. |
Text |
Идентичен шаблону String . |
Идентичен шаблону String . |
Tel |
Отображает элемент ввода, атрибут type которого содержит tel . |
Отображает значения данных. |
Time |
Отображает элемент ввода, атрибут type которого содержит time (позволяет установить время, но не дату). |
Отображает дату из переменной DateTime
|
Url |
Отображает значение в элементе ввода textbox . |
Отображает ссылку с помощью элемента HTML а . Для значений данных устанавливается атрибут href и внутренний HTML.
|
Внимание!
Используя атрибут
UIHint
, будьте внимательны. Если мы выберем для свойства шаблон, который не может работать с его типом, то получим исключение: например, применив шаблонBoolean
к свойствуstring
.
Шаблон Object
является особым случаем. Он используется шаблонными вспомогательными методами, чтобы создать HTML для объекта модели представления. Этот шаблон проверяет каждое свойство объекта и выбирает наиболее подходящий шаблон для типа каждого свойства. Шаблон Object
учитывает метаданные, такие как атрибуты UIHint
и DataType
.
Применяем метаданные в дополняющем классе (buddy class)
Не всегда можно применить метаданные к классу сущности модели. Так обычно происходит, когда классы моделей генерируются автоматически, например, с помощью инструментов ORM, таких как Entity Framework (хотя и не таким образом, как мы использовали Entity Framework в приложении SportsStore). Любые изменения, которые мы вносим в автоматически сгенерированные классы (такие как применение атрибутов), будут потеряны при следующем обновлении или регенерации классов.
Чтобы решить эту проблему, нужно определить класс модели как частичный и создать еще один частичный класс, который содержит метаданные. Многие инструменты для автоматической генерации классов создают частичные классы по умолчанию, в том числе и Entity Framework. В листинге 20-16 показан измененный класс Person
, который мог бы быть сгенерированным автоматически: в нем нет метаданных, и он определен как частичный.
Листинг 20-16: Частичный класс модели
using System;
using System.ComponentModel.DataAnnotations;
namespace HelperMethods.Models
{
[MetadataType(typeof (PersonMetaData))]
public partial class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
// ...other types omitted from listing for brevity...
}
Мы сообщаем MVC Framework о дополняющем классе с помощью атрибута MetadataType
, который принимает тип дополняющего класса в качестве аргумента. Дополняющий класс должен быть определен в том же пространстве имен и также должен быть частичным. Чтобы продемонстрировать, как это работает, мы добавили в проект новую папку под названием Models/Metadata
. В этой папке мы создали новый класс под названием PersonMetadata.cs
, содержание которого показано в листинге 20-17.
Листинг 20-17: Определяем дополняющий класс метаданных
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace HelperMethods.Models
{
[DisplayName("New Person")]
public partial class PersonMetaData
{
[HiddenInput(DisplayValue = false)]
public int PersonId { get; set; }
[Display(Name = "First")]
public string FirstName { get; set; }
[Display(Name = "Last")]
public string LastName { get; set; }
[Display(Name = "Birth Date")]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
[Display(Name = "Approved")]
public bool IsApproved { get; set; }
}
}
Дополняющий класс должен содержать только те свойства, к которым мы хотим применить метаданные – нет необходимости воспроизводить все свойства класса Person
, например.
Подсказка
Обязательно измените пространство имен, в которое Visual Studio добавляет новый класс - дополняющий класс должен быть в том же пространстве имен, что и класс модели (в данном примере -
HelperMethods.Models
).
Работаем со сложными типами свойств
Процесс формирования шаблонов полагается на шаблон Object
, который мы описали в предыдущем разделе. Каждое свойство проверяется, и для него выбирается шаблон для создания кода HTML, который будет визуализировать это свойство и его значения данных.
Возможно, вы заметили, что свойство HomeAddress
не отображалось как часть класса Person
, когда мы использовали EditorForModel
. Так происходит потому, что шаблон Object
работает только с простыми типами, то есть типами, которые могут быть получены из значения string
с помощью метода GetConverter
класса System.ComponentModel.TypeDescriptor
. Поддерживаемые типы включают внутренние типы C#, такие как int
, bool
и double
, а также многие общие типы MVC Framework, такие как Guid
и DateTime
.
Из этого следует, что шаблонные вспомогательные методы не являются рекурсивными. Получив объект для обработки, шаблонный вспомогательный метод сгенерирует HTML только для простых типов свойств и проигнорирует все свойства, которые представляют собой сложные объекты.
Возможно, это и неудобно, но это разумная политика; MVC Framework не знает, как были созданы наши объекты моделей, и если бы шаблон Object
был рекурсивным, то он бы задействовал медленно загружающиеся функции ORM, а затем читал и отображал бы каждый объект в нижележащей базе данных. Если мы хотим создать HTML для сложного свойства, то это нужно сделать явно, используя отдельный вызов к шаблонному вспомогательному методу. Чтобы показать, как это делается, мы внесли изменения в представление CreatePerson.cshtml
, которые показаны в листинге 20-18.
Листинг 20-18: Работаем со сложным типом
@model HelperMethods.Models.Person
@{
ViewBag.Title = "CreatePerson";
Html.EnableClientValidation(false);
}
<h2>CreatePerson: @Html.LabelForModel()</h2>
@using (Html.BeginRouteForm("FormRoute", new {}, FormMethod.Post,
new {@class = "personClass", data_formType = "person"}))
{
<div class="column">
@Html.EditorForModel()
</div>
<div class="column">
@Html.EditorFor(m => m.HomeAddress)
</div>
<input type="submit" value="Submit" />
}
Чтобы отобразить свойство HomeAddress
, мы добавили вызов к строго типизированному вспомогательному методу EditorFor
.(Мы также добавили некоторые элементы div
, чтобы к генерируемому HTML применялись стили CSS, которые мы определили для класса column
в листинге 20-10). Результат показан на рисунке 20-10.
Рисунок 20-10: Отображаем сложное свойство
Подсказка
Свойство
HomeAddress
возвращает объектAddress
, и мы можем применить к классуAddress
те же метаданные, которые применили к классуPerson
. Когда мы используем вспомогательный методEditorFor
в свойствеHomeAddress
, шаблонObject
вызывается явно, так что все соглашения, относящиеся к метаданным, соблюдены.