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

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

19.2. Создание пользовательских вспомогательных методов

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

Создаем внутренний вспомогательный метод

Самый простой вид вспомогательных методов - внутренние (inline) вспомогательные методы, которые определяются внутри представления. Чтобы упростить наш пример представления, мы можем создать внутренний вспомогательный метод с помощью тега @helper, как показано в листинге 19-3.

Листинг 19-3: Создаем внутренний вспомогательный метод
@model string

@{
	Layout = null;
}

@helper ListArrayItems(string[] items)
{
	foreach (string str in items)
	{
		<b>@str </b>
	}
}

<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>Index</title>
</head>
<body>
	<div>
		Here are the fruits: @ListArrayItems(ViewBag.Fruits)
	</div>
	<div>
		Here are the cities: @ListArrayItems(ViewBag.Cities)
	</div>
	<div>
		Here is the message:
		<p>@Model</p>
	</div>
</body>
</html>

Внутренние вспомогательные методы имеют такие же имена и параметры, как и обычные методы C#. В данном примере мы определили вспомогательный метод под названием ListArrayItems, который принимает в качестве параметра строковый массив. Хотя внутренний вспомогательный метод выглядит как метод, он не возвращает никакого значения. Его содержание обрабатывается и помещается в ответ клиенту.

Подсказка

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

Тело внутреннего вспомогательного метод имеет такой же синтаксис, как и все остальное представление Razor. Строки литералов рассматриваются как статический HTML, а операторы, которые требуют обработки Razor, отмечаются префиксом @. Вспомогательный метод в примере смешивает статический HTML и теги Razor для перечисления элементов в массиве и дает тот же результат, что и наше первоначальное представление, сокращая дублирование кода и разметки.

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

Листинг 19-4: Изменяем содержимое вспомогательного метода
@helper ListArrayItems(string[] items)
{
	<ul>
		@foreach (string str in items)
		{
			<li>@str</li>
		}
	</ul>
}

Нужно внести только одно изменение - это может показаться незначительным преимуществом в таком простом примере, но в реальных проектах поможет сохранить представления простыми и последовательными. Результат этого изменения показан на рисунке 19-2.

Подсказка

Обратите внимание, что в этом примере мы отметили префиксом @ ключевое слово foreach, а в листинге 19-3 - нет. Дело в том, что первый элемент в теле вспомогательного метода изменился на элемент HTML, следовательно, мы должны сообщить Razor с помощью тега @, что мы используем оператор C #. В предыдущем примере элементов HTML не было, так что Razor рассматривал содержание тела вспомогательного метода как код. Такие мелочи было бы трудно отслеживать, но, к счастью, Visual Studio их подсвечивает.

Рисунок 19-2: Измененяем разметку в вспомогательном методе

Создаем внешние вспомогательные методы

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

В качестве альтернативы можно использовать внешние (external) вспомогательные методы HTML, которые выражаются как методы расширения C#. Внешние вспомогательные методы используются шире, но и создавать их сложнее, потому что C# не очень хорошо обрабатывает элементы HTML.

Чтобы продемонстрировать их работу, мы создали в проекте папку Infrastructure, а в ней - новый класс CustomHelpers.cs. Содержимое этого файла показано в листинге 19-5.

Листинг 19-5: Содержимое файла CustomerHelpers.cs
using System.Web.Mvc;

namespace HelperMethods.Infrastructure
{
	public static class CustomHelpers
	{
		public static MvcHtmlString ListArrayItems(this HtmlHelper html, string[] list)
		{
			TagBuilder tag = new TagBuilder("ul");
			foreach (string str in list)
			{
				TagBuilder itemTag = new TagBuilder("li");
				itemTag.SetInnerText(str);
				tag.InnerHtml += itemTag.ToString();
			}
			return new MvcHtmlString(tag.ToString());
		}
	}
}

Данный вспомогательный метод выполняет ту же функцию, что и встроенный вспомогательный метод в предыдущем примере - он принимает массив строк и генерирует HTML-элемент ul, содержащий элемент li для каждой строки в массиве.

