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

182

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

Пример приложения

Для целей этой статьи в Visual Studio создан новый проект MVC по имени HelperMethods с использованием шаблона Empty (Пустой) и отметкой флажка MVC в разделе Add folders and core references for (Добавить папки и основные ссылки для). В проект добавлен контроллер Home, код которого показан в примере ниже:

using System.Web.Mvc;

namespace HelperMethods.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Fruits = new string[] { "Яблоко", "Апельсин", "Груша" };
            ViewBag.Cities = new string[] { "Москва", "Лондон", "Париж" };

            string message = "Это HTML-элемент: <input>";

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

В методе действия Index() представлению передается пара массивов string через объект ViewBag, а в качестве объекта модели указан тип string. В папке /Views/Home был создан файл представления Index.cshtml с содержимым, приведенным в примере ниже. Это строго типизированное представление (с типом модели string) и в нем не применяется компоновка.

@model string
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div>
        Фрукты: 
        @foreach(string fruit in ViewBag.Fruits)
        {
            <b>@fruit</b>
        }
    </div>
    <div>
        Города: 
        @foreach(string city in ViewBag.Cities)
        {
            <b>@city</b>
        }
    </div>
    <div>
        Сообщение: 
        <p>@Model</p>
    </div>
</body>
</html>

Чтобы увидеть, как визуализируется это представление, необходимо запустить приложение. Стандартная конфигурация маршрутизации, добавленная в проект средой Visual Studio, отобразит корневой URL, автоматически запрошенный браузером, на действие Index в контроллере Home:

Запуск примера приложения

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

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

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

Простейшей разновидностью вспомогательных методов является встраиваемый вспомогательный метод, который определяется внутри представления. Мы можем создать встраиваемый вспомогательный метод для упрощения нашего примера представления, используя дескриптор @helper, как показано в примере ниже:

@model string
@{
    Layout = null;
}

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

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div>
        Фрукты: 
        @ListArrayStringItems(ViewBag.Fruits)
    </div>
    <div>
        Города: 
        @ListArrayStringItems(ViewBag.Cities)
    </div>
    <div>
        Сообщение: 
        <p>@Model</p>
    </div>
</body>
</html>

Подобно обычным методам C#, встраиваемые вспомогательные методы имеют имена и параметры. В этом примере был определен вспомогательный метод по имени ListArrayStringItems(), который принимает в качестве параметра строковый массив. Хотя встраиваемый вспомогательный метод выглядит похожим на метод, возвращаемое значение отсутствует. Содержимое тела вспомогательного метода обрабатывается и помещается в ответ, предназначенный для клиента.

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

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

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

...
@helper ListArrayStringItems(string[] items)
{
    <ul>
        @foreach (string value in items)
        {
            <li>@value</li>
        }
    </ul>
}
...

Изменения вносить пришлось только в одном месте. Это может выглядеть незначительным преимуществом в простом проекте, но является удобным способом сохранения представлений простыми и согласованными в реальном проекте. Результат произведенных изменений показан на рисунке ниже:

Изменение разметки во вспомогательном методе

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

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

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

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

using System;
using System.Web.Mvc;

