Продолжим рассматривать возможности ядра ASP.NET MVC 3 для работы с Моделью. В этот раз давайте разберемся, что содержится в её метаданных и откуда берётся эта информация.
Зачем нужны метаданные в веб-приложениях
Для начала вспомним что такое метаданные. Как уже упоминалось раньше, они позволяют указывать дополнительную информацию об объекте. При этом они отделены от него и не изменяют представляющий его класс. Поэтому их добавление и удаление прозрачно для кода, который не использует их.
При разработке необходимо четко разделять данные и метаданные, исходя из абстракции Модели и решаемых задач бизнес-логики. Например, адрес электронной почты пользователя, как правило, относится к данным. А вот указание в каком виде выводить значение свойства – уже к метаданным. Но при этом нет строгого разделения – то, что в одной абстракции является данными, в другой может стать метаданными.
Наверное, наиболее широко в ASP.NET MVC метаданные применяются для передачи уточнений в Представления: указание типа, отображаемого имени, признака обязательного наличия значения и т.д. Кроме того, разработчик веб-приложения может столкнуться с задачей, когда ему необходимо пересылать дополнительные метаданные вместе с Моделью. В частности, это может потребоваться при использовании своих или сторонних компонентов в пользовательском интерфейсе веб-приложения.
Метаданные Модели
Необходимо отметить, что в .NET любой объект обязательно сопровождается метаданными. Это и данные для самой .NET и различная информация, добавленная разработчиками сборок и классов.
Но дальше речь пойдет только о той их части, которая описывает саму Модель. Можно сказать, что она стандартизирована в ASP.NET MVC. Чтобы определить что в неё входит, существует специальный класс ModelMetadata. Он описывает информацию, которую и будем в дальнейшем называть метаданными Модели.
Его экземпляры уже встречались в предыдущих частях. В частности, при создании атрибутов проверки данных они использовались для добавления отображаемого имени свойства в сообщения об ошибках.
Давайте разделим содержащуюся в классе ModelMetadata информацию на группы и перечислим её:
1. Описание объекта (Модели):
- PropertyName – имя свойства, в котором расположен объект.
- ContainerType – тип родительского объекта, который содержит данный объект.
- ModelType – имя свойства и тип объекта, к которому относятся метаданные.
- Model – значение объекта.
- DataTypeName – текстовое уточненное имя типа (например "EmailAddress", "Password", "Url" и т.д.).
2. Информация о связанных метаданных:
- Properties – коллекция метаданных, принадлежащих свойствам объекта (если таковые у него есть).
- Provider – указывает на провайдер метаданных, используемый для данного объекта.
3. Признаки значения: (в скобках указаны значения по умолчанию)
- IsComplex – флаг комплексного значения (только для чтения). Т.е. такого значения, которое не может быть представлено в виде строки или получено из неё.
- IsNullableValueType – показывает может ли тип объекта иметь значение null (только для чтения).
- IsReadOnly (false) – признак того, что значение объекта предназначено только для чтения.
- IsRequired – признак того, что данный объект обязательно должен содержать значение. Для типов которые не могут быть равны null, по умолчанию равно true, для остальных false.
- ConvertEmptyStringToNull (true) – признак того, что полученная от формы ввода данных пустая строка должна быть представлена в виде значения null.
- RequestValidationEnabled (true) – при установки в false, исключает значение из проверки запроса.
- ShowForDisplay (true) – указывает, на необходимость вывода объекта в нередактируемых Представлениях, таких как таблицы или списки.
- ShowForEdit (true) – указывает, что данный объект выводится в формах ввода данных.
4. Параметры отображения значения:
- DisplayName – отображаемое имя.
- ShortDisplayName – краткое имя.
- Description – текст с подробным описанием объекта.
- NullDisplayText – текст для отображения в случае, если значение объекта равно null.
- SimpleDisplayText – текст, который выводится вместо комплексного значения объекта. По умолчанию используется одно из следующих значений (в указанном порядке):
- если объект равен null, то возвращается NullDisplayText;
- если объект переопределяет метод ToString(), то выводится его результат;
- если у объекта нет свойств, то выводится String.Empty;
- если первое свойство объекта равно null, то выводится NullDisplayText.
- либо выводится результат вызова метода ToString() у первого свойства объекта.
- DisplayFormatString – задает формат вывода значения объекта.
- EditFormatString – аналогично предыдущему, но для редактирования значения.
- HideSurroundingHtml – указывает на необходимость вывода значения без дополнительных HTML тегов (например, label) и часто используется для скрытых (hidden) полей.
- Order – "вес" объекта при сортировке. Может использоваться, например, при выводе значений в виде таблицы, указывая номер колонки для данного объекта.
- TemplateHint – определяет шаблон для вывода значения.
- Watermark – текст "водяного знака" (например, изначальная подсказка в поле ввода).
4. Пользовательские метаданные
- AdditionalValues – коллекция типа Dictionary<string, Object>, позволяющая разработчикам указывать свои значения в метаданных Модели.
Обратите внимание, что сама по себе установка метаданных ничего не гарантирует. Например, чтобы был отображен текст ShortDisplayName, код, используемый в Представлении, должен уметь с ним работать.
Кроме перечисленных свойств, интерес также могут представить два вспомогательных метода:
- GetDisplayName() – возвращает имя объекта. Выбирает не пустое значение из следующих, указанных в порядке очередности перебора: DisplayName, PropertyName и ModelType.Name.
- GetValidators() – возвращает коллекцию экземпляров класса ModelValidator, которые описывают параметры проверки значения для данного объекта.
Теперь посмотрим, какие возможности для работы с этими метаданными предоставляет ASP.NET MVC 3.
Атрибут [AdditionalMetadata]
В ASP.NET MVC 3 появился достаточно легкий способ внести значение в коллекцию AdditionalValues. Это атрибут [AdditionalMetadata], конструктор которого принимает имя параметра и само значение.
Например, необходимо указать, что поле Homepage при отображении должно иметь иконку, обозначающую переход по ссылке. Это можно сделать с помощью рассматриваемого атрибута так:
[AdditionalMetadata("ShowUrlIcon", true)]
public string Homepage { get; set; }
В дальнейшем, код в Представлении должен будет проверить наличие этого указания в метаданных Модели и, при обнаружении, вывести соответствующую иконку.
Модификация метаданных Модели при помощи атрибута
Рассмотрим следующую ситуацию: создаваемый атрибут должен изменять метаданные Модели. Для этого ему необходимо добавить реализацию интерфейса IMetadataAware:
namespace System.Web.Mvc
{
public interface IMetadataAware
{
void OnMetadataCreated(ModelMetadata metadata);
}
}
Метод OnMetadataCreated() вызывается при создании метаданных и получает их экземпляр в качестве параметра. Это позволяет реализовывать сценарии, использующие динамические значения (например, текущее время). Это отличает данный вариант от использования [AdditionalMetadata], поскольку в качестве параметров атрибута можно передавать только константы.
В результате, IMetadataAware является удобным и простым в разработке вариантом установки значений метаданных Модели. Для примера, посмотрим как может быть реализован атрибут [AllowHtml]:
public class AllowHtmlAttribute : Attribute, IMetadataAware
{
#region IMetadataAware Members
public void OnMetadataCreated(ModelMetadata metadata)
{
if (metadata == null) {
throw new ArgumentNullException();
}
metadata.RequestValidationEnabled = false;
}
#endregion
}
Кстати, именно такое решение используется для данного атрибута в ASP.NET MVC 3. Убедиться в этом можно, если заглянуть в его исходный код, например с помощью декомпилятора.
Провайдеры метаданных модели
Самое время рассмотреть, кто заполняет метаданные Модели при их создании и откуда там оказываются значения из параметров атрибутов?
Для этого ASP.NET MVC использует специальные классы, называемые провайдерами метаданных. Они отвечают за создание экземпляров ModelMetadata для Модели. Данные для этого могут быть получены как из атрибутов, так и из любых других источников.
Разработка собственного провайдера позволяет осуществить полный контроль над созданием метаданных Модели. Давайте рассмотрим существующую в ASP.NET MVC реализацию по умолчанию и начнем с её базовых классов.
Абстрактная база для провайдеров – класс ModelMetadataProvider
В качестве базового класса для всех провайдеров метаданных выступает ModelMetadataProvider. Посмотрим на его определение:
namespace System.Web.Mvc
{
public abstract class ModelMetadataProvider
{
protected ModelMetadataProvider();
public abstract IEnumerable<ModelMetadata> GetMetadataForProperties(
object container, Type containerType);
public abstract ModelMetadata GetMetadataForProperty(
Func<object> modelAccessor, Type containerType, string propertyName);
public abstract ModelMetadata GetMetadataForType(
Func<object> modelAccessor, Type modelType);
}
}
Для разработки своей реализации на его основе потребуется создать три метода:
- GetMetadataForProperties() – возвращает коллекцию метаданных для всех свойств Модели.
- GetMetadataForProperty() – возвращает метаданные для указанного свойства.
- GetMetadataForType() - возвращает метаданные для типа самой Модели.
Обратите внимание, что провайдер не связан с конкретным типом Модели. Он получает его в качестве параметра и должен самостоятельно определить для каких свойств какие метаданные нужно назначить.
Упрощаем задачу создания провайдера – класс AssociatedMetadataProvider
Реализация указанных выше методов достаточно не простая задача. Кроме того, во многих случаях эту задачу можно свести к общему методу получения метаданных Модели для заданного свойства. Возможно разработчики ASP.NET MVC рассуждали также, поэтому предоставили класс AssociatedMetadataProvider, являющийся наследником ModelMetadataProvider. Он реализует все указанные выше абстрактные методы, требуя взамен создать только один:
protected abstract ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName);
Обратите внимание, что в CreateMetadata() уже передается список атрибутов свойства, что несколько упрощает задачу разработки.
Кроме того, именно данный класс использует IMetadataAware для получения метаданных из атрибутов. Поэтому указанный интерфейс будет задействован только если в качестве провайдера используется наследник AssociatedMetadataProvider или самостоятельно создана поддержка работы с ним.
Провайдер по умолчанию – класс DataAnnotationsModelMetadataProvider
Наконец перейдем к провайдеру по умолчанию. Это класс DataAnnotationsModelMetadataProvider, наследник упомянутого выше AssociatedMetadataProvider.
Вспомним, что начиная с 3 версии ASP.NET MVC получила поддержку атрибутов, расположенных в пространстве имен System.ComponentModel.DataAnnotations. Думаю многие уже догадались, что провайдер по умолчанию заполняет метаданные Модели их значениями. Этот факт необходимо учесть при разработке собственных провайдеров, т.к. при этом легко незаметно потерять поддержку атрибутов DataAnnotations.
Одним из вариантов реализации может стать использование DataAnnotationsModelMetadataProvider в качестве базового класса. В этом случае достаточно переопределить CreateMetadata() и вызвать в его начале базовую реализацию, после которой уже добавить нужную логику работы c метаданными:
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(
System.Collections.Generic.IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName)
{
var metaData = base.CreateMetadata(
attributes, containerType, modelAccessor, modelType, propertyName);
// TODO: Add the logic here ...
return metaData;
}
}
Кроме того, существует еще один провайдер – EmptyModelMetadataProvider. Он используется для Моделей, которым не нужные метаданные.
Смена провайдера метаданных Модели
Для установки используемого в данный момент провайдера метаданных отвечает класс:
public class ModelMetadataProviders
{
public static ModelMetadataProvider Current { get; set; }
}
Его единственное статическое свойство указывает на используемый экземпляр. Причем, установка его в значение null эквивалентно использованию EmptyModelMetadataProvider.
В качестве эксперимента, можно добавить следующий код в Application_Start():
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
ModelMetadataProviders.Current = null;
}
В результате, при запуске веб-приложения можно убедиться, что вместо отображаемых имен выводятся имена свойств, а значения паролей теперь не маскируются специальными символами.
Исключение составят только данные атрибутов проверки данных. Вводимые значения по прежнему контролируются и выводятся сообщения об ошибках. Это явно показывает, что их информация храниться отдельно и не получается от провайдера метаданных. Ведь действительно, в классе ModelMetadata нет полей для параметров проверки значений.
В следующей части рассмотрим несколько сценариев использования своих провайдеров метаданных модели и примеры их разработки. После чего разберемся, где размещаются параметры проверки значений.