Pull to refresh

Шаблоны отображения и редактирования данных в ASP.NET MVC 2

Reading time 26 min
Views 31K
Original author: Brad Wilson

Перевод серии статей посвящённых шаблонам отображения и редактирования.



  1. Введение в шаблоны. (Оригинал)
  2. Метаданные в шаблонах. (Оригинал)
  3. Встроенные шаблоны. (Оригинал)
  4. Создание собственных шаблонов. (Оригинал)
  5. Мастер-шаблоны. (Оригинал)



Часть первая. Введение.


Введение в шаблоны


Одно из главных новых нововведений в ASP.NET MVC — это шаблоны.


Шаблоны подобны динамическим данным (Dynamic Data) в классическом ASP.NET. По существующему объекту данного типа, система автоматически генерирует разметку для его отображения или редактирования, вне зависимости от того представляет ли собой объект простые данные (integer, decimal, string и т.п.), или же является представителем класса.


Html.Display


Для того чтобы отобразить элемент, используется три метода отображения (каждый из которых имеет по несколько перегрузок):


  • String based: <%= Html.Display(«PropertyName») %>
  • Expression based: <%= Html.DisplayFor(model => model.PropertyName) %>
  • Model: <%= Html.DisplayForModel() %>

1-ый метод, может использоваться для показа данных как из ViewData, так и из модели, тип которой вы можете и не знать.


Следующий метод используется в основном для передачи данных из модели. Кроме того, этот метод можно использовать, например, полностью игнорируя значения исходных данных (model => someOtherValue).


И последний метод является хелпером, для текущей модели. Метод DisplayForModel эквивалентен записи DisplayFor(model => model).


Начнём примеры с модели:


public class Contact
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public int Age { get; set; }
}

… затем action:


pubilc ViewResult Details([DefaultValue(0)] int id)
{
	return View(contact[id]);
}

… и наконец View:


<%@ Page Language="C#"
	MasterPageFile="~/Views/Shared/Site.master"
	Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ContentPlaceHolderID="MainContent" runat="server">
	<%= Html.DisplayForModel() %>
</asp:Content>

В результате получим такую страничку:


Пример шаблона отображения

Html.Editor


Подобно Html.Display, также три метода, которые используются для генерации html для редактирования объекта:


  • String based: <%= Html.Editor(«PropertyName») %>
  • Expression based: <%= Html.EditorFor(model => model.PropertyName) %>
  • Model: <%= Html.EditorForModel() %>

Если я поменяю в своём View метод DisplayForModel на EditorForModel, то страница будет выглядеть так:


Пример шаблона редактирования

Как видите, система сгенерировала текстбоксы для строк и для целых значений.


Что происходит на самом деле?


Система шаблонов в MVC 2 содержит несколько встроенных шаблонов.


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


Если вы не будете использовать шаблоны, то разметка может выглядеть примерно так:


<% foreach (var prop in ViewData.ModelMetadata.Properties) { %>
	<div class="display-label"><%= prop.GetDisplayName() %></div>
	<div class="display-field"><%= Html.Display(prop.PropertyName) %></div>
<% } %>

Стоит иметь в виду, что сейчас это ещё не законченная реализация. Я расскажу чего здесь не хватает позже.


Это основа шаблона для отображения сложного объекта: пройтись по всем свойствам текущей модели, для каждой из них показать лэйбл и вызвать метод Html.Display() для отображения значения свойства.


Что происходит когда мы хотим показать строку? А происходит приблизительно следующее:


<%= Html.Encode(Model) %>

И снова, это не реальный код, То что происходит на самом деле, обсуждается позже.


Подмена шаблонов


Существует множество достоинств встроенных шаблонов, но пожалуй самое важное, это возможность подменить их в любом месте рендеринга.


Давайте рассмотрим пример подмены шаблона для строк. Для этого создадим partial view, который назовём String и расположим в папке ~/Views/ControllerName/DisplayTemplates:


Способ подмены шаблона строки

В файле напишем:


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%= Html.Encode(Model) %> <em>Привет всем!</em>

Обновив страницу мы увидим:


Пример подменённай строки

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


Если нам понадобится написать шаблон для редактирования, то его необходимо разместить в папке EditorTemplates.


Часть вторая. ModelMetadata


Осмысление модели


Один из новых классов, появившийся вместе с ASP.NET MVC 2 — это Model Metadata. Этот класс разработан для того, чтобы сообщить вам необходимую информацию об объекте для его отображения. И хотя в основном это используется для написания шаблонов, метаданные доступны всегда, не только в шаблонах.


Что такое Model?


В контексте использования ModelMetadata, определение понятия «Model» немного расплывается.


