Контроллеры

73

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

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

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

Для целей этой и следующих статей мы создадим новый проект MVC по имени ControllersAndActions с использованием шаблона Empty (Пустой), отметив флажок MVC в разделе Add folders and core references for (Добавить папки и основные ссылки для), а также проект модульного тестирования под названием ControllersAndActions.Tests. Модульные тесты, которые будут создаваться, не требуют имитированных реализаций, поэтому пакет Moq устанавливать не придется, но нужно установить пакет MVC, чтобы тесты имели доступ к базовым классам контроллеров.

В окне консоли диспетчера пакетов NuGet среды Visual Studio введите следующую команду:

Install-Package Microsoft.Aspnet.Mvc -version 5.0.0 -projectname ControllersAndActions.Tests

После создания проекта выберите пункт ControllersAndActions Properties (Свойства ControllersAndActions) в меню Project (Проект) среды Visual Studio, в открывшемся диалоговом окне перейдите на вкладку Web (Веб) и отметьте переключатель Specific Page (Определенная страница) в категории Start Action (Начальное действие). Вводить какое-либо значение не нужно - достаточно только выбора переключателя.

Понятие контроллера

Вы сталкивались со случаями использования контроллеров почти во всех предшествующих статьях, посвященных ASP.NET MVC. Наступило время заглянуть "за кулисы".

Создание контроллера с помощью интерфейса IController

В MVC Framework классы контроллеров должны реализовать интерфейс IController из пространства имен System.Web.Mvc, показанный в примере ниже:

using System.Web.Routing;

namespace System.Web.Mvc
{
    public interface IController
    {
        void Execute(RequestContext requestContext);
    }
}

Чтобы получить определение этого интерфейса, необходимо загрузить исходный код MVC Framework, который чрезвычайно полезен для выяснения внутренней работы средств инфраструктуры.

Как видите, интерфейс IController очень прост. Его единственный метод Execute() вызывается, когда запрос направляется этому классу контроллера. Инфраструктура MVC Framework выясняет, на какой класс ориентирован запрос, за счет чтения значения свойства controller, сгенерированного данными маршрутизации, или через специальные классы маршрутизации.

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

using System.Web.Mvc;
using System.Web.Routing;

namespace ControllersAndActions.Controllers
{
    public class BasicController : IController
    {
        public void Execute(RequestContext requestContext)
        {
            string controller = (string)requestContext.RouteData.Values["controller"];
            string action = (string)requestContext.RouteData.Values["action"];

            requestContext.HttpContext.Response.Write(
                    string.Format("Контроллер: {0}, Метод действия: {1}",
                    controller, action));
        }
    }
}

Методу Execute() интерфейса IController передается объект RequestContext, предоставляющий информацию о текущем запросе и маршруте, который ему соответствует (и приводит к вызову данного контроллера для обработки этого запроса). В классе RequestContext определены два свойства, описанные в таблице ниже:

Свойства, определяемые классом RequestContext
Имя Описание
HttpContext

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

RouteData

Возвращает объект RouteData, который описывает маршрут, соответствующий запросу

Объект HttpContextBase предоставляет доступ к набору объектов, описывающих текущий запрос, которые называются объектами контекста; мы еще вернемся к ним позже. Объект RouteData описывает маршрут. Важные свойства класса RouteData перечислены в таблице ниже:

Свойства, определяемые классом RouteData
Свойство Описание
Route

Возвращает реализацию RouteBase, которая соответствует маршруту

RouteHandler

Возвращает реализацию IRouteHandler, которая обрабатывает маршрут Values

Values

Возвращает коллекцию значений сегментов, индексированную по имени

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

Часть проблемы, возникающей при создании специальных контроллеров, связана с отсутствием доступа к таким средствам, как представления. Это означает, что придется работать на более низком уровне, чем и объясняется запись содержимого напрямую в ответ клиенту. Свойство HttpContextBase.Response возвращает объект HttpResponseBase, который позволяет конфигурировать и добавлять содержимое к ответу, предназначенному для отправки клиенту. Это еще одна точка соприкосновения между платформой ASP.NET и инфраструктурой MVC Framework.

Если запустить приложение и перейти на URL вида /Basic/Index, то специальный контроллер сгенерирует вывод, показанный на рисунке ниже:

Результат, сгенерированный классом BasicController

Реализация интерфейса IController позволяет создать класс, который MVC Framework распознает как контроллер и отправляет ему запросы, безо всяких ограничений относительно того, как запрос обрабатывается, и какой ответ для него генерируется. Это удачный пример, поскольку он показывает, насколько расширяемой может быть инфраструктура MVC Framework даже для ключевых строительных блоков вроде контроллеров, однако написание сложного приложения подобным образом может быть сопряжено с немалыми трудностями.

