Метаданные модели

110

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

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

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

Использование метаданных для управления редактированием и видимостью

Свойство UserId в классе User отображать пользователю нежелательно и тем более не следует предоставлять возможность его редактирования. Большинство классов моделей имеют, по крайней мере, одно такое свойство, часто относящееся к лежащему в основе механизму хранения; например, это может быть первичный ключ, управляемый реляционной базой данных, как демонстрировалось при создании приложения GameStore. С помощью атрибута HiddenInput можно заставить вспомогательный метод генерировать скрытое поле ввода.

В примере ниже показано, как применить атрибут HiddenInputAttribute к классу User:

using System;
using System.Web.Mvc;

namespace HelperMethods.Models
{
    public class User
    {
        [HiddenInput]
        public int UserId { 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.EditorFor() и Html.EditorForModel() будут визуализировать представление декорированного свойства (так называется свойство, к которому был применен атрибут), предназначенное только для чтения. Это можно видеть на рисунке ниже, где показан результат запуска приложения и перехода на URL вида /Home/CreateUser:

Представление со свойством только для чтения

Значение свойства UserId отображается, но редактировать его пользователь не может. Сгенерированная для свойства HTML-разметка выглядит следующим образом:

...
<div class="editor-field">
    0
    <input id="UserId" name="UserId" type="hidden" value="0" />
</div>
...

Значение этого свойства (0 в данном случае) визуализируется буквально, но вспомогательный метод также включает скрытый элемент <input> для свойства, что удобно в случае HTML-форм, поскольку обеспечивает отправку значения для свойства вместе с остальными составляющими формы. Мы вернемся к этому вопросу во время рассмотрения привязки моделей и проверки достоверности моделей позже. Чтобы полностью скрыть свойство, необходимо присвоить свойству DisplayValue атрибута HiddenInput значение false, как показано в примере ниже:

public class User
{
    [HiddenInput(DisplayValue=false)]
    public int UserId { get; set; }
    // ...
}

При использовании вспомогательного метода Html.EditorForModel() на объекте User будет создан скрытый элемент <input>, так что значение для свойства UserId будет включено во все отправки формы, но метка и литеральное значение будут опущены. Это обеспечивает эффект сокрытия свойства UserId от пользователя:

Сокрытие свойств объекта модели от пользователя

Если выбрана визуализация HTML-разметки для отдельных свойств, можно по-прежнему создать скрытый элемент <input> для свойства UserId, воспользовавшись вспомогательным методом Html.EditorFor():

...
@Html.EditorFor(u => u.UserId)
...

Свойство HiddenInput обнаруживается, и если DisplayValue равно true, генерируется следующая HTML-разметка:

...
<input id="UserId" name="UserId" type="hidden" value="0" />
...

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

public class User
{
    [ScaffoldColumn(false)]
    public int UserId { get; set; }
    // ...
}

Когда вспомогательные методы для формирования шаблонов встречают атрибут ScaffoldColumn, примененный подобным образом, они полностью пропускают помеченное им свойство; никаких скрытых элементов <input> не создается, и никакие детали, связанные с этим свойством, в генерируемую HTML-разметку не включаются. Внешний вид сгенерированной HTML-разметки при отображении будет таким же, как и в случае использования атрибута HiddenInput, но при отправке формы для этого свойства значение возвращаться не будет.

Это оказывает влияние на привязку моделей. Атрибут ScaffoldColumn не влияет на вспомогательные методы, работающие с индивидуальными свойствами, такие как EditorFor(). Если вызвать @Html.EditorFor(m => m. UserId) в представлении, то редактор для свойства UserId сгенерируется, невзирая на присутствие атрибута ScaffoldColumn.

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

По умолчанию вспомогательные методы Label(), LabelFor(), LabelForModel() и EditorForModel() используют в качестве содержимого для генерируемых меток (элементов label) имена свойств. Например, следующий вызов вспомогательного метода:

@Html.Label(u => u.BirthDate)

сгенерирует такой HTML-элемент:

<label for="">BirthDate</label>

Естественно, пользователю не всегда желательно отображать сами имена свойств (тем более на английском языке). Для этого можно применить атрибут DisplayName из пространства имен System.ComponentModel, передав ему в качестве параметра желаемое значение для свойства Name. В примере ниже демонстрируется применение этого атрибута к классу User:

using System;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;

namespace HelperMethods.Models
{
    [DisplayName("новый юзер")]
    public class User
    {
        [HiddenInput(DisplayValue=false)]
        public int UserId { get; set; }

        [DisplayName("Имя")]
        public string FirstName { get; set; }

        [DisplayName("Фамилия")]
        public string LastName { get; set; }

        [DisplayName("Дата рождения")]
        public DateTime BirthDate { get; set; }

        [DisplayName("Адрес")]
        public Address HomeAddress { get; set; }

        [DisplayName("Подтвердил регистрацию?")]
        public bool IsApproved { get; set; }

        [DisplayName("Роль")]
        public Role Role { get; set; }
    }

    // ...
}

Когда вспомогательные методы для меток визуализируют элемент label для свойства BirthDate, они обнаруживают атрибут Display и используют значение его параметра Name для внутреннего текста, примерно так:

<label for="">Дата рождения</label>

Вспомогательные методы также распознают атрибут Display, который находится в пространстве имен System.ComponentModel.DataAnnotations. Преимущество такого атрибута в том, что он может быть применен к классам, а это позволяет использовать вспомогательный метод Html.LabelForModel() - в примере ниже атрибут DisplayName применялся к классу User. (Атрибут DisplayName можно применять также и к свойствам, но по привычке он используется только для классов моделей.) Результат применения атрибутов Display и DisplayName показан на рисунке ниже:

Применение атрибутов Display и DisplayName для управления метками

Использование метаданных для значений данных

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

[DisplayName("новый юзер")]
public class User
{
    // ...

    [DisplayName("Дата рождения")]
    [DataType(DataType.Date)]
    public DateTime BirthDate { get; set; }

    // ...
}

Атрибут DataType принимает в качестве параметра значение перечисления DataType. В этом примере было указано значение DataType.Date, которое заставляет шаблонизированные вспомогательные методы визуализировать значение свойства BirthDate в виде даты без компонента времени, как показано на рисунке ниже:

Использование атрибута DataType для управления отображением значения типа DateTime

Изменение будет более выразительным в случае просмотра приложения в браузере, который поддерживает типы элементов <input>, описанные в спецификации HTML5. Наиболее часто используемые значения перечисления DataType описаны в таблице ниже:

Значения перечисления DataType
Значение Описание
DateTime

Отображает дату и время (это стандартное поведение для значений System.DateTime)

Date

Отображает часть DateTime, касающуюся даты

Time

Отображает часть DateTime, касающуюся времени

Text

Отображает одиночную строку текста

PhoneNumber

Отображает телефонный номер

MultilineText

Визуализирует значение в элементе <textarea>

Password

Отображает данные с маскированием отдельных символов

Url

Отображает данные в виде URL (используя HTML-элемент <a>)

EmailAddress

Отображает данные в виде адреса электронной почты (используя HTML-элемент а с атрибутом href, установленным в mailto)

Результат использования этих значений зависит от типа свойства, с которым они ассоциированы, и применяемого вспомогательного метода. Например, значение MultilineText приведет к тому, что вспомогательные методы, которые создают редакторы для свойств, будут создавать HTML-элемент <textarea>, но вспомогательные методы для отображения это значение проигнорируют. Такое поведение имеет смысл - элемент <textarea> позволяет пользователю редактировать значение, что неприемлемо в ситуации с отображением данных в форме, предназначенной только для чтения. Точно так же значение Url оказывает влияние только на вспомогательные методы для отображения, которые генерируют HTML-элемент <a> с целью создания ссылки.

Использование метаданных для выбора шаблона отображения

Как должно быть понятно по названию, шаблонизированные вспомогательные методы используют шаблоны отображения для генерации HTML-разметки. Применяемый шаблон основан на типе обрабатываемого свойства и разновидности используемого вспомогательного метода. Для указания желаемого шаблона при генерации HTML-разметки предназначен атрибут UIHint, как показано в примере ниже:

[DisplayName("новый юзер")]
public class User
{
    // ...

    [DisplayName("Имя")]
    [UIHint("MultilineText")]
    public string FirstName { get; set; }
        
    // ...
}

В этом примере указан шаблон MultilineText, который визуализирует HTML-элемент <textarea> для свойства FirstName при использовании одного из вспомогательных методов для редакторов, таких как EditorFor() или EditorForModel(). В таблице ниже описан набор шаблонов встроенных в ASP.NET MVC Framework:

Встроенные шаблоны представлений ASP.NET MVC Framework
Шаблон Эффект (в редакторе) Эффект (при отображении)
Boolean

Визуализирует флажок для значений типа bool. Для допускающих null значений bool? создается элемент <select> с опциями True, False и Not Set

Как для редактора, но с добавлением атрибута disabled, который визуализирует элементы управления HTML, предназначенные только для чтения

Collection

Визуализирует подходящий шаблон для каждого элемента в последовательности IEnumerable. Элементы в последовательности не обязательно должны быть одного и того же типа

Как для редактора

Decimal

Визуализирует элемент <input> для однострочного текстового поля и форматирует значение данных для отображения двух десятичных позиций

Визуализирует значение данных, сформатированное с двумя десятичными позициями

DateTime

Визуализирует элемент <input>, который имеет атрибут type, установленный в datetime, и содержит полную дату и время

Визуализирует полное значение переменной DateTime

Date

Визуализирует элемент <input>, который имеет атрибут type, установленный в date, и содержит компонент даты (но не времени)

Визуализирует компонент даты для переменной DateTime

EmailAddress

Визуализирует значение в элементе <input> для однострочного текстового поля

Визуализирует ссылку с использованием HTML-элемента <a>, в атрибуте href которого указано mailto и URL

HiddenInput

Создает скрытый элемент <input>

Визуализирует значение данных и создает скрытый элемент <input>

Html

Визуализирует значение в элементе <input> для однострочного текстового поля

Визуализирует ссылку с использованием HTML-элемента <a>

MultilineText

Визуализирует HTML-элемент <textarea>, содержащий значение данных

Визуализирует значение данных

Number

Визуализирует элемент <input>, атрибут type которого установлен в number

Визуализирует значение данных

Object

Смотрите объяснение, приведенное после таблицы

Смотрите объяснение, приведенное после таблицы

Password

Визуализирует значение в элементе <input> для однострочного текстового поля так, что символы не отображаются, но могут редактироваться

Визуализирует значение данных - символы не маскируются

String

Визуализирует значение в элементе <input> для однострочного текстового поля

Визуализирует значение данных

Text

Идентичен шаблону String

Идентичен шаблону String

Tel

Визуализирует элемент <input>, атрибут type которого установлен в tel

Визуализирует значение данных

Time

Визуализирует элемент <input>, который имеет атрибут type, установленный в time, и содержит компонент времени (но не даты)

Визуализирует компонент времени для переменной DateTime

Url

Визуализирует значение в элементе <input> для однострочного текстового поля

Визуализирует ссылку с использованием HTML-элемента <a>. Внутренний текст и атрибут href устанавливаются в значение данных

Использовать атрибут UIHint следует осторожно. В случае выбора шаблона, который не может оперировать на конкретном типе свойства (например, при применении шаблона Boolean к свойству типа string), возникнет исключение.

Шаблон Object является специальным случаем. Этот шаблон используется вспомогательными методами для формирования шаблонов при генерации HTML-разметки для всего объекта модели представления. Данный шаблон просматривает каждое свойство объекта модели и выбирает шаблон, наиболее подходящий для типа свойства. Шаблон Object учитывает метаданные, такие как атрибуты UIHint и DataType.

Применение метаданных к родственным классам

Применять метаданные к классу сущностной модели возможно не всегда. Обычно подобная ситуация возникает, когда классы моделей генерируются автоматически, например, с помощью инструментов ORM, подобных Entity Framework (хотя и не так, как инфраструктура Entity Framework использовалась в приложении GameStore). Любые изменения, применяемые к автоматически сгенерированным классам, вроде добавления атрибутов, будут утеряны при следующем обновлении или повторной генерации классов.

Решение этой проблемы предусматривает определение класса модели как partial и создание второго класса partial, который содержит метаданные. Многие инструменты для генерации классов создают классы автоматически как partial, и Entity Framework входит в их число. В примере ниже показан класс User, модифицированный так, чтобы он мог генерироваться автоматически. Он не содержит метаданные и определен как partial:

using System;

namespace HelperMethods.Models
{
    [MetadataType(typeof(UserMetaData))]
    public partial class User
    {
        public int UserId { 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; }
    }

    // ...
}

Инфраструктуре ASP.NET MVC Framework сообщается о родственном классе с помощью атрибута MetadataType, который принимает в качестве аргумента тип родственного класса. Родственные классы должны определяться в том же самом пространстве имен и также быть классами partial. Чтобы продемонстрировать, каким образом это работает, в пример проекта добавлена папка по имени Models/Metadata. В этой папке создан файл класса по имени UserMetadata.cs, содержимое которого показано в примере ниже:

using System;
using System.Web.Mvc;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace HelperMethods.Models
{
    [DisplayName("новый юзер")]
    public partial class UserMetaData
    {
        [HiddenInput(DisplayValue = false)]
        public int UserId { get; set; }

        [DisplayName("Имя")]
        public string FirstName { get; set; }

        [DisplayName("Фамилия")]
        public string LastName { get; set; }

        [DisplayName("Дата рождения")]
        [DataType(DataType.Date)]
        public DateTime BirthDate { get; set; }

        [DisplayName("Адрес")]
        public Address HomeAddress { get; set; }

        [DisplayName("Подтвердил регистрацию?")]
        public bool IsApproved { get; set; }

        [DisplayName("Роль")]
        public Role Role { get; set; }
    }
}

Родственный класс должен содержать только свойства, к которым необходимо применять метаданные - нет нужды в дублировании всех свойств класса User, к примеру. Уделите особое внимание изменению пространства имен, которое Visual Studio добавляет в новый файл класса. Родственный класс должен находиться в том же самом пространстве имен, что и класс модели, т.е. HelperMethods.Models в рассматриваемом примере проекта.

Работа со свойствами сложных типов

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

Вы могли заметить, что при использовании вспомогательного метода EditorForModel() свойство HomeAddress не было визуализировано как часть класса User. Это происходит потому, что шаблон Object оперирует только на простых типах - тех, которые могут быть получены из значения string с применением метода GetConverter() класса System.ComponentModel.TypeDescriptor. Поддерживаемые типы включают встроенные типы C#, такие как int, bool и double, а также множество общих типов платформы, в том числе Guid и DateTime.

В результате вспомогательные методы для формирования шаблонов не являются рекурсивными. При заданном объекте для обработки шаблонизированный вспомогательный метод для формирования шаблонов будет генерировать HTML-разметку только для свойств простых типов, игнорируя любые свойства, которые являются сложными объектами.

Несмотря на неудобства, эта политика вполне разумна. Инфраструктуре ASP.NET MVC Framework не известно, как созданы объекты модели, и если бы шаблон Object был рекурсивным, то просто бы включилось средство ленивой загрузки ORM, которое привело бы к чтению и визуализации каждого объекта в применяемой базе данных. Визуализация HTML-разметки для сложного свойства должна производиться явно, путем отдельного вызова шаблонизированного вспомогательного метода. В примере ниже показано, как это делается, для чего в файл CreateUser.cshtml были внесены изменения:

@model HelperMethods.Models.User

@{
    ViewBag.Title = "CreateUser";
    Html.EnableClientValidation(false);
}

<h2>Создать пользователя: @Html.LabelForModel()</h2>
@using (Html.BeginRouteForm("FormRoute", null, FormMethod.Post,
    new { @class = "userCssClass", data_formType="user" }))
{ 
    <div class="column">
        @Html.EditorForModel();
    </div>
    <div class="column">
        @Html.EditorFor(user => user.HomeAddress)
    </div>
    <input type="submit" value="Отправить" />
}

Чтобы отобразить свойство HomeAddress, добавлен вызов строго типизированного вспомогательного метода EditorFor(). (Также было добавлено несколько элементов <div> для поддержки структуры генерируемой HTML-разметки, полагаясь на CSS-стиль, который был определен для класса column ранее.) Результат можно видеть на рисунке ниже:

Отображение сложного свойства

Свойство HomeAddress типизировано для возврата объекта Address, и к классу Address можно применить все те же самые метаданные, которые применялись к классу User. Шаблон Object вызывается явно, когда используется вспомогательный метод EditorFor() для свойства HomeAddress, поэтому все соглашения о метаданных соблюдены. Мы применили следующие настройки метаданных, чтобы отобразить англоязычные свойства на русский язык:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;

namespace HelperMethods.Models
{
    [MetadataType(typeof(UserMetaData))]
    public partial class User
    {
        // ...
    }

    public class Address
    {
        [DisplayName("Адрес 1")]
        public string Line1 { get; set; }

        [DisplayName("Адрес 2")]
        public string Line2 { get; set; }

        [DisplayName("Город")]
        public string City { get; set; }

        [DisplayName("Почтовый индекс")]
        public string PostalCode { get; set; }

        [DisplayName("Страна")]
        public string Country { get; set; }
    }

    // ...
}
Пройди тесты
Лучший чат для C# программистов