Рассмотрим опять же модель из предыдущей части:


public class Contact
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public int Age { get; set; }
}

Мы создали строго-типизированное представление для этой модели. Если в этом представлении вы обратитесь к ViewData.ModelMetadata, то модель в этом случае будет объект класса Contact.


Однако, с помощью объекта метаданных вы можете получить информацию обо всех свойствах объекта. Вы можете получить коллекцию объектов ModelMetadata для каждого из свойств. В контексте нашего примера, мы получим 3 новых метаобъекта, для FirstName, LastName и Age. Если вы посмотрите на метаданные для свойства FirstName, вы увидите что тип данных модели string, а тип объекта-контейнера — Contact. Таким образом вы можете рекурсивно перебрать все свойства сложного объекта.


Где мне взять ModelMetadata?


Используя текущую модель.


Самый простой способ получить доступ к метаданным — это использовать свойство ViewData. В этом случае ModelMetadata описывает модель, представленную в ViewData. В случае рендеринга шаблона для объекта, этот способ является самым простым.


Получить одно из свойств метаданных, которые у вас уже есть.


Если у вас уже есть объект метаданных, вы можете использовать свойство Properties, которое возвращает список объектов ModelMetadata для каждого свойства.


Используя выражения


Класс ModelMetadata имеет два статических метода: FromStringExpression и FromLambdaExpression. Эти методы используются когда мы хотим получить из выражения (к примеру «PropertyName» или «m => m.PropertyName») соответствующие метаданные. Большинство существующих HTML хелперов уже переписаны в рамках этих двух методов.


Что у них внутри?


Перед тем как говорить от том как работать с ModelMetadata, давайте посмотрим какую информацию предоставляет объект класса ModelMetadata.


Свойства относящиеся к модели и её контейнеру:


  • Model и ModelType
    Возвращает модель и тип модели. И хотя значение может быть null, мы всё же можем узнать тип данных.
  • ContainerType и PropertyName
    Возвращает тип объекта, содержащего рассматриваемое свойство и имя свойства. Не все
    модели представляют свойства, поэтому эти свойства могут содержать null.
  • Properties
    Возвращает коллекцию объектов ModelMetadata, которые описывают свойства текущей модели.

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


  • ConvertEmptyStringToNull
    Флаг, показывающий нужно ли конвертировать пустую строку, полученную с клиента, в null.
    Default: true
  • DataTypeName
    Строка, с помощью которой можно получить информацию о типе данных (например, чтобы узнать, является ли текущая строка адресом электронной почты). Часто используемые имена типов данных: «EmailAddress», «Html», «Password» и «Url».
    Default: null
  • Description
    Описание модели.
    Default: null
  • DisplayFormatString
    Формат для отображения значения модели в шаблоне.
    Default: null
  • DisplayName
    Отображаемое имя модели.
    Default: null
  • EditFormatString
    Формат строки, который будет использоваться для редактирования значения в шаблоне.
    Default: null
  • HideSurroundingHtml
    Флаг, показывающий что текущее поле не должно сопровождаться какими-либо html-тегами (например, label). Часто используется для генерации полей типа «hidden»
    Default: false
  • IsComplexType
    Флаг, показывающий должна ли система относится к типу как к сложному (и поэтому использовать шаблон для комплексного типа), или же как к простому (например string).
    Пользователем не устанавливается.
  • IsNullableValueType
    Флаг, показывающий является ли тип Nullable-типом/
    Пользователем не устанавливается.
  • IsReadOnly
    Флаг, показывающий что данное свойство только для чтения.
    Default: false
  • IsRequired
    Является ли свойство обязательным.
    Default: true для не nullable-типов; false для всех остальных.
  • NullDisplayText
    Текст, который должен отображаться, если модель равна null.
    Default: null
  • ShortDisplayName
    Короткое имя для текущей модели. Предназначено для использования в заголовках таблицы. Если это свойство не установлено, то используется свойство DisplayName.
    Default: null
  • ShowForDisplay
    Должна ли использоваться текущая модель для просмотра.
    Default: true
  • ShowForEdit
    Должна ли использоваться текущая модель для редактирования
    Default: true
  • SimpleDisplayText
    Текст, который должен показываться для заданной модели в строке итогов. В противном случае используется отображение сложного объекта.
    Default: см. далее
  • TemplateHint
    Хинт, используемый для модели.
    Default: null
  • Watermark
    Текст, который может отображаться как подсказка, в строке ввода при редактировании.
    Default: null

Методы


  • GetDisplayName()
    Этот метод может быть использован для получения имени модели. Возвращает значение свойства DisplayName, если оно не равно null. Если PropetyName не null, то возвращает его. Иначе возвращает ModelType.Name.
  • GetValidators()
    Этот метод используется для того, чтобы получить валидаторы для данной модели. Могут быть использованы как для серверной валидации, так и для генерации правил клиентской валидации.