namespace HelperMethods.Infrastructure
{
    public static class CustomHelpers
    {
        public static MvcHtmlString ListArrayStringItems(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> для каждой строки в массиве.

Первым параметром внешнего вспомогательного метода HTML является объект HtmlHelper, предваренный ключевым словом this, чтобы сообщить компилятору C# о том, что определяется расширяющий метод C#. Объект HtmlHelper предоставляет доступ к информации, которая может быть полезной при создании содержимого, через свойства, описанные в таблице ниже:

Полезные свойства, определенные в классе HtmlHelper
Свойство Описание
RouteCollection

Возвращает набор маршрутов, определенных приложением

ViewBag

Возвращает данные ViewBag, переданные из метода действия в представление, которое вызвало вспомогательный метод

ViewContext

Возвращает объект ViewContext, который предоставляет доступ к деталям запроса и тому, как он был обработан

Свойство ViewContext наиболее полезно, когда нужно создать содержимое, которое адаптируется к обрабатываемому запросу. Часто используемые свойства класса ViewContext описаны в таблице ниже:

Полезные свойства, определенные в классе ViewContext
Свойство Описание
Controller

Возвращает контроллер, обрабатывающий текущий запрос

HttpContext

Возвращает объект HttpContext, который описывает текущий запрос

IsChildAction

Возвращает true, если представление, вызвавшее вспомогательный метод, визуализировано дочерним действием

RouteData

Возвращает данные маршрутизации для запроса

View

Возвращает экземпляр реализации IView, из которого был вызван вспомогательный метод

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

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

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

TagBuilder tag = new TagBuilder("ul");

Наиболее полезные члены класса TagBuilder описаны в таблице ниже:

Полезные методы и свойства, определенные в классе TagBuilder
Метод или свойство Описание
InnerHtml

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

SetInnerText(string)

Устанавливает текстовое содержимое HTML-элемента. Параметр string кодируется, чтобы сделать содержимое безопасным для отображения

AddCssClass(string)

Добавляет класс CSS к HTML-элементу

MergeAttribute(string, string, bool)

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

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

return new MvcHtmlString(tag.ToString());

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

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

Специальный внешний вспомогательный метод используется немного по-другому, чем встраиваемый метод. В примере ниже приведены изменения, внесенные в файл /Views/Home/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>
        Фрукты: 
        @Html.ListArrayStringItems(ViewBag.Fruits as string[])
    </div>
    <div>
        Города: 
        @Html.ListArrayStringItems(ViewBag.Cities as string[])
    </div>
    <div>
        Сообщение: 
        <p>@Model</p>
    </div>
</body>
</html>

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

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

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

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

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

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

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

Инфраструктура ASP.NET MVC Framework обеспечивает защиту от злонамеренных данных, автоматически кодируя их так, что они могут быть безопасно добавлены на HTML-страницу. Пример такого кодирования можно видеть в контроллере Home рассматриваемого приложения, где представлению в качестве объекта модели передается потенциально ненадежная строка:

public ActionResult Index()
{
    // ...
    string message = "Это HTML-элемент: <input>";
    // ...
}

Этот объект модели содержит допустимый HTML-элемент, но когда значение визуализируется Razor, получается следующая HTML-разметка:

...
<div>
    Сообщение: 
    <p>Это HTML-элемент: &lt;input&gt;</p>
</div>
...

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

Демонстрация проблемы

Чтобы продемонстрировать проблему, в классе CustomerHelpers создан новый вспомогательный метод, как показано в примере ниже:

using System;
using System.Web.Mvc;

namespace HelperMethods.Infrastructure
{
    public static class CustomHelpers
    {
        // ...

        public static MvcHtmlString DisplayMessage(this HtmlHelper html, string msg)
        {
            string result = String.Format("Сообщение: <p>{0}</p>", msg);
            return new MvcHtmlString(result);
        }
    }
}

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

@model string
@using HelperMethods.Infrastructure
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <style>legend {font-weight:bold}</style>
</head>
<body>
    <fieldset>
        <legend>Это сообщение сгенерированное представлением</legend>
        <div>
            Сообщение:
            <p>@Model</p>
        </div>
    </fieldset>
    <br /><br />
    <fieldset>
        <legend>Это сообщение сгенерированное вспомогательным методом</legend>
        @Html.DisplayMessage(Model)
    </fieldset>
</body>
</html>

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

Сравнение кодирования значений данных

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

Кодирование содержимого вспомогательного метода

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

using System;
using System.Web.Mvc;

namespace HelperMethods.Infrastructure
{
    public static class CustomHelpers
    {
        // ...

        public static string DisplayMessage(this HtmlHelper html, string msg)
        {
            return String.Format("Сообщение: <p>{0}</p>", msg);
        }
    }
}

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

Обеспечение кодирования механизмом визуализации ответа, получаемого из вспомогательного метода

Проблема с элементом <input> решена, но элементы <p> также были закодированы, что совершенно не подходит. В таких ситуациях требуется большая избирательность, чтобы кодировать только значения данных. В примере ниже показано, как это сделать:

using System;
using System.Web.Mvc;

namespace HelperMethods.Infrastructure
{
    public static class CustomHelpers
    {
        // ...

        public static MvcHtmlString DisplayMessage(this HtmlHelper html, string msg)
        {
            string encodedMessage = html.Encode(msg);
            string result = String.Format("Сообщение: <p>{0}</p>", encodedMessage);
            return new MvcHtmlString(result);
        }
    }
}

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

Результат описанного изменения показан на рисунке:

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

Здесь видно, что содержимое, сгенерированное внешним вспомогательным методом, совпадает с содержимым, сгенерированным за счет использования значения модели напрямую в представлении.

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