Первый параметр этого вспомогательного метода – объект HtmlHelper с ключевым словом this, которое сообщает компилятору C#, что мы определяем метод расширения. Свойства HtmlHelper обеспечивают доступ к информации, которая может быть полезна при создании контента; они описаны в таблице 19-1.

Таблица 19-1: Свойства класса HtmlHelper
Свойство Описание
RouteCollection Возвращает набор маршрутов, определенных в приложении.
ViewBag Возвращает данные объекта из ViewBag, который был передан методом действия в представление, вызвавшее данный вспомогательный метод.
ViewContext Возвращает объект ViewContext, который обеспечивает доступ к информации о запросе и процессе его обработки (и который мы опишем далее в этой главе).

Свойство ViewContext наиболее полезно для создания контента, который адаптируется к текущему запросу. В таблице 19-2 описаны наиболее часто используемые свойства, определенные классом ViewContext.

Таблица 19-2: Свойства класса ViewContext
Свойство Описание
Controller Возвращает контроллер, обрабатывающий текущий запрос.
HttpContext Возвращает объект HttpContext для текущего запроса.
IsChildAction Возвращает true, если вызвавшее вспомогательный метод представление визуализируется дочерним действием (дочерние действия рассмотрены в главе 18).
RouteData Возвращает данные маршрутизации для запроса.
View Возвращает экземпляр реализации IView, которая вызвала вспомогательный метод.

С помощью вспомогательных методов вы можете получить всеобъемлющую информацию о запросе, но по большей части они используются, чтобы сохранить последовательность форматирования. Чтобы генерировать контент для определенных запросов, можно использовать встроенные (built-in) вспомогательные методы (они будут описаны далее в этой главе) или с помощью частичных представлений и дочерних действий реализовывать более сложные решения.

В нашем примере вспомогательного метода не требуется никакой информации о запросе, но нужно создать несколько элементов HTML. Самый простой способ создать код HTML во вспомогательных методах - это использовать класс TagBuilder, который позволяет создавать строки HTML, не используя специальные символы. Класс TagBuilder является частью сборки System.Web.WebPages.Mvc, но так как в нем используется функция миграции типов (type forwarding), кажется, что он является частью сборки System.Web.Mvc. Visual Studio добавляет в проекты MVC обе сборки, и использовать класс TagBuilder достаточно легко, хотя его и нет в документации Microsoft Developer Network (MSDN) API.

Чтобы создать новый экземпляр TagBuilder, передайте в конструктор как параметр имя элемента HTML, который хотите создать. В классе TagBuilder не нужно использовать угловые скобки (< и >), то есть создать элемент ul можно следующим образом:

TagBuilder tag = new TagBuilder("ul");

Наиболее часто используемые члены класса TagBuilder описаны в таблице 19-3.

Таблица 19-3: Члены класса TagBuilder
Член Описание
Innerhtml Свойство, которое позволяет записать содержимое элемента как строку HTML. Значение, присвоенное этому свойству, не будет закодировано, что означает, что с его помощью можно создавать вложенные элементы HTML.
SetInnerText(string) Устанавливает текстовое содержимое элемента HTML. Параметр string кодируется для безопасности отображения (см. раздел о кодировании строк HTML ранее в этой главе).
AddCssClass(string) Добавляет класс CSS к элементу HTML.
MergeAttribute(string, string, bool) Добавляет атрибут к элементу HTML. Первый параметр - это имя атрибут, второй - его значение. Параметр bool определяет, нужно ли заменить существующий атрибут с таким же названием.

Результатом вспомогательного метода HTML является объект MvcHtmlString, содержимое которого записывается непосредственно в ответ клиенту. В нашем примере мы передаем результат метода TagBuilder.ToString в конструктор объекта MvcHtmlString, как показано здесь:

return new MvcHtmlString(tag.ToString());