Значение по умолчанию для SimpleDisplayText следует следующим правилам:


  • Если Model равна null, возвращается NullDisplayText
  • Если метод модели ToString имеет перегрузку, то возвращается результат его вызова.
  • Если у модели нет свойств, возвращается String.Empty
  • Если первое свойство модели равно null, возвращается NullDisplayText
  • Иначе возвращается ToString() первого свойства.

Как формируется ModelMetadata?


Мы добавили настраиваемую систему метаданных в ASP.NET MVC 2. По умолчанию, мета-объекты формируются на основе данных, полученных из атрибутов.


Следующие атрибуты используются для формирования модели метаданных:


  • [HiddenInput] (System.Web.Mvc)
    Свойство, помеченное этим атрибутом, будет генерировать «hidden»-поле в режиме редактирования. По умолчанию, в этом случае также не будут использоваться сопровождающие html-теги. В случае если вы установите флаг DisplayValue в true, будут генерироваться и сопровождающий html и «hidden»-поле. Кроме того, в этом случае установится свойство TemplateHint (которое может быть перекрыто атрибутом [UIHint])
  • [UIHint] (System.ComponentModel.DataAnnotations)
    Устанавливает свойство TemplateHint.
  • [DataType] (System.ComponentModel.DataAnnotations)
    Устанавливает свойство DataTypeName.
  • [ReadOnly] (System.ComponentModel)
    Устанавливает свойство IsReadOnly. Обратите внимание, что любое свойство без сеттера будет автоматически помечаться атрибутом [ReadOnly].
  • [DisplayFormat] (System.ComponentModel.DataAnnotations)
    С помощью этого аттрибута можно установить NullDisplayText для метаданных. Если установить свойство атрибута DataFormatString, то оно применится свойству DisplayFormatString метаданных. Если установить свойство атрибута ApplyFormatInEditMode в true, то он применится также и для EditFormatString. Установка свойства ConvertEmptyStringToNull повлияет на свойство метаданных ConvertEmptyStringToNull.
  • [ScaffoldColumn] (System.ComponentModel.DataAnnotations)
    Устанавливаются свойства ShowForDisplay и ShowForEdit
  • [DisplayName] (System.ComponentModel)
    Используется для свойства DisplayName.

Часть третья. Встроенные шаблоны.


Подход к использованию шаблонов


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


Path


Когда решено какой шаблон использовать, система просматривает несколько путей. По каждому из них, ищет ascx-файл с именем «DisplayNames/TemplateName» или «EditorName/TemplateName», в зависимости от того какой результат вы хотите.


Поиск происходит в следующем порядке:


  • ~/Areas/AreaName/Views/ControllerName/DisplayTemplates/TemplateName.aspx и .ascx
  • ~/Areas/AreaName/Views/Shared/DisplayTemplates/TemplateName.aspx и .ascx
  • ~/Views/ControllerName/DisplayTemplates/TemplateName.aspx и .ascx
  • ~/Views/Shared/DisplayTemplates/TemplateName.aspx и .ascx

(Замените DisplayTemplates на EditorTemplates для шаблонов редактирования.)


Имена шаблонов


Имена шаблонов ищутся в следующем порядке:


  • Значение свойства TemplateHint объекта ModelMetadata
  • Значение свойства DataTypeName объекта ModelMetadata
  • Имя типа.
  • Если объект является простым типом, то «String»
  • Если объект является сложным типом и интерфейсом, то «Object»
  • Если объект является сложным типом и не интерфейсом, то рекурсивно, через всю иерархию вложенных свойств, пытается получить имя для каждого свойства

Когда происходит поиск по имени типа, то имеется в виду простое имя (например Type.Name), без пространства имён. Кроме того, если тип является Nullable<T>, поиск выполняется для типа T (то есть шаблон Boolean будет использоваться как для «bool», так и для «Nullable<bool>»). То есть если вы пишете шаблон для value-типа, вам необходимо учитывать, будет ли тип nullable. Ниже рассматривается пример со встроенным шаблоном для типа Boolean.


Класс TemplateInfo


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


Основное свойство, используемое в TemplateInfo — это FormattedModelValue. Значением этого поля является либо корректно отформатированное значение модели, в виде строки (на основе формата указанного в ModelMetadata), либо оригинальное значение модели (если не указан формат строки).


Есть ещё несколько вещей, которые мы тоже будем использовать (например свойство TemplateDepth и метод Visited). Но смысл их я объясню по мере знакомства с ними.


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