Классы с именами, заканчивающимися на Base

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

В Microsoft решили ввести возможность тестирования, поддерживая при этом совместимость с существующими приложениями ASP.NET Web Forms, так что в результате появились классы Base. Эти классы так называются из-за того, что они имеют те же самые имена, как у основных классов платформы ASP.NET, за которыми следует слово Base.

Так, например, платформа ASP.NET предоставляет контекстную информацию о текущем запросе и ряде ключевых служб приложения через объект HttpContext. Соответствующим ему классом Base является HttpContextBase, экземпляр которого передается методу Execute(), определенному в интерфейсе IController (в последующих примерах будут продемонстрированы и другие классы Base). В первоначальных классах и классах Base определены одни и те же свойства и методы, но классы Base всегда абстрактны, а это значит, что их легко применять для модульного тестирования.

Иногда вы получите экземпляр одного из первоначальных классов ASP.NET, такого как HttpContext. В таком случае необходимо создать дружественный к MVC класс Base, подобный HttpContextBase. Это делается с использованием одного из классов Wrapper, которые имеют такие же имена, как первоначальные классы, дополненные словом Wrapper, например, HttpContextWrapper. Классы Wrapper являются производными от классов Base и имеют конструкторы, которые принимают экземпляры первоначальных классов:

HttpContext context = getHttpContextFromSomewhere();
HttpContextBase baseContext = new HttpContextWrapper(context);

Первоначальные классы, классы Base и классы Wrapper определены в пространстве имен System.Web, поэтому платформа ASP.NET может гладко поддерживать приложения MVC Framework и более старые приложения Web Forms.

Создание контроллера за счет наследования от класса Controller

Как было продемонстрировано в предыдущем примере, инфраструктура MVC Framework допускает практически неограниченную настройку и расширение. Чтобы обеспечить любой требуемый вид обработки запросов и генерации результатов, можно реализовать интерфейс IController. Вам не нравятся методы действий? Вы не хотите беспокоиться по поводу визуализированных представлений? В таком случае можете взять дело в свои руки и реализовать лучший, быстрый и более элегантный способ обработки запросов. Либо же вы можете воспользоваться средствами, предлагаемыми командой разработчиков MVC Framework из Microsoft, и унаследовать свои контроллеры от класса System.Web.Mvc.Controller.

Класс Controller обеспечивает поддержку обработки запросов, которая знакома большинству разработчиков приложений MVC. Она применялась во всех примерах, рассмотренных в предыдущих статьях. Класс Controller предоставляет три ключевых средства, которые описаны ниже:

Методы действий

Поведение контроллера разнесено по множеству методов (вместо реализации в виде единственного метода Execute()). Каждый метод действия отображается на соответствующий URL и вызывается с параметрами, извлеченными из входящего запроса.

Результаты действий

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

Фильтры

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

Если только вы не имеете дело со специфичным требованием, то лучшим подходом к созданию контроллеров будет их наследование от класса Controller, что, как и можно было ожидать, делает среда Visual Studio, когда создает новый класс в ответ на выбор пункта меню Add --> Scaffold (Добавить --> Шаблон).

В примере ниже приведен код простого контроллера под названием DerivedController, созданного подобным образом. Он сгенерирован с применением варианта MVC 5 Controller - Empty (Контроллер MVC 5 - Пустой) с несколькими простыми изменениями, предназначенными для установки свойства ViewBag и выбора представления:

using System;
using System.Web;
using System.Web.Mvc;

namespace ControllersAndActions.Controllers
{
    public class DerivedController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = 
                "Привет из контроллера DerivedController метода действия Index";
            return View("MyView");
        }
    }
}

Класс Controller также обеспечивает связь с системой представлений Razor. В этом примере мы возвращаем результат вызова метода View(), которому в качестве параметра передается имя представления для визуализации клиенту. Чтобы создать это представление, создайте папку Views/Derived, щелкните на ней правой кнопкой мыши и выберите в контекстном меню пункт Add --> MVC 5 View Page (Razor) (Добавить --> Страница представления MVC 5 (Razor)). Укажите в качестве имени MyView.cshtml и щелкните на кнопке ОК для создания файла представления.

Приведите содержимое файла в соответствие с примером:

@{
    ViewBag.Title = "Index";
}

<h2>MyView</h2>
Сообщение от контроллера: &laquo; @ViewBag.Message &raquo;

После запуска приложения и перехода на URL вида /Derived/Index этот метод действия вызывается, а представление MyView визуализируется:

Результат, сгенерированный классом DerivedController

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

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