Этот оператор генерирует фрагмент кода HTML, который содержит элементы ul и li, и возвращает их в движок представлений, чтобы далее они были записаны в ответ.

Используем пользовательский внешний вспомогательный метод

Применение пользовательских внешних вспомогательных методов немного отличается от внутренних. В листинге 19-6 показаны изменения, которые мы внесли в файл /Views/Home/Index.cshtml для замены внутреннего вспомогательного метода на внешний.

Листинг 19-6: Используем пользовательский внешний вспомогательный метод в файле Index.cshtml
@model string
@using HelperMethods.Infrastructure

@{
	Layout = null;
}

<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>Index</title>
</head>
<body>
	<div>
		Here are the fruits: @Html.ListArrayItems((string[])ViewBag.Fruits)
	</div>
	<div>
		Here are the cities: @Html.ListArrayItems((string[])ViewBag.Cities)
	</div>
	<div>
		Here is the message:
		<p>@Model</p>
	</div>
</body>
</html>

Убедитесь, что в файле есть пространство имен, содержащее вспомогательный метод. Чтобы добавить его, мы использовали тег @using, но если вы разрабатываете много пользовательских вспомогательных методов, то можно добавить данное пространство имен в файл /Views/Web.config, чтобы они были доступны во всех представлениях.

Мы ссылаемся на вспомогательный метод с помощью @Html.<helper>, где <helper> - это имя метода расширения, в данном случае Html.ListArrayItems. Часть Html в этом выражении ссылается на свойство, которое определено в базовом классе представления и возвращает объект HtmlHelper, то есть тип, к которому мы применили метод расширения в листинге 19-5.

Мы передаем данные во внешний вспомогательный метод таким же образом, как и во внутренний или любой другой метод C#. Нужно только привести динамических свойства объекта ViewBag к типу, определенному этим внешним методом - в данном случае, к массиву строк. Пусть это и не такой красивый синтаксис, как во внутренних вспомогательных методах, но только так можно создать удобный многоразовый вспомогательный метод.

Когда использовать вспомогательные методы

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

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

Управляем кодировкой строк в вспомогательных методах

В MVC Framework для защиты от вредоносного ввода применяется автоматическое кодирование, которое позволяет гарантировать безопасность добавления данных на страницу. Пример кодирования вы можете увидеть в листинге 19-7, где мы передаем потенциально небезопасную строку в представление в качестве модели объекта (в листинге повторяется код контроллера Home).

Листинг 19-7: Контроллер Home
using System.Web.Mvc;

namespace HelperMethods.Controllers
{
	public class HomeController : Controller
	{
		public ActionResult Index()
		{
			ViewBag.Fruits = new string[] {"Apple", "Orange", "Pear"};
			ViewBag.Cities = new string[] {"New York", "London", "Paris"};

			string message = "This is an HTML element: <input>";

			return View((object) message);
		}
	}
}

Объект модели содержит допустимый элемент HTML, но после обработки Razor мы видим следующий код:

<div>
	Here is the message:
	<p>This is an HTML element: &lt;input&gt;</p>
</div>

Это основная мера безопасности, которая запрещает браузеру интерпретировать значения данных как действительную (допустимую) разметку, так как это является основой для распространенной формы атак, при которой злоумышленники пытаются изменить поведение приложения, пытаясь добавлять свой собственный код HTML или разметку JavaScript. Razor автоматически кодирует значения данных, когда они используются в представлении, но так как вспомогательные методы должны генерировать HTML, то они пользуются более высоким уровнем доверия со стороны движка представления - и в силу этого требуют более пристального внимания.

Описание проблемы

Чтобы дать вам представление о проблеме, связанной с кодировкой строк, мы создали новый вспомогательный метод в классе CustomerHelpers, как показано в листинге 19-8. Этот вспомогательный метод принимает в качестве параметра строку и генерирует такой же HTML, который мы включили в представление Index.

Листинг 19-8: Определяем новый вспомогательный метод
using System.Web.Mvc;
using System;