Всего существует 9 имён шаблонов, которые используются системой для отображения данных: «Boolean», «Decimal», «EmailAddress», «HiddenInput», «Html», «Object», «String», «Text» и «Url». Два из них («Text» и «String») реализованы одинаково. У некоторых из них есть соответствующие аналоги для шаблонов редактирования, у некоторых — нет.


Встроенные шаблоны в ASP.NET MVC реализованы в коде, но для примера я переписал их функциональность в .ascx файлах. Это поможет вам лучше разобраться с ними, и вам будет проще написать свою версию любого из этих шаблонов.


DisplayTemplate/String.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %>

Никаких сюрпризов: просто отображаем модель, заменив в ней специальные символы html.


DisplayTemplates/Html.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%= ViewData.TemplateInfo.FormattedModelValue %>

Этот шаблон даже немного проще предыдущего, потому что тип «Html» подразумевает, что содержимое будет в виде html и поэтому не должно быть кодировано. Будьте аккуратны при использовании этого типа если данные получены от конечного пользователя, во избежание XSS-атак!


DisplayTemplates/EmailAddress.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<a href="mailto:<%= Html.AttributeEncode(Model) %>"><%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %></a>

Этот шаблон предполагает, что ваша модель является адресом электронной почты, и автоматически создаёт ссылку на неё. Обратите внимание, что модель используется для адреса электронной почты, а FormattedModelValue для отображения.


DisplayTemplates/Url.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<a href="mailto:<%= Html.AttributeEncode(Model) %>"><%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %></a>

Работает подобно предыдущему примеру.


DisplayTemplates/HiddenInput.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<% if (!ViewData.ModelMetadata.HideSurroundingHtml) { %>
<%=	Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %>
<% } %>

Этот шаблон используется в паре с атрибутом [HiddenInput] (описанный выше). Он будет генерировать отображаемое значение только если пользователь явно не указал свойство HideSurroundHtml.


DisplayTemplates/Decimal.ascx


<%@ control language="C#" inherits="System.Web.Mvc.ViewUserControl" %>
<script runat="server">
private object FormattedValue
{
	get
	{
		if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelMetadata.Model)
		{
			return String.Format(System.Globalization.CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelMetadata.Model);
		}
		return ViewData.TemplateInfo.FormattedModelValue;
	}
}
</script>
<%= Html.Encode(FormattedValue) %>

Этот шаблон отображает значение типа decimal, с двумя знаками после запятой, поскольку этот тип в основном используется для денежных представлений. Обратите внимание на условие такого форматирования.


DisplayTemplates/Boolean.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<script runat="server">
	private bool? ModelValue
	{
		get
		{
			bool? value = null;
			if (ViewData.Model != null)
			{
				value = Convert.ToBoolean(ViewData.Model, System.Globalization.CultureInfo.InvariantCulture);
			}
			return value;
		}
	}
</script>
<% if (ViewData.ModelMetadata.IsNullableValueType) { %>
<select class="list-box tri-state" disabled="disabled">
	<option value="" <%= ModelValue.HasValue ? "" : "selected='selected'" %>>
		Not Set</option>
	<option value="true" <%= ModelValue.HasValue && ModelValue.Value ? "selected='selected'" : "" %>>
		True</option>
	<option value="false" <%= ModelValue.HasValue && !ModelValue.Value ? "selected='selected'" : "" %>>
		False</option>
</select>
<% } else { %>
<input class="check-box" disabled="disabled" type="checkbox"
	<%= ModelValue.Value ? "checked='checked'" : "" %> />
<% } %>

Шаблон для Boolean интересен тем, что в нём необходимо генерировать разметку в зависимости от того является ли тип nullable, или нет. Для non-nullable используется обычный чекбокс, в противном случае отображается выпадающий список с тремя значениями.


DisplayTemplates/Object.ascx


Стоит объяснить логику работы этого шаблона, прежде чем мы рассмотрим код, поскольку очень много работы проделывается от вашего имени.


Главная цель шаблон для типа object это всех свойств сложного объекта, вместе с html-разметкой для каждого. Кроме того, он ответственный за отображение значения модели NullDisplayText, если значение её null. И обеспечивает, так называемое «мелкое погружение» (shallow dive), отображая только нужный уровень свойств. Позднее мы поговорим о настройке этого шаблона, включая подготовку операции «глубокое погружение» (deep dive).


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>

<% if (Model == null) { %>
	<%= ViewData.ModelMetadata.NullDisplayText %>
<% } else if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
	<%= ViewData.ModelMetadata.SimpleDisplayText %>
