Главная страница   /   20.3. Использование метаданных модели (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

20.3. Использование метаданных модели

Как вы уже убедились, шаблонные вспомогательные методы ничего не знают о нашем приложении и его типах моделей, и поэтому мы в конечном итоге получаем не тот 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 вызывается явно, так что все соглашения, относящиеся к метаданным, соблюдены.