namespace HelperMethods.Infrastructure
{
	public static class CustomHelpers
	{
		public static MvcHtmlString ListArrayItems(this HtmlHelper html, string[] list)
		{
			TagBuilder tag = new TagBuilder("ul");
			foreach (string str in list)
			{
				TagBuilder itemTag = new TagBuilder("li");
				itemTag.SetInnerText(str);
				tag.InnerHtml += itemTag.ToString();
			}
			return new MvcHtmlString(tag.ToString());
		}

		public static MvcHtmlString DisplayMessage(this HtmlHelper html, string msg)
		{
			string result = String.Format("This is the message: <p>{0}</p>", msg);
			return new MvcHtmlString(result);
		}
	}
}

Мы используем метод String.Format, чтобы создать разметку HTML и передать результат в качестве аргумента в конструктор MvcHtmlString. В листинге 19-9 показаны изменения в представлении /View/Home/Index.cshtml, с помощью которых мы будем использовать новый вспомогательный метод (а также выделим контент, который генерируется вспомогательным методом).

Листинг 19-9: Используем вспомогательный метод DisplayMessage в представлении Index
@model string
@using HelperMethods.Infrastructure

@{
	Layout = null;
}

<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>Index</title>
</head>
<body>
	<p>This is the content from the view:</p>
	<div style="border: thin solid black; padding: 10px">
		Here is the message:
		<p>@Model</p>
	</div>

	<p>This is the content from the helper method:</p>
	<div style="border: thin solid black; padding: 10px">
		@Html.DisplayMessage(Model)
	</div>
</body>
</html>

Вы можете увидеть результат применения вспомогательного метода, запустив приложение, как показано на рисунке 19-3.

Рисунок 19-3: Сравниваем кодировку значений данных

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

Кодируем контент, генерируемый вспомогательными методами

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

Самое простое решение - изменить тип вывода вспомогательного метода на string, как показано в листинге 19-10. Это сообщает движку представления, что контент не является безопасным и должен быть закодирован, прежде чем он будет добавлен в представление.

Листинг 19-10: Razor кодирует контент, созданный вспомогательным методом
using System.Web.Mvc;
using System;

namespace HelperMethods.Infrastructure
{
	public static class CustomHelpers
	{
		public static MvcHtmlString ListArrayItems(this HtmlHelper html, string[] list)
		{
			TagBuilder tag = new TagBuilder("ul");
			foreach (string str in list)
			{
				TagBuilder itemTag = new TagBuilder("li");
				itemTag.SetInnerText(str);
				tag.InnerHtml += itemTag.ToString();
			}
			return new MvcHtmlString(tag.ToString());
		}

		public static string DisplayMessage(this HtmlHelper html, string msg)
		{
			return String.Format("This is the message: <p>{0}</p>", msg);
		}
	}
}

Таким образом, Razor будет кодировать весь контент, который возвращается вспомогательным методом. В целом это очень удобно, за исключением тех случаев, когда необходимо создать элементы HTML, как в данном примере. Результат показан на рисунке 19-4.

Рисунок 19-4: Движок представлений кодирует ответ вспомогательного метода

Мы решили проблему с элементом input, но наши элементы p также были закодированы, и мы получили не совсем то, что хотели. В таких ситуациях нужно быть более избирательными и кодировать только значения данных. Это показано в листинге 19-11.

Листинг 19-11: Применяем выборочное кодирование данных во вспомогательном методе
public static MvcHtmlString DisplayMessage(this HtmlHelper html, string msg) 
{
	string encodedMessage = html.Encode(msg);
	string result = String.Format(
		"This is the message: <p>{0}</p>", encodedMessage);
	return new MvcHtmlString(result);
}

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

Результат этого изменения показан на рисунке 19-5, на котором вы можете увидеть, что контент, генерируемый внешним вспомогательным методом, совпадает с контентом, который генерируется с помощью значения модели непосредственно в представлении.

Рисунок 19-5: Эффект выборочного кодирования контента во внешнем вспомогательном методе