<% } else { %>
	<% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm))) { %>
		<% if (prop.HideSurroundingHtml) { %>
			<%= Html.Display(prop.PropertyName) %>
		<% } else { %>
			<% if (!String.IsNullOrEmpty(prop.GetDisplayName())) { %>
				<div class="display-label"><%= prop.GetDisplayName() %></div>
			<% } %>
			<div class="display-field"><%= Html.Display(prop.PropertyName) %></div>
		<% } %>
	<% } %>
<% } %>

Давайте рассмотрим этот пример более детально.


<% if (Model == null) { %>
	<%= ViewData.ModelMetadata.NullDisplayText %>
<% } 

В этом месте всё понятно, выводится NullDisplayText, в случае если модель равна null.


else if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
	<%= ViewData.ModelMetadata.SimpleDisplayText %>
<% } 

В этом месте мы ограничиваем уровень вложенности свойств («мелкое погружение», shallow dive). Класс TemplateInfo отслеживает глубину вложенности шаблонов. Для верхнего уровня свойство TemplateDepth равно 1.


else { %>
	<% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay 
				&& !ViewData.TemplateInfo.Visited(pm))) { %>

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


<% if (prop.HideSurroundingHtml) { %>
	<%= Html.Display(prop.PropertyName) %>
<% } 

Если это свойство установлено в true, то шаблон просто просит свойство отобразиться, без лишней html-разметки.


<% if (!String.IsNullOrEmpty(prop.GetDisplayName())) { %>
	<div class="display-label"><%= prop.GetDisplayName() %></div>
<% } %>
<div class="display-field"><%= Html.Display(prop.PropertyName) %></div>

В этом месте проверяется есть ли отображаемое имя у свойства и, если оно есть, оно выводится, обрамляемое div-ом. Затем запускается рендеринг для свойства.


Встроенные шаблоны редактирования


Шаблоны редактирования немного сложнее, чем шаблоны отображения, поскольку они включают возможность редактирования значений. Они строятся на основе существующих HTML-хелперов. Всего существует 7 шаблонов редактирования: «Boolean», «Decimal», «HiddenInput», «MultilineText», «Object», «Password» и «String».


Часто, в качестве имени элемента в html-хелперы будут передаваться пустые строки. Обычно это неправильно, но в случае с шаблонами у нас есть «контекст» свойства. С помощью этого нам проще работать со сложными объектами, поскольку мы можем сохранить иерархию вложенности свойств (например «Contact.HomeAddress.City»).


Когда вы передаёте имя в html-хелпер, вы как бы говорите «дай мне текстбокс для редактирования свойства объекта, которого зовут „City“. Но что случится если ваш шаблон не для адреса, как сложного объекта, а для города, как для строки? Если вы передаёте пустую строку, в качестве имени, то вы говорите html-хелперу „дай мне текстбокс, чтобы отредактировать себя“.


EditorTemplates/String.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%= Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue,
		new { @class = "text-box single-line" }) %>

И снова начнём со строки, поскольку это простейший для понимания шаблон. Этот шаблон говорит системе, что хочет получить текстбокс (для редактирования себя), и мы инициируем его форматированным значением редактируемого свойства. Дополнительно, для этого текстбокса мы хотим использовать два класса CSS „text-box“ и „single-line“.


EditorTemplates/Password.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%= Html.Password("", ViewData.TemplateInfo.FormattedModelValue,
		new { @class = "text-box single-line password" }) %>

Шаблон для ввода пароля похож на шаблон для строки, за исключением того, что мы вызываем метод хелпера Password и используем ещё один класс CSS (»password").


EditorTemplates/MultilineText.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%= Html.TextArea("", ViewData.TemplateInfo.FormattedModelValue.ToString(),
					0, 0, new { @class = "text-box multi-line" }) %>

И снова без всяких сюрпризов. Вызываем метод TextArea, указываем количество строк и колонок равное 0 (поскольку мы используем CSS), и вместо класса «single-line» используем класс «multi-line».


EditorTemplates/HiddenInput.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<script runat="server">
	private object ModelValue
	{
		get
		{
			if (Model is System.Data.Linq.Binary)
			{
				return Convert.ToBase64String(((System.Data.Linq.Binary)Model).ToArray());
			}
			if (Model is byte[])
			{
				return Convert.ToBase64String((byte[])Model);
			}
			return Model;
		}
	}
</script>
<% if (!ViewData.ModelMetadata.HideSurroundingHtml) { %>
	<%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %>
<% } %>
<%= Html.Hidden("", ModelValue) %>

Как видите, намного сложнее, чем в шаблоне для отображения. Свойство ModelValue определяет является ли модель массивом байт или бинарным linq2sql объектом, и конвертирует их в строку с помощью base64. Затем записывает результат в hidden-поле.


EditorTemplates/Decimal.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<script runat="server">
	private object ModelValue
	{
		get
		{
			if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelMetadata.Model)
			{
				return String.Format(System.Globalization.CultureInfo.CurrentCulture,
							"{0:0.00}", ViewData.ModelMetadata.Model);
			}
			return ViewData.TemplateInfo.FormattedModelValue;
		}
	}
</script>
<%= Html.TextBox("", ModelValue, new { @class = "text-box single-line" }) %>

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


EditorTemplates/Boolean.ascx


<%@ control language="C#" inherits="System.Web.Mvc.ViewUserControl" %>

<script runat="server">
	private List<SelectListItem> TriStateValues
	{
		get
		{
			return new List<SelectListItem> {
				new SelectListItem { Text = "Not Set",
									 Value = String.Empty,
									 Selected = !Value.HasValue },
				new SelectListItem { Text = "True",
									 Value = "true",
									 Selected = Value.HasValue && Value.Value },
				new SelectListItem { Text = "False",
									 Value = "false",
									 Selected = Value.HasValue && !Value.Value },
			};
		}
	}
	private bool? Value
	{
		get
		{
			bool? value = null;
			if (ViewData.Model != null)
			{
				value = Convert.ToBoolean(ViewData.Model,
							System.Globalization.CultureInfo.InvariantCulture);
			}
			return value;
		}
	}
</script>

<% if (ViewData.ModelMetadata.IsNullableValueType) { %>
	<%= Html.DropDownList("", TriStateValues, new { @class = "list-box tri-state" })%>
<% } else { %>
	<%= Html.CheckBox("", Value ?? false, new { @class = "check-box" })%>
<% } %>

Этот шаблон тоже похож на соответствующий шаблон для отображения.


EditorTemplates/Object.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<% if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
	<%= ViewData.ModelMetadata.SimpleDisplayText%>
<% }
	else { %>    
	<% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForEdit
					&& !ViewData.TemplateInfo.Visited(pm))) { %>
		<% if (prop.HideSurroundingHtml) { %>
			<%= Html.Editor(prop.PropertyName) %>
		<% } else { %>
			<% 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>
		<% } %>
	<% } %>
<% } %>

Точно также этот шаблон практически не изменился, только добавился вызов метода ValidationMessage.


Часть четвёртая. Создание собственных шаблонов


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


Models/SampleModel.cs


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

public class SampleModel
{
	public static SampleModel Create()
	{
		return new SampleModel
		{
			Boolean = true,
			EmailAddress = "admin@contoso.com",
			Decimal = 21.1234M,
			Integer = 42,
			Hidden = "Uneditable",
			HiddenAndInvisible = "Also uneditable",
			Html = "This is <b>HTML</b> enabled",
			MultilineText = "This\r\nhas\r\nmultiple\r\nlines",
			NullableBoolean = null,
			Password = "supersecret",
			String = "A simple string",
			Url = "http://www.microsoft.com/",
		};
	}

	public bool Boolean { get; set; }

	[DataType(DataType.EmailAddress)]
	public string EmailAddress { get; set; }

	public decimal Decimal { get; set; }

	[HiddenInput]
	public string Hidden { get; set; }

	[HiddenInput(DisplayValue = false)]
	public string HiddenAndInvisible { get; set; }

	[DataType(DataType.Html)]
	public string Html { get; set; }

	[Required]
	[Range(10, 100)]
	public int Integer { get; set; }

	[DataType(DataType.MultilineText)]
	public string MultilineText { get; set; }

	public bool? NullableBoolean { get; set; }

	[DataType(DataType.Password)]
	public string Password { get; set; }

	public string String { get; set; }

	[DataType(DataType.Url)]
	public string Url { get; set; }

	[DisplayFormat(NullDisplayText = "(null value)")]
	public ChildModel ChildModel { get; set; }
}

Models/ChildModel.cs


using System.ComponentModel.DataAnnotations;

[DisplayColumn("FullName")]
public class ChildModel
{
	[Required, StringLength(25)]
	public string FirstName { get; set; }

	[Required, StringLength(25)]
	public string LastName { get; set; }

	[ScaffoldColumn(false)]
	public string FullName 
	{
		get
		{
			return FirstName + " " + LastName;
		}
	}
}

Controllers/HomeController.cs


using System.Web.Mvc;

public class HomeController : Controller
{
	static SampleModel model = SampleModel.Create();

	public ViewResult Index()
	{
		return View(model);
	}

	public ViewResult Edit()
	{
		return View(model);
	}

	[HttpPost]
	[ValidateInput(false)]
	public ActionResult Edit(SampleModel editedModel)
	{
		if (ModelState.IsValid)
		{
			model = editedModel;
			return RedirectToAction("Details");
		}

		return View(editedModel);
	}
}

Views/Home/Index.aspx


<%@ Page Language="C#" MasterPageFile="~/Views/shared/Site.master" Inherits="ViewPage<SampleModel>" %>

<asp:Content ContentPlaceHolderID="MainContent" runat="server">
	<h3>Details</h3>
	<fieldset style="padding: 1em; margin: 0; border: solid 1px #999;">
		<%= Html.DisplayForModel() %>
	</fieldset>
	<p><%= Html.ActionLink("Edit", "Edit") %></p>
</asp:Content>

Views/Home/Edit.aspx


<%@ Page Language="C#" MasterPageFile="~/Views/shared/Site.master" Inherits="ViewPage<SampleModel>" %>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
	<h3>Edit</h3>
	<% using (Html.BeginForm()) { %>
		<fieldset style="padding: 1em; margin: 0; border: solid 1px #999;">
			<%= Html.ValidationSummary("Broken stuff:") %>
			<%= Html.EditorForModel() %>
			<input type="submit" value="  Submit  " />
		</fieldset>
	<% } %>
	<p><%= Html.ActionLink("Details", "Index") %></p>
</asp:Content>

Шаблон отображения по умолчанию


Если мы запустим приложение с шаблонами по умолчанию, то мы увидим такую страницу:


Шаблона отображения по умолчанию

Страница для редактирования будет выглядеть так:


Шаблона редактирования по умолчанию

Используем таблицы


Давайте сделаем нашу разметку в виде таблицы, чтобы пара имя/значение отображалась в одной строке. Обратите внимание, что в шаблоне для редактирования выводятся звёздочки для обязательных полей.


Views/Shared/DisplayTemplates/Object.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<% if (Model == null) { %>
	<%= ViewData.ModelMetadata.NullDisplayText %>
<% } else if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
	<%= ViewData.ModelMetadata.SimpleDisplayText %>
<% } else { %>
	<table cellpadding="0" cellspacing="0" border="0">
	<% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm))) { %>
		<% if (prop.HideSurroundingHtml) { %>
			<%= Html.Display(prop.PropertyName) %>
		<% } else { %>
			<tr>
				<td>
					<div class="display-label" style="text-align: right;">
						<%= prop.GetDisplayName() %>
					</div>
				</td>
				<td>
					<div class="display-field">
						<%= Html.Display(prop.PropertyName) %>
					</div>
				</td>
			</tr>
		<% } %>
	<% } %>
	</table>
<% } %>

В итоге получаем следующий вид:


Подменённый шаблон отображения

Views/Shared/EditorTemplates/Object.ascx


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<% if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
	<%= ViewData.ModelMetadata.SimpleDisplayText %>
<% } else { %>
	<table cellpadding="0" cellspacing="0" border="0">
	<% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForEdit && !ViewData.TemplateInfo.Visited(pm))) { %>
		<% if (prop.HideSurroundingHtml) { %>
			<%= Html.Editor(prop.PropertyName) %>
		<% } else { %>
			<tr>
				<td>
					<div class="editor-label" style="text-align: right;">
						<%= prop.IsRequired ? "*" : "" %>
						<%= Html.Label(prop.PropertyName) %>
					</div>
				</td>
				<td>
					<div class="editor-field">
						<%= Html.Editor(prop.PropertyName) %>
						<%= Html.ValidationMessage(prop.PropertyName, "*") %>
					</div>
				</td>
			</tr>
		<% } %>
	<% } %>
	</table>
<% } %>

Таким образом мы сделали такую разметку:


Подменённый шаблон редактирования

Мелкое и глубокое погружение (shallow dive vs. deep dive)


На скриншоте выше, ChildModel отображается как (null value), потому что мы установили это значение с помощью атрибута в коде.


Обратите внимание, что даже в режиме редактирования, мы не можем изменить это свойство.


Если мы изменим шаблон редактирования, удалив в нём первое условие «if», то получим следующее:


Глубокое погружение в шаблоне редактирования

А в режиме просмотра:


Мелкое погружение в шаблоне отображения

Поскольку мы не изменили шаблон для отображения, мы всё ещё получаем результат с логикой «мелкого погружения» (shallow dive). Полное имя показывается потому что мы используем для модели атрибут [DisplayColumn], с помощью которого указали что отображать надо свойство FullName.

.

Если мы изменим шаблон отображения для использования «глубокого погружения» (deep dive), то получим следующее:


Глубокое погружение в шаблона отображения

Часть пятая. MasterPage Шаблоны


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


DisplayTemplates/Template.master


<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<script runat="server">
	protected override void OnInit(EventArgs e) {
		base.OnInit(e);
		if (ViewData.ModelMetadata.HideSurroundingHtml) {
			TablePlaceholder.Visible = false;
		}
		else {
			Controls.Remove(Data);
			DataPlaceholder.Controls.Add(Data);
		}
	}
</script>
<asp:ContentPlaceHolder runat="server" id="Data" />
<asp:PlaceHolder runat="server" id="TablePlaceholder">
	<table cellpadding="0" cellspacing="0" border="0" width="100%">
		<tr>
			<td style="width: 10em;">
				<div class="display-label" style="text-align: right;">
					<asp:ContentPlaceHolder runat="server" id="Label">
						<%= ViewData.ModelMetadata.GetDisplayName() %>
					</asp:ContentPlaceHolder>
				</div>
			</td>
			<td>
				<div class="display-field">
					<asp:PlaceHolder runat="server" id="DataPlaceholder" />
				</div>
			</td>
		</tr>
	</table>
</asp:PlaceHolder>

Суть поведения этой страницы заключается в определении двух ContentPlaceHolder-ов, называемыми «Label» и «Data». Контент «Label» должен отображать имя свойства, а «Data», соответственно, значение.


Этот шаблон использует одну особенность веб-форм. В методе OnInit реорганизует страницу, в зависимости от того хотите ли вы использовать для модели обрамляющую html-разметку, или нет.


EditorTemplates/Template.master


<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<script runat="server">
	protected override void OnInit(EventArgs e) {
		base.OnInit(e);
		if (ViewData.ModelMetadata.HideSurroundingHtml) {
			TablePlaceholder.Visible = false;
		}
		else {
			Controls.Remove(Data);
			DataPlaceholder.Controls.Add(Data);
		}
	}
</script>
<asp:ContentPlaceHolder runat="server" id="Data" />
<asp:PlaceHolder runat="server" id="TablePlaceholder">
	<table cellpadding="0" cellspacing="0" border="0" width="100%">
		<tr>
			<td style="width: 10em;">
				<asp:ContentPlaceHolder runat="server" id="Label">
					<div class="editor-label" style="text-align: right;">
						<%= ViewData.ModelMetadata.IsRequired ? "*" : "" %>
						<%= Html.Label("") %>
					</div>
				</asp:ContentPlaceHolder>
			</td>
			<td>
				<div class="editor-field">
					<asp:PlaceHolder runat="server" id="DataPlaceholder" />
					<asp:ContentPlaceHolder runat="server" ID="Validation">
						<%= Html.ValidationMessage("", "*") %>
					</asp:ContentPlaceHolder>
				</div>
			</td>
		</tr>
	</table>
</asp:PlaceHolder>

Версия шаблона для редактирования отличается только тем, что есть третий placeholder, предназначенный для сообщений валидаторов и для отмечаются обязательные поля.


Простые типы с использованием шаблонов


В простых типах для использования мастер-шаблонов используется серверный тег <asp:Content ...>


DisplayTemplates/String.aspx


<%@ Page Language="C#" MasterPageFile="Template.Master" Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ContentPlaceHolderID="Data" runat="server">
	<%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %>
</asp:Content>

EditorTemplates/String.aspx


<%@ Page Language="C#" MasterPageFile="Template.Master" Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ContentPlaceHolderID="Data" runat="server">
	<%= Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue,
new { @class = "text-box single-line" }) %>
</asp:Content>

Шаблон для сложного типа


Шаблон для сложного типа стал намного проще, потому что мы вынесли общую разметку в мастер-шаблон. Остаётся только логика.


DisplayTemplates/Object.aspx


<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<% if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
	<%= ViewData.ModelMetadata.SimpleDisplayText %>
<% } else { %>
	<% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm))) { %>
		<%= Html.Display(prop.PropertyName) %>
	<% } %>
<% } %>

EditorTemplates/Object.aspx


<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<% if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
	<%= ViewData.ModelMetadata.SimpleDisplayText %>
<% } else { %>
	<% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForEdit && !ViewData.TemplateInfo.Visited(pm))) { %>
		<%= Html.Editor(prop.PropertyName) %>
	<% } %>
<% } %>

Обратите внимание, что эти шаблоны не используют MasterPage, потому что в них нет разметки, а только логика. Разметка генерируется при вызове шаблонов для простых типов.


Заключение


ASP.NET MVC 2 находится ещё в стадии бета-тестирования и кое-что может измениться к релизу. Но основные моменты останутся именно такими как и описано, и освоив это материал, вы окажетесь более подготовлены к выходу финальной версии.

Tags:
Hubs:
+15
Comments 6
Comments Comments 6

Articles