ASP.NET MVC Framework

Магдануров Гайдар

Юнев Владимир

ГЛАВА 4

Контроллеры

 

 

В паттерне MVC контроллеры выполняют следующие последовательные функции:

□ контроллер реагирует на действия клиента, например: нажатие кнопки отправки формы на сервер, Ajax-запросы, которые генерирует браузер, и др.;

□ контроллер оперирует с моделью, изменяя ее состояние;

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

 

Назначение контроллеров

Обзор контроллеров в ASP.NET MVC

Для разработчика ASP.NET MVC-контроллер представляет собой класс, унаследованный от базового класса Controller (который в свою очередь унаследован от класса ControllerBase, реализующего интерфейс IController). Каждый файл контроллера MVC Framework должен подчиняться следующим требованиям:

□ располагаться в папке Controllers корня проекта;

□ иметь суффикс Controller, например HomeController.cs, AccountController.cs;

□ класс контроллера должен иметь то же название, что и файл:

HomeController, AccountController.

Каждый контроллер содержит набор методов, которые в терминах MVC Framework называются действиями (actions). Действия реализуют бизнес-логику ответа и изменение модели данных в зависимости от действия пользователя или запроса клиента. Действия всегда возвращают результат, реализующий класс, наследующий ActionResult: такими классами являются следующие стандартные типы: ViewResult, JsonResult, FileResult, RedirectResult, RedirectToRouteResult, ContentResult, EmptyResult. В зависимости от возвращаемого типа клиент получит тот или иной тип набора данных: HTML-страницу, JSON-данные, бинарный файл и др. Кроме того, MVC Framework позволяет определять вам свои собственные определения типа возвращаемых значений.

Для примера рассмотрим проект ASP.NET MVC, который создается по умолчанию:

□ в файле AccountController.cs определен класс-контроллер AccountController, который содержит набор методов, возвращающих результат типа ActionResult;

□ класс AccountController, кроме всего прочего, содержит методы LogOn, LogOff, Register, ChangePassword и ChangePasswordSuccess, которые являются действиями, возвращающими результат типа ActionResult;

□ действия этого класса возвращают разнообразные типы значений. Так, большинство действий возвращают результат типа ViewResult, вызывая стандартный метод View. Некоторые методы могут вернуть результат в виде RedirectResult с помощью вызова метода Redirect или RedirectToRouteResult с помощью RedirectToAction;

□ в зависимости от типа возвращаемого значения пользователь получит определенный результат. Если метод вернул ViewResult, то пользователь получит HTML-код и увидит страницу. В случае когда результатом вызова действия будут данные типа RedirectResult, то браузер пользователя перенаправит вызов на другую страницу. В случае же когда тип возвращаемого значения — это RedirectToRouteResult, MVC Framework перенаправит вызов на другое действие текущего или иного контроллера.

Рассмотрим более конкретный пример, метод LogOn класса AccountController:

public ActionResult LogOn(string userName,

                  string password,

                  bool rememberMe,

                  string returnUrl)

{

  if (!ValidateLogOn(userName, password))

  {

    return View () ;

  }

  FormsAuth.SignIn(userName, rememberMe) ;

  if (!String.IsNullOrEmpty(returnUrl))

  {

    return Redirect(returnUrl);

  }

  else

  {

    return RedirectToAction("Index", "Home");

  }

}

Этот метод представляет действие, которое в зависимости от полученных от пользователя данных производит авторизацию пользователя либо сообщает об ошибке авторизации. В данном случае выполняется проверка данных пользователя и, если они неверны, возвращается стандартное представление, сопоставленное данному действию. Если данные верны, происходит авторизация, и пользователь перенаправляется либо на главную страницу, либо на URL, указанный в параметре returnUrl. Перенаправление на главную страницу происходит через вызов метода RedirectToAction, который возвращает результат типа RedirectToRouteResult, перенаправление на другой URL происходит через вызов Redirect, который возвращает результат типа RedirectResult.

 

Простой пример реализации контроллера

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

Перед созданием нашего примера заполним базу данных пользователей данными:

□ добавим две роли пользователей: Administrators и Users;

□ добавим пользователей Admin с ролью Administrators и User с ролью Users.

Для добавления этих данных необходимо воспользоваться встроенным в Visual Studio средством для управления пользователями, которое можно вызвать, нажав последнюю иконку на панели Solution Explorer, которая появляется во время того, когда активен проект, реализующий поддержку стандартных провайдеров базы данных пользователей (рис. 4.1).

Рис. 4.1. Панель кнопок Solution Explorer

Для того чтобы создать контроллер в Visual Studio 2008, необходимо проделать следующие действия: в контекстном меню папки Controllers выбрать пункт Add, затем Controller (рис. 4.2).

Рис. 4.2. Пункт меню, позволяющий добавить в проект новый контроллер

В появившемся окне необходимо ввести название класса нового контроллера, в нашем случае AdminController (рис. 4.3).

После этого Visual Studio сгенерирует в пространстве имен по умолчанию необходимый нам класс:

public class AdminController : Controller {

  //

  // GET: /Admin/

  public ActionResult Index() {

    return View();

  }

}

Рис. 4.3. Окно ввода имени контроллера

Visual Studio только облегчает вам жизнь, упрощая рутинные операции, но при желании вы могли бы создать этот класс самостоятельно.

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

public ActionResult Index()

{

  if (User.IsInRole("Administrators"))

  {

    MembershipProvider mp = Membership.Provider;

    int userCount;

    var users = mp.GetAllUsers(0, Int32.MaxValue, out userCount);

    ViewData.Model = users;

    return View ();

  }

  else

  {

    return RedirectToAction("Index", "Home");

  }

}

Здесь, после проверки на принадлежность текущего пользователя к группе Administrators, создается список зарегистрированных пользователей, после чего он передается в специальный объект viewData, который в MVC Framework содержит модель данных, необходимых для представления. Если пользователь прошел проверку на принадлежность к группе Administrators, то после создания набора действие завершится вызовом метода viewData, который сформирует представление, сопоставленное данному действию. В случае же, когда пользователь, не имеющий права на доступ к этому действию, вызовет его, действие перенаправит вызов на другое действие Index контроллера Home, что, по сути, означает перенаправление на главную страницу сайта.

Для отображения наших данных нам необходимо создать представление. В MVC Framework представление для контроллера должно создаваться по определенным правилам:

□ все представления для определенного контроллера должны находиться в папке, название которой повторяет название контроллера, например: Home, Account;

□ все такие папки должны находиться в папке Views, которая располагается в корне проекта или веб-сайта;

□ каждый отдельный файл представления должен иметь название, совпадающее с именем действия контроллера, которому оно соответствует, например LogOn.aspx, Register.aspx.

Согласно этим правилам создадим папку Admin в папке Views, в которую через контекстное меню добавим представление Index (рис. 4.4).

Рис. 4.4. Пункт меню, позволяющий добавить новое представление

Рис. 4.5. Окно создания нового представления

В окне Add View (рис. 4.5) необходимо указать название представления (без расширения файла), остальные параметры пока оставьте без изменения. Добавим разметку и код для формирования представления в файле Index.aspx:

             ContentPlaceHolderID="MainContent"

             runat="server">

Список пользователей

 

   

     

     

     

     

     

     

     

      <% foreach

       (MembershipUser user in

          (MembershipUserCollection)ViewData.Model) { %>

       

         

         

         

         

         

         

       

      <% } %>

   

Имя Email Последняя активность Подтвержден Заблокирован
<%= Html.ActionLink("Выбрать",

                "Select",

                new {userid = (Guid)user.ProviderUserKey}) %>

         

<%= Html.Encode(user.UserName) %> <%= Html.Encode(user.Email) %> <%= user.LastActivityDate %> <%= user.IsApproved %> <%= user.IsLockedOut %>

 

Теперь мы можем запустить проект и посмотреть на результат работы. Для этого нам необходимо войти в систему под пользователем Admin и перейти по адресу http://"наш сайт"/Admin. Если вы все сделали правильно, то результатом будет примерно такой список пользователей — рис. 4.6.

Рис. 4.6. Список пользователей

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

Листинг 4.1. Действия контроллера AdminController

public ActionResult Select(Guid? userId)

{

  if (!userId.HasValue)

    throw new HttpException(404, "Пользователь не найден");

  if (User.IsInRole("Administrators"))

  {

    MembershipProvider mp = Membership.Provider;

    MembershipUser user = mp.GetUser(userId, false);

    ViewData.Model = user;

    return View () ;

  }

  else

  {

    return RedirectToAction("Index", "Home");

  }

}

public ActionResult Update(Guid? userId,

                string email,

                bool isApproved,

                bool isLockedOut)

{

  if (!userId.HasValue)

    throw new HttpException(404, "Пользователь не найден");

  if (User.IsInRole("Administrators"))

  {

    MembershipProvider mp = Membership.Provider;

    MembershipUser user = mp.GetUser(userId, false);

    user.Email = email; user.IsApproved = isApproved;

    if (user.IsLockedOut && !isLockedOut)

      user.UnlockUser();

    mp.UpdateUser(user);

    return RedirectToAction("Index");

  }

  else

  {

    return RedirectToAction("Index", "Home");

  }

}

public ActionResult Delete(Guid? userId)

{

  if (!userId.HasValue)

    throw new HttpException(404, "Пользователь не найден");

  if (User.IsInRole("Administrators"))

  {

    MembershipProvider mp = Membership.Provider;

    MembershipUser user = mp.GetUser(userId, false);

    mp.DeleteUser(user.UserName, true);

    return RedirectToAction("Index");

  }

  else

  {

    return RedirectToAction("Index", "Home");

  }

}

В листинге 4.1 определено три действия для отображения данных конкретного пользователя, удаления данных и сохранения новых данных. Обратите внимание, что действия Update и Delete не возвращают сопоставленного представления, а только перенаправляют запрос на вызов других действий с имеющимися представлениями. Только действие Select возвращает сопоставленное представление. Давайте создадим соответствующее представление для контроллера Admin, назвав его Select, и определим его следующим образом:

runat="server">

<%=Html.ActionLink("Вернуться в список", "Index")%>

<% MembershipUser user = (MembershipUser) ViewData.Model; %>

Пользователь <%= Html.Encode(user.UserName) %>

<% using (Html.BeginForm("Update", "Admin")) { %>

<%= Html.Hidden("userId", (Guid)user.ProviderUserKey) %>

  Данные

 

   

    <%=Html.TextBox("email", user.Email)%>

 

 

   

       user.IsApproved)%>подтвержден

 

 

 

    user.IsLockedOut)%>заблокирован

 

<%=Html.ActionLink("Удалить", "Delete", new {userId = (Guid)

     user.ProviderUserKey})%>

<% } %>

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

Рис. 4.7. Форма редактирования данных пользователя

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

 

Архитектура контроллеров

 

Мы рассмотрели простейший вариант создания контроллеров и их действий, а также сопоставленных с ними представлений. Но архитектура MVC Framework помимо рассмотренных механизмов содержит массу больших и малых механизмов, которые позволяют регулировать контроллеры, управлять вызовами кода, защищать контроллеры и действия от нежелательных вызовов, формировать базовые архитектуры для набора действий или даже группы контроллеров.

Далее мы более подробно рассмотрим архитектуру реализации контроллеров в MVC Framework. По мере рассмотрения, наш простейший код будет видоизменяться согласно новым техникам, о которых будет идти рассказ.

 

Порядок вызова архитектурных механизмов

ASP.NET MVC реализует следующий порядок выполнения внутренних механизмов:

1. При первом обращении к приложению в Global.asax производится регистрация маршрутов механизма маршрутизации ASP.NET в специальном объекте RouteTable.

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

3. Обработчик MvcHandler, с помощью данных полученного объекта RequestContext, инстанцирует объект фабрики контроллеров типа IControllerFactory. Если он не был задан специально, по умолчанию инстанцируется объект класса DafaultControllerFactory.

4. В фабрике контроллеров вызывается метод GetControllerInstance, который принимает параметр в виде типа необходимого контроллера. Этот метод возвращает инстанцированный объект необходимого контроллера.

5. У полученного объекта контроллера вызывается метод Execute, которому передается объект RequestContext.

6. С помощью ActionInvoker, свойства класса Controller типа ControllerActionInvoker, определяется необходимое действие и выполняется его код. Если ActionInvoker специально не сопоставлен, то используется свойство ActionInvoker по умолчанию. Для инициализации параметров действия используется механизм Model Binding, который может быть переопределен.

7. Определение действия может быть изменено в случае, когда пользователь определил для действий атрибуты типа ActionMethodSelectorAttribute. Для каждого такого атрибута вызывается метод IsValidForRequest, которому передается набор параметров типа ControllerContext и MethodInfo. Метод IsValidForRequest возвращает результат в виде bool, который определяет, подходит ли выбранное действие для исполнения в контексте запроса.

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

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

10. В случае когда Actioninvoker был специально переопределен, то после вызова действия результат действия передается методу CreateActionResult свойства Actioninvoker, который волен переопределить возвращаемый результат действия и вернуть результат типа ActionResult.

11. В случае, когда Actioninvoker не был переопределен, действие должно вернуть результат выполнения в виде ActionResult.

12. Для результата типа ActionResult вызывается метод ExecuteResult, который осуществляет отображение результата для контекста запроса. Создав свой вариант класса, наследующий ActionResult и реализующий ExecuteResult, можно переопределить действие и возвращать результат в нужном виде, например, формировать RSS-ленту.

Рассмотрим все шаги, относящиеся к контроллерам и действиям, более подробно. Для этого выделим основные темы:

□ фабрика контроллеров;

□ действия, фильтры и атрибуты;

□ механизм Model Binding.

 

Фабрика контроллеров

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

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

Фабрика контроллеров в ASP.NET реализует интерфейс IControllerFactory, который содержит всего два метода:

public interface IControllerFactory {

IController CreateController(RequestContext requestContext,

                             string controllerName);

void ReleaseController(IController controller);

}

Здесь CreateController должен создавать экземпляр контроллера, а ReleaseController — разрушать его или проводить какие-то другие действия после завершения работы контроллера. Вы можете создать свою фабрику контроллеров, реализовав этот интерфейс. Но более простым способом расширения фабрики является ее реализация с помощью наследования от класса DefaultFactoryController.

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

□ необходимо создать механизм, который позволит ограничивать создание определенных контроллеров на основе "черного списка";

□ решение на базе маршрутов таблицы маршрутизации не может нас удовлетворить, поскольку создание маршрутов производится через исполняемый код, который по каким-то причинам не может быть модифицирован для загрузки маршрутов из внешнего источника;

□ решение на базе маршрутов не может нас удовлетворить в связи с тем, что у нас много универсальных маршрутов, которые затрагивают сразу много контроллеров;

□ требуется возможность работать с "черным списком" в виде редактирования определенного файла для возможности разделения и предоставления прав к нему.

Согласно этим требованиям создадим фабрику контроллеров, которая позволит нам ограничивать выполнение контроллеров по их именам через XML-файл "черного списка". Вот пример такого файла:

 

 

Как можно увидеть, данным файлом мы хотели бы заблокировать выполнение контроллеров AccountController и AdminController.

Реализуем фабрику контроллеров, наследуя класс DefaultFactoryController:

public class ControllerFactory : DefaultControllerFactory {

  protected override IController

  GetControllerInstance(Type controllerType)

  {

    if (controllerType == null)

      return base.GetControllerInstance(controllerType);

    XmlDocument xdoc = new XmlDocument();

    string blacklistPath =

    HttpContext.Current.Server.MapPath("~/blacklist.xml");

    xdoc.Load(blacklistPath);

    XmlNodeList nodes = xdoc.GetElementsByTagName("blacklist");

    foreach (XmlNode node in nodes[0].ChildNodes)

    {

      if (node.Attributes["typeName"].Value == controllerType.Name)

        throw new HttpException(404, "Страница не найдена");

    }

    return base.GetControllerInstance(controllerType);

  }

}

Как вы можете видеть, единственным методом нашего класса является реализация перегруженного метода GetControllerInstance класса DefaultControllerFactory. В нем мы реализуем следующую последовательность действий:

1. Проверяем, не передается ли в фабрику контроллера типа контроллера в виде null, если это так, то завершаем выполнение вызовом базового метода для выполнения действия по умолчанию.

2. Загружаем наш XML-файл с "черным списком" и ищем в нем имя типа контроллера, который запрошен для создания.

3. Если в "черном списке" существует запись о блокировании данного контроллера, то возвращаем ответ на запрос в виде 404 ошибки HTTP "Страница не найдена".

4. Если контроллер отсутствует в "черном списке", то мы выполняем базовое действие по умолчанию для поиска и создания необходимого контроллера.

Для того чтобы наш код заработал, мы должны зарегистрировать нашу фабрику контроллеров. Функцию регистрации выполняет метод SetControllerFactory класса controllerBuilder. Добавим его вызов в файл Global.asax:

protected void Application_Start()

{

RegisterRoutes(RouteTable.Routes);

ControllerBuilder.Current.SetControllerFactory( new ControllerFactory());

}

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

Другим, пожалуй, самым распространенным вариантом использования фабрики контроллеров является реализация архитектурного паттерна Инверсия контроля (Inversion of Control), который в данном применении позволяет в приложении уменьшить зависимость и ослабить связи между контроллерами. Для реализации такого механизма используются сторонние библиотеки, вроде Unity Application Blocks от Microsoft, Spring.NET или Ninject.

 

Действия, фильтры и атрибуты

 

Переопределение свойства

Actionlnvoker

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

По умолчанию метод InvokeAction класса ActionInvoker вместе с самим вызовом действия реализует механизм обработки заданных через атрибуты фильтров для действия.

Всего InvokeAction формирует четыре группы фильтров:

□ ActionFilters — вызываются во время исполнения действия;

□ ResultFilters — вызываются после исполнения действия при обработке результата;

□ AuthorizationFilters — вызываются до исполнения действия, чтобы произвести проверку доступа;

□ ExceptionFilters — вызываются во время обработки возникшего исключения.

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

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

Листинг 4.2

public class FastControllerActionInvoker : ControllerActionInvoker

{

  public override bool InvokeAction(

    ControllerContext controllerContext, string actionName)

  {

    if (controllerContext == null)

      throw new ArgumentNullException("controllerContext");

    if (String.IsNullOrEmpty(actionName))

      throw new ArgumentException("actionName");

    ControllerDescriptor controllerDescriptor =

      GetControllerDescriptor(controllerContext);

    ActionDescriptor actionDescriptor = FindAction(controllerContext,

      controllerDescriptor, actionName);

    if (actionDescriptor != null)

    {

      IDictionary parameters =

        GetParameterValues(controllerContext, actionDescriptor);

      var actionResult = InvokeActionMethod(controllerContext,

        actionDescriptor, parameters);

      InvokeActionResult(controllerContext, actionResult);

      return true;

    }

    return false;

  }

}

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

Для использования нового класса FastControllerActionInvoker нужно присвоить его экземпляр свойству ActionInvoker необходимого контроллера. Например, используем наш новый класс для контроллера AccountController стандартного проекта MVC:

public AccountController()

  : this(null, null)

{

  ActionInvoker = new FastControllerActionInvoker();

}

 

Атрибуты ActionMethodSelectorAttribute

Мы рассмотрели работу механизма ControllerActionInvoker, который призван найти и выполнить необходимое действие контроллера. Одной из особенностей этого поиска является поиск установленных для действий атрибутов типа ActionMethodSelectorAttribute. Эти атрибуты имеют одно-единственное предназначение — определение того, может ли быть вызвано это действие в данном контексте запроса или нет. Рассмотрим определение класса ActionMethodSelectorAttribute:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false,

            Inherited = true)]

public abstract class ActionMethodSelectorAttribute : Attribute {

  public abstract bool IsValidForRequest(

       ControllerContext controllerContext,

       MethodInfo methodInfo);

}

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

Для удобства разработчиков MVC Framework уже реализует два атрибута, наследующих от ActionMethodSelectorAttribute:

□ AcceptVerbsAttribute — позволяет задать для действия допустимые типы HTTP-запросов из следующего списка: GET, POST, PUT, DELETE, HEAD. Запросы, отличные от указанных, будут игнорироваться;

□ NonActionAttribute — позволяет пометить метод, не являющийся действием. Такой метод невозможно будет вызвать никаким внешним запросом.

Используем эти атрибуты для нашего контроллера AdminController. Так как действия Index, Select и Delete могут быть вызваны только GET-запросами, пометим их соответствующим атрибутом, как показано в следующем фрагменте:

[AcceptVerbs(HttpVerbs.Get)]

public ActionResult Index()

[AcceptVerbs(HttpVerbs.Get)]

public ActionResult Delete(Guid? userId)

[AcceptVerbs(HttpVerbs.Get)]

public ActionResult Select(Guid? userId)

Наоборот, действие Update вызывается только POST-запросами, поэтому пометим их следующим образом:

[AcceptVerbs(HttpVerbs.Post)]

public ActionResult Update(Guid? userId, string email,

  bool isApproved, bool isLockedOut);

Теперь, если мы попытаемся вызвать действие Update из строки запроса браузера, набрав URL вроде такого , то получим ошибку с сообщением о том, что страница не была найдена. Без атрибута AcceptVerbs метод был бы вызван.

Для демонстрации действия атрибута NonActionAttribute проведем некоторые изменения в нашем коде. Обратите внимание на то, что в методах класса ActionController повторяется следующий код:

User.IsInRole("Administrators")

Вынесем его в отдельный метод UserlsAdmin:

[NonAction]

private bool UserIsAdmin()

{

  return User.IsInRole(''Administrators");

}

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

 

Атрибуты, производные от FilterAttribute

 

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

□ AuthorizeAttribute — позволяет указывать ограничения для имен пользователей и ролей, которые могут вызвать данное действие;

□ HandleErrorAttribute — позволяет определять действия для обработки необработанных исключений;

□ ValidateAntiForgeryTokenAttribute — проверяет контекст запроса на соответствие указанному маркеру безопасности при получении данных из форм;

□ ValidateInputAttribute — управляет механизмом проверки запроса на небезопасные значения. Позволяет отключить такого рода проверку для случаев, когда требуется получить данные разного вида, в том числе потенциально опасные.

 

AuthorizeAttribute

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

[NonAction]

private bool UserIsAdmin()

{

  return User.IsInRole("Administrators");

}

С использованием атрибута AuthorizeAttribute нужда в этом методе пропадает. Чтобы продемонстрировать действие AuthorizeAttribute, перепишем контроллер AdminController по-новому, так, как показано в листинге 4.3.

Листинг 4.3

public class AdminController : Controller

{

  [AcceptVerbs(HttpVerbs.Get)]

  [Authorize(Roles = "Administrators")]

  public ActionResult Index()

  {

    MembershipProvider mp = Membership.Provider;

    int userCount;

    var users = mp.GetAllUsers(0, Int32.MaxValue, out userCount);

    ViewData.Model = users;

    return View () ;

  }

  [AcceptVerbs(HttpVerbs.Post)]

  [Authorize(Roles = "Administrators")]

  public ActionResult Select(Guid? userId)

  {

    if (!userId.HasValue)

      throw new HttpException(404, "Пользователь не найден");

    MembershipProvider mp = Membership.Provider;

    MembershipUser user = mp.GetUser(userId, false);

    ViewData.Model = user;

    return View () ;

  }

  [AcceptVerbs(HttpVerbs.Post)]

  [Authorize(Roles = "Administrators")]

  public ActionResult Update(Guid? userId, string email,

                        bool isApproved, bool isLockedOut)

  {

    if (!userId.HasValue)

      throw new HttpException(404, "Пользователь не найден");

    MembershipProvider mp = Membership.Provider;

    MembershipUser user = mp.GetUser(userId, false);

    user.Email = email; user.IsApproved = isApproved;

    if (user.IsLockedOut && !isLockedOut)

      user.UnlockUser();

    mp.UpdateUser(user);

    return RedirectToAction("Index");

  }

  [AcceptVerbs(HttpVerbs.Get)]

  [Authorize(Roles = "Administrators")]

  public ActionResult Delete(Guid? userId)

  {

    if (!userId.HasValue)

      throw new HttpException(404, "Пользователь не найден");

    MembershipProvider mp = Membership.Provider;

    MembershipUser user = mp.GetUser(userId, false);

    mp.DeleteUser(user.UserName, true);

    return RedirectToAction("Index");

  }

}

Как вы можете заметить, мы избавились от рутинной операции проверки права доступа к действию контроллера путем задания для каждого действия атрибута [Authorize(Roles = "Administrators")]. Этот атрибут предписывает механизму MVC выполнить проверку права доступа пользователя при попытке вызвать действие нашего контроллера. Важным достоинством данного атрибута является его элегантность и унификация. Вместо того чтобы самим писать такой важный код, как код проверки прав доступа, мы оперируем механизмом метаданных в виде атрибута AuthorizeAttribute, помечая нужные нам участки кода. Так снижается возможность ошибки программиста, которая в случае работы с задачей безопасности может стоить очень дорого. Другим плюсом использования атрибута AuthorizeAttribute является заметное уменьшение кода, особенно в сложных вариантах, когда требуется предоставить доступ набору групп и пользователей.

Атрибут AutorizeAttibute принимает два параметра:

□ Roles — позволяет задавать перечисление ролей, которые имеют доступ к действию, через запятую;

□ Users — позволяет задавать перечисление пользователей, которые имеют доступ к действию, через запятую.

Так, например, следующий фрагмент кода определяет, что доступ к действию могут получить только члены группы Administrators и пользователи SuperUserl и SuperUser2:

[Authorize(Roles = "Administrators", Users = "SuperUserl, SuperUser2")]

 

HandleErrorAttibute

Атрибут HandleErrorAttribute предназначен для того, чтобы однообразно сформировать механизм обработки необработанных в контроллерах исключений. Атрибут HandleErrorAttribute применим как к классу контроллера, так и к любому действию. Кроме того, допустимо указывать атрибут несколько раз. По умолчанию, без параметров, механизм MVC с помощью атрибута HandleErrorAttribute при возникновении исключения произведет переадресацию на представление Error, которое должно находиться в папке -/Views/Shared. Однако это действие можно изменить под свои потребности. Для манипулирования порядком действия атрибута HandleErrorAttribute у него есть ряд параметров:

□ ExceptionType — указывает тип исключения, на возникновение которого должен реагировать данный атрибут;

□ View — указывает представление, которое нужно показать пользователю при срабатывании атрибута;

□ Master — указывает наименование Master View, которое будет использоваться при демонстрации пользователю представления;

□ Order — указывает на последовательный номер, в порядке которого атрибут будет исполняться.

Для демонстрации работы атрибута HandleErrorAttribute создадим представление AdminError, которое будет использоваться только тогда, когда произойдет ошибка при работе с контроллером AdminController. В листинге 4.3 представлен код представления.

<%@ Page Title="" Language="C#"

  MasterPageFile="~/Views/Shared/Site.Master"

  Inherits="System.Web.Mvc.ViewPage" %>

  runat="server">

  Ошибка! Произошло необработанное исключение.

  runat="server">

  <% var model = (HandleErrorlnfo)ViewData.Model; %>

 

Внимание

  <р>При работе сайта произошла исключительная

    ситуация в действии <%= model.ActionName %>

    контроллера <%= model.ControllerName %>.

    Ниже представлена дополнительная информация об исключении:

 

 

<%= model.Exception.Message %>

 

<%= model.Exception.StackTrace %>

Обратите внимание, для получения доступа к расширенной информации об исключении мы используем свойство Model объекта ViewData, предварительно приведя его к типу HandleErrorInfo. Механизм атрибута HandleErrorAttribute создает для представления элемент типа HandleErrorInfo, объект которого содержит следующие данные:

□ ActionName — имя действия, в котором произошло исключение;

□ ControllerName — имя контроллера, в котором произошло исключение;

□ Exception - объект типа Exception, в котором содержится вся информация об исключении, в том числе строка сообщения и трассировка стека.

Для того чтобы проверить наше представление, создадим для тестирования новое действие TestException в контроллере AdminController:

public ActionResult TestException()

{

  throw new Exception("Проверка исключения");

}

Пометим наш контроллер AdminController атрибутом HandleErrorAttribute в следующем виде, как это показано во фрагменте:

[HandleError(View = "AdminError")]

public class AdminController : Controller

Теперь, чтобы механизм атрибута HandleErrorAttribute заработал, необходимо включить механизм Custom Errors в файле web.config так, как показано во фрагменте:

После запуска приложения и попытки доступа к действию TestException мы получим сообщение об ошибке (рис. 4.8).

Кроме перечисленного, атрибут HandleErrorAttribute имеет ряд важных особенностей:

□ если механизм Custom Errors запрещен, или исключение уже было обработано другим атрибутом, то исполнение атрибута прекращается;

□ если исключение является HTTP-исключением с кодом, отличным от 500, исполнение атрибута прекращается. Другими словами, этот атрибут не обрабатывает HTTP-исключения с любыми кодами ошибок, кроме 500;

□ атрибут устанавливает Response.TrySkipIisCustomErrors = true для того, чтобы попытаться переопределить страницы веб-сервера, настроенные для отображения ошибок;

□ атрибут устанавливает код HTTP-ответа в значение 500, которое сообщает клиенту о возникшей при запросе ошибке.

Использование атрибута HandleErrorAttribte позволяет гибко настраивать реакцию вашего приложения на возникновение исключительных ситуаций.

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

 

ValidateAntiForgeryTokenAttribute

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

Вместе с возможностями защиты от несанкционированного доступа изменяются и способы проникновения и взлома защиты. Одним из таких способов проникнуть через защиту сайта является атака под названием Cross-site Request Forgery Attack (CSRF). Суть этой атаки заключается в следующем:

□ сайт должен иметь авторизацию по cookies, а пользователь, через которого планируется атака, должен иметь возможность автоматической авторизации;

□ на некой специальной странице в Интернете создается элемент формы со скрытыми параметрами и автоматическим отправлением этих данных по адресу сайта, на котором зарегистрирован пользователь;

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

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

Механизм ASP.NET MVC имеет поддержку такого рода защиты в виде атрибута ValidateAntiForgeryTokenAttribute и helper-метода Html.AntiForgeryToken. В нашем примере с контроллером AdminController есть слабое и уязвимое место — это действие Delete, которое выполняется с помощью GET-запросов и может быть использовано злоумышленником для того, чтобы преднамеренно удалять данные о пользователях. Правильно сформированные формы не должны разрешать любые модификации данных по GET-запросам. Иными словами, GET-запросы должны выполнять действия "только для чтения", а все остальные действия должны происходить через POST-запросы. Перепишем наш механизм действия Delete и добавим к нему и действию Update поддержку атрибута ValidateAntiForgeryTokenAttribute, для этого изменим разметку представления так, как показано в следующем фрагменте:

<% using (Html.BeginForm("Update", "Admin")) { %>

<%= Html.Hidden("userId", (Guid)user.ProviderUserKey) %>

<%= Html.AntiForgeryToken() %>

  Данные

 

   

    <%=Html.TextBox("email", user.Email)%>

 

 

   

             user.IsApproved)%>подтвержден

   

 

 

   

               user.IsLockeCOut)%>заблокирован

   

 

<% } %>

<% using(Html.BeginForm("Delete", "Admin")) { %>

<%= Html.Hidden("userId", (Guid)user.ProviderUserKey) %>

<%= Html.AntiForgeryToken() %>

<% } %>

Как вы можете заметить, к основной форме мы добавили поле Html.AntiForgeryToken(), а вместо ссылки для удаления создали еще одну форму, которая также защищена полем Html.AntiForgeryToken().

Теперь добавим поддержку защиты в наш контроллер AdminController для действий Update и Delete, как показано во фрагменте:

[AcceptVerbs(HttpVerbs.Post)]

[Authorize(Users = "Admin")]

[ValidateAntiForgeryToken]

public ActionResult Update(Guid? userId,

   string email, bool isApproved, bool isLockedOut)

[AcceptVerbs(HttpVerbs.Post)]

[Authorize(Users = "Admin")]

[ValidateAntiForgeryToken]

public ActionResult Delete(Guid? userId)

Обратите внимание, что мы ограничили доступ к нашему обновленному действию Delete только для POST-запросов. Для защиты от CSRF-атак мы добавили атрибут ValidateAntiForgeryTokenAttribute. Это все, что нам нужно сделать, чтобы защитить данные формы от несанкционированного доступа.

Для более высокого уровня безопасности атрибуту ValidateAntiForgeryTokenAttribute можно передать параметр Salt ("соль"), который представляет собой числовое значение. Параметр Salt — это дополнительный секретный ключ, который используется при формировании итогового проверяемого значения, для повышения уровня защиты.

Для более гибкой настройки helper-метод Html.AntiForgeryToken имеет ряд параметров:

□ salt — задает значение Salt, которое используется в атрибуте ValidateAntiForgeryTokenAttribute;

□ domain — задает значение параметра Domain для объекта HttpCookie, указывающее конкретный домен, с которым ассоциирован cookie;

□ path — задает значение параметра Path для объекта HttpCookie, указывающее виртуальный путь, с которым ассоциирован cookie.

После наших исправлений в контроллере AdminController не осталось действий, которые манипулируют с данными по GET-запросам, а действия с POST-запросами защищены механизмом атрибута ValidateAntiForgeryTokenAttribute.

 

ValidateInputAttribute

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

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

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

Изменим форму ввода, добавив следующий фрагмент кода:

 

  <%=Html.TextArea("comment", user.Comment) %>

Соответственно изменим определение действия Update контроллера AdminController:

[AcceptVerbs(HttpVerbs.Post)]

[Authorize(Users = "Admin")]

[ValidateAntiForgeryToken]

public ActionResult Update(Guid? userId, string email,

      string comment, bool isApproved, bool isLockedOut)

{

  if (!userId.HasValue)

    throw new HttpException(404, "Пользователь не найден");

  MembershipProvider mp = Membership.Provider;

  MembershipUser user = mp.GetUser(userId, false);

  user.Email = email;

  user.Comment = comment;

  user.IsApproved = isApproved;

  if (user.IsLockedOut && !isLockedOut) user.UnlockUser();

  mp.UpdateUser(user);

  return RedirectToAction("Index");

}

Для того чтобы пользователь видел сообщение, модифицируем частичное представление LogOnUserControl.ascx так, как показано в листинге 4.4.

Листинг 4.4. Представление LogOnUserControl.ascx

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

<%

  if (Request.IsAuthenticated) {

%>

    Welcome <%= Html.Encode(Page.User.Identity.Name) %>!

    [ <%= Html.ActionLink("Log Off", "LogOff", "Account") %> ]

   

<%= Membership.GetUser(Page.User.Identity.Name).Comment %>

  }

  else {

%>

    [ <%= Html.ActionLink("Log On", "LogOn", "Account") %> ]

<%

  }

%>

После запуска попробуем добавить в поле комментария для любого из пользователей значение добрый день! и сохранить изменения. Так как механизм проверки ввода включен по умолчанию, мы получим сообщение об ошибке (рис. 4.9).

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

[AcceptVerbs(HttpVerbs.Post)]

[Authorize(Users = "Admin")]

[ValidateAntiForgeryToken]

[ValidateInput(false)]

public ActionResult Update(Guid? userId, string email,

       string comment, bool isApproved, bool isLockedOut)

Атрибут ValidateInput с параметром false указывает механизму ASP.NET MVC на то, что проверка параметров запроса для данного действия не требуется. После указания этого значения атрибута мы сможем задавать для значения Comment данные с HTML-тегами. Сообщение будет выведено рядом с именем пользователя, как показано на рис. 4.10.

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

 

Атрибуты ActionFilterAttribute и OutputCacheAttribute

Существует еще один способ расширения механизма ASP.NET MVC. Через атрибут ActionFilterAttribute разработчик может задать выполнение кода по нескольким событиям, происходящим при работе механизма MVC. Вот следующие события, на которые можно реагировать с помощью определения атрибута ActionFilterAttribute: перед выполнением действия, после выполнения действия, перед исполнением результата и после исполнения результата.

Атрибут ActionFilterAttribute представляет собой абстрактный класс с четырьмя виртуальными методами: OnActionExecuting, OnActionExecuted, onResultExecuting, onResultExecuted. Реализация атрибута ложится на плечи разработчика. Обработка этих событий может быть полезна, например, для реализации механизма логов с целью вычисления времени исполнения действий. Другим вариантом использования может быть модификация HTTP-заголовков для ответа клиенту. Таким образом работает включенный в состав ASP.NET MVC атрибут OutputCacheAttribute.

OutputCacheAttribute предназначен для управления стандартными HTTP-заголовками, влияющими на кэширование веб-страниц браузером пользователя. Разработчикам классического ASP.NET этот механизм знаком по директиве @ OutputCache для ASPX-страниц и ASCX-компонентов. OutputCacheAttribute может быть определен как для класса контроллера, так и для отдельного действия. Для управления действием у OutputCacheAttribute есть ряд параметров:

□ Duration — значение времени в секундах, на которое производится кэширование;

□ Location — значение перечисления OutputCacheLocation, которое определяет местоположение для кэшированного содержимого: на стороне клиента или сервера. По умолчанию устанавливается значение OutputCacheLocation.Any, это означает, что содержимое может кэшироваться в любом месте;

□ Shared — булево значение, которое определяет, может ли использоваться один экземпляр кэшированного значения для многих страниц. Используется, когда действие возвращает результат в виде не целой страницы, а в виде частичного результата;

□ VaryByCustom — любой текст для управления кэшированием. Если этот текст равен browser, то кэширование будет производиться условно по имени браузера и его версии (major version). Если у VaryByCustom будет указана строка, то вы обязаны переопределить метод GetVaryByCustomString в файле Global.asax для осуществления условного кэширования;

□ varyByHeader — строка с разделенными через точку с запятой значениями HTTP-заголовков, по которым будет производиться условное кэширование;

□ varyByParam — задает условное кэширование, основанное на значениях строки запроса при GET или параметрах при POST;

□ varyByContentEncodings — указывает условие кэширования в зависимости от содержимого директивы HTTP-заголовка Accept-Encoding;

□ CacheProfile — используется для указания профиля кэширования заданного через web.config и секцию caching;

□ NoStore — принимает булево значение. Если значение равно true, то добавляет в директиву HTTP-заголовка Cache-Control параметр no-store;

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

В качестве примера рассмотрим следующий фрагмент кода, в котором устанавливается кэширование 1800 секунд (30 минут) любого результата контроллера AdminController вне зависимости от параметров запроса:

[OutputCache(Duration = 1800, VaryByParam = ="none")]

public class AdminController : Controller

В другом фрагменте, наоборот, с помощью атрибута OutputCacheAttribute отключается любое кэширование результатов контроллера AdminController:

[OutputCache(Location = OutputCacheLocation.None)]

public class AdminController : Controller

В своей работе атрибут OutputCacheAttribute переопределяет метод OnResultExecuting, который вызывается перед исполнением результата действия, когда результат типа ActionResult преобразуется в ответ на запрос пользователя, например в HTML-страницу. Вы можете создать свои варианты реализации ActionFilterAttribute, реализовав атрибут, переопределяющий ActionFilterAttribute. Для демонстрации подобной реализации создадим атрибут, который реализует сжатие результирующих страниц с помощью GZip-сжатия, которое поддерживается всеми современными браузерами.

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

Листинг 4.5

using System;

using System.IO.Compression;

using System.Web.Mvc;

namespace MVCBookProject {

  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,

    Inherited = true, AllowMultiple = false)]

  public class GZipCompress : ActionFilterAttribute {

    public override void OnActionExecuting(

           ActionExecutingContext filterContext)

    {

      string acceptEncoding = filterContext.HttpContext.Request

          .Headers["Accept-Encoding"];

      if (string.IsNullOrEmpty(acceptEncoding)) return;

      var response = filterContext.HttpContext.Response;

      if (acceptEncoding.ToLower().Contains("gzip"))

      {

        response.AppendHeader("Content-encoding", "gzip");

        response.Filter = new GZipStream(

           response.Filter, CompressionMode.Compress);

      }

    }

  }

}

Обратите внимание, что атрибут GZipCompressAttribute наследует от ActionFilterAttribute и реализует метод OnActionExecuting, в котором С Помощью класса GZipStream производится сжатие. Использование нашего атрибута тривиально, например, применяем его для контроллера AdminController так, как показано во фрагменте кода:

[GZipCompress]

public class AdminController : Controller

Реализация своего варианта ActionFilterAttribute — это очень мощное средство для расширения механизма ASP.NET MVC. Благодаря ему, мы реализовали прозрачное и простое средство для GZip-сжатия ответов для клиента. Другим стандартным средством, которое использует ActionFilterAttribute, является атрибут outputcacheAttribute, который позволяет управлять кэшированием результатов действий контроллера.

 

Стандартные реализации класса ActionResult

 

ActionResult — это базовый класс, экземпляр которого возвращает любое действие контроллера в ASP.NET MVC. В MVC существует несколько стандартных реализаций класса ActionResult: ViewResult, JsonResult, FileResult, RedirectResult, RedirectToRouteResult, ContentResult, EmptyResult. Их назначение и тип возвращаемых данных перечислены в табл. 4.1.

#i_031.jpg

Когда действие завершает свое выполнение, оно возвращает результат в виде базового класса ActionResult или его наследников. После этого механизм MVC вызывает у возвращенного результата стандартный метод ExecuteResult, который и формирует результат, получаемый клиентом.

 

ViewResult

ViewResult — это стандартный и самый используемый на практике результат, наследующий тип ActionResult, который возвращается действиями контроллеров. Назначение ViewResult — это определение представления, которое будет использовано механизмом MVC для представления состояния модели.

У ViewResult и базового класса ViewResultBase, от которого ViewResult унаследован, есть ряд параметров:

□ ViewData — хранилище данных модели, которые используются представлением для отображения результата работы действия;

□ TempData — аналогичное viewData хранилище данных модели, но с существенным отличием, которое позволяет данным храниться после перенаправления запроса на другое действие;

□ viewName — имя представления, которое должно отреагировать на изменение модели контроллером. Иными словами, этот параметр указывает механизму MVC, какое представление нужно использовать для отображения результата работы действия;

□ MasterName — имя master-представления, которое должно быть использовано для отображения результата работы действия;

□ view — экземпляр представления, которое должно быть использовано для отображения результата работы действия. Может быть использовано вместо параметра viewName для определения представления.

Обычно для возвращения результата типа viewResult из действия используется стандартный метод контроллера view, который принимает те же параметры, что и viewResult. Рассмотрим пример вызова метода view:

public ActionResult Select(Guid? userid)

{

  MembershipProvider mp = Membership.Provider;

  MembershipUser user = mp.GetUser(userId, false);

  return view("Select", "Site", user);

}

В приведенном фрагменте действие Select возвращает результат типа viewResult, который формируется стандартным методом контроллера view. В данном случае метод view принимает три параметра: имя представления Select, имя master-представления Site и модель данных user.

 

JsonResult

JsonResult — это стандартная возможность механизма MVC возвращать результат на запрос пользователя в виде JSON-данных. JSON — это формат данных, название которого расшифровывается как JavaScript Object Notation или объектная нотация JavaScript. Хотя в названии присутствует слово JavaScript, формат данных JSON языконезависимый и может быть использован при разработке на любом языке.

Примечание

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

*********************************

Класс JsonResult содержит несколько свойств, для более гибкой настройки возвращаемого результата:

□ ContentEncoding — устанавливает значение HTTP-параметра ContentEncoding, который определяет кодировку возвращаемого результата;

□ ContentType — устанавливает значение HTTP-параметра ContentType, если не указано, то по умолчанию устанавливается в application/json;

□ Data — любые данные, которые могут быть сериализованы в формат JSON с помощью класса JavaScriptSerializer.

Использовать JsonResult для возвращения результата в виде JSON-данных очень просто, для этого в контроллере существует стандартный метод Json, который принимает все параметры JsonResult и возвращает готовый результат. Рассмотрим пример действия для контроллера AdminController, которое возвращает JSON-данные по запросу с параметром имени пользователя:

public JsonResult SelectUserData(string userName)

{

  if (string.IsNullOrEmpty(userName))

    throw new HttpException(404, "Пользователь не найден");

  MembershipProvider mp = Membership.Provider;

  MembershipUser user = mp.GetUser(userName, false);

  UserData userData = new UserData()

  {

    Comment = user.Comment,

    Email = user.Email,

    IsApproved = user.IsApproved,

    IsLockedOut = user.IsLockedOut

  };

  return Json(userData, null, Encoding.UTF8);

}

В представленном фрагменте кода для передачи набора данных о пользователе в виде JSON-данных используется единственный метод Json, которому передается набор данных. Результатом, который получит пользователь в ответ, например, на такой запрос будет текст в следующем формате:

{"UserId":null,"Email":"[email protected]","Comment":"","IsApproved":true, "IsLockedOut":false,"CurrentMembershipUser":null}

 

FileResult

Очень часто в ответ на запрос пользователя требуется вернуть не HTML-страницу или данные в формате JSON, а какой-нибудь бинарный файл. FileResult — это механизм, который как раз и позволяет возвратить файл как результат работы действия контроллера.

У FileResult есть два важных свойства, которые требуется указывать при возвращении результата действия:

□ contentType — свойство, которое задается через конструктор класса FileResult и не может быть изменено напрямую. ContentType указывает MIME-тип содержимого передаваемого файла;

□ FileDownloadName — свойство, указывающее на файл, который требуется передать в ответ на запрос.

Рассмотрим использование FileResult на следующем примере. Пусть нам требуется на пользовательский запрос возвращать сопоставленный с пользователем рисунок. Реализуем эту возможность с помощью файловой системы. Для этого создадим в корне проекта папку Admin, в которой будем хранить рисунки пользователей в формате PNG с именем вида: GUID пользователя.рng. Действие GetUserImage контроллера AdminController, которое будет возвращать изображение с помощью FileResult, представлено в следующем фрагменте:

public ActionResult GetUserImage(string userName)

{

  if (string.IsNullOrEmpty(userName))

    throw new HttpException(404, "Пользователь не найден");

  MembershipProvider mp = Membership.Provider;

  MembershipUser user = mp.GetUser(userName, false);

  if (user == null)

    throw new HttpException(404, "Пользователь не найден");

  string userGuidString = ((Guid) user.ProviderUserKey).ToString();

  string fileName = userGuidString + ".png";

  return File(fileName, "image/png");

}

Обратите внимание, что для возвращения результата типа FileResult в примере используется стандартный метод контроллера File, который упрощает возврат результата в виде FileResult. Методу File передается два параметра: путь к возвращаемому файлу и его MIME-тип, который в данном случае для PNG-файла равен image/png.

В MVC Framework существует еще один класс для работы с файлами — класс FileContentResult, который наследует от FileResult и позволяет возвращать данные не на основании пути к файлу, а с помощью существующего потока данных, который может генерироваться в самом действии.

 

RedirectResult и RedirectToRouteResult

Важным свойством MVC Framework является возможность перенаправлять запрос на другие действия контроллеров либо другие URL-адреса. Для этого в MVC встроены механизмы RedirectResult и RedirectToRouteResult, которые наследуют от ActionResult и являются допустимыми результатами работы любого действия.

RedirectResult предназначен для того, чтобы возвратить результат пользователю в виде перенаправления на заданный адрес URL. У RedirectResult есть только одно свойство, которое инициализируется через конструктор, — Url, оно указывает строку адреса, на которую будет перенаправлен пользователь в ответ на запрос. Контроллеры MVC содержат стандартный метод Redirect, который формирует ответ в виде RedirectResult. В следующем фрагменте приведено действие, результатом которого является перенаправление пользователя на сайт :

public ActionResult GetAspNetSite()

{

  return Redirect(" http://www.asp.net/mvc/ ");

}

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

□ routeName — указывает наименование маршрута, на который нужно выполнить перенаправление запроса;

□ routeValues — указывает набор значений параметров маршрута типа RouteValueDictionary, с помощью которых производится поиск маршрута и выполняется перенаправление.

Для упрощения работы с RedirectToRouteResult механизм MVC реализует для контроллеров, наряду с методами RedirectToRoute, набор стандартных методов RedirectToAction, которые призваны облегчить формирование перенаправления вызова на другие действия или контроллеры. Например, следующий фрагмент кода перенаправляет вызов из текущего действия в действие Index текущего контроллера:

return RedirectToAction("Index");

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

return RedirectToAction("Index", "Account");

 

ContentResult

ContentResult — это весьма простая реализация ActionResult, которая предназначена для того, чтобы в ответ на запрос передавать любой пользовательский строковый набор данных. Для реализации логики у ContentResult есть три свойства:

□ ContentType — MIME-тип передаваемых в ответ на запрос данных;

□ ContentEncoding — кодировка данных;

□ Content — строка данных для передачи в ответ на запрос.

Благодаря ContentResult разработчик получает возможность генерировать ответы на запросы в любом виде, который можно представить в виде строки текста. Этот тип ActionResult может быть полезен при работе с механизмом RenderAction. RenderAction — это часть библиотеки MVCContrib, которая содержит расширения MVC Framework, не вошедшие в основной функционал. RenderAction позволяет представлению вывести в месте вызова результат выполнения действия. При таком применении результат типа ContentResult подходит более всего. Для упрощения контроллеры содержат специальный метод Content, который возвращает значение типа ContentResult.

 

EmptyResult

Последний из рассмотренных стандартных вариантов ActionResult — это EmptyResult. Этот механизм предназначен для того, чтобы в ответ на запрос не возвращать ничего. Переопределенный в EmptyResult метод ExecuteResult не содержит ни строчки кода.

 

Создание своей реализации ActionResult

Важной особенностью механизма ActionResult является то, что вы можете создать свой собственный вариант, который будет формировать результат в том виде, который вам нужен. Например, вы можете разработать класс, наследующий ActionResult, который будет возвращать клиентам результаты запроса в виде XML-разметки. Классическим примером создания своего варианта ActionResult является реализация класса, который на запрос пользователя создает ответ в виде RSS-ленты. Продемонстрируем реализацию такого класса, добавив к нашему контроллеру AdminController действие Rss, которое будет возвращать пользователю RSS-ленту со списком зарегистрированных пользователей.

Первым делом создадим класс RssResult, который наследует ActionResult, как показано в листинге 4.6.

Листинг 4.6. Класс RssResult

namespace MVCBookProject {

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

  using System.ServiceModel.Syndication;

  public class RssResult : ActionResult {

    public SyndicationFeed Feed { get; set; }

    public RssResult(SyndicationFeed feed)

    {

      Feed = feed;

    }

    public override void ExecuteResult(ControllerContext context)

    {

      context.HttpContext.Response.ContentType =

           "application/rss+xml";

      Rss20FeedFormatter formatter = new Rss20FeedFormatter(Feed);

      using (XmlWriter writer =

           XmlWriter.Create(context.HttpContext.Response.Output))

      {

        if (writer != null)

          formatter.WriteTo(writer);

      }

    }

  }

}

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

Использование класса RssResult ничем не отличается от применения других вариантов классов ActionResult. Добавим действие Rss в контроллер AdminControiier так, как показано во фрагменте:

[AcceptVerbs(HttpVerbs.Get)]

public RssResult Rss()

{

  MembershipProvider mp = Membership.Provider;

  int userCount;

  var users = mp.GetAllUsers(0, Int32.MaxValue, out userCount);

  List items = new List();

  if (userCount > 0)

  {

    string bodyTemplate = @"email: {0}, comment: {1},

          last activity: {2}, is locked: {3}, is approved: {4}";

    foreach (MembershipUser item in users)

    {

      string body = String.Format(bodyTemplate, item.Email,

            item. Comment, item. LastActivityDate,

            item.IsLockedOut, item.IsApproved);

      items.Add(new SyndicationItem(item.UserName, body, null));

    }

  }

  SyndicationFeed feed = new SyndicationFeed("Cписок пользователей",

       " http://localhost/rss ", Request.Url, items);

  return new RssResult(feed);

}

Обратите внимание, что это действие возвращает результат в виде экземпляра класса RssResult, которому передается сгенерированный RSS-поток. После того как мы реализовали RssResult и действие Rss, можно попытаться запросить результат этого действия через браузер, перейдя по относительной ссылке /Admin/Rss. В итоге вы должны получить результат в виде RSS-потока, похожий на тот, который изображен на рис. 4.11.

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

 

Model Binding

После того как механизм MVC Framework получил запрос от клиента с набором некоторых параметров, произвел определение необходимого контроллера и действия, возникает задача сопоставления параметров запроса параметрам выбранного действия контроллера. Задача решается просто, когда параметров немного. В этом случае сопоставление параметров происходит по их наименованию: параметрам метода действия с определенным именем присваиваются значения параметров запроса с теми же именами. В качестве примера рассмотрим действие Update контроллера AdminController. Во фрагменте приведено определение метода с параметрами:

public ActionResult Update(Guid? userid, string email,

      string comment, bool isApproved, bool isLockedOut)

Для этого действия подразумевается, что при его вызове будут переданы параметры с именами: userid, email, comment, isApproved, isLockedOut. Такие параметры передаются с запросом при отправлении формы с нашего представления Select. В следующем фрагменте рассмотрим основной HTML-код формы этого представления, который отображается в браузере пользователя:

...

 

      value="a4530eee-8634-4258-ac00-0ea63f7cc783" />

...

 

...

 

...

 

      name="isApproved" type="checkbox" value="true" />

...

 

      type="checkbox" value="true" />

...

 

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

Но что делать, когда форма содержит десятки вводимых полей? Неужели создавать десятки параметров у метода действия контроллера? Нет, MVC Framework содержит механизм, который позволяет избежать такого некрасивого шага, как многочисленные параметры метода. Такой механизм называется Model Binding (привязка модели). Чтобы продемонстрировать работу этого механизма, выполним ряд изменений в коде. Для начала определим комплексный тип, который будет содержать все необходимые данные, передаваемые в действие Update:

public class UserData

{

  public Guid? UserId { get; set; }

  public string Email { get; set; }

  public string Comment { get; set; }

  public bool IsApproved { get; set; }

  public bool IsLockedOut { get; set; }

}

Обратите внимание, что для определения параметров мы используем свойства. Механизм Model Binding требует, чтобы использовались свойства, но не простые поля. Соответственно данному типу изменим определение метода Update:

public ActionResult Update(UserData userData)

{

  if (!userData.UserId.HasValue)

    throw new HttpException(404, "Пользователь не найден");

  MembershipProvider mp = Membership.Provider;

  MembershipUser user = mp.GetUser(userData.UserId, false);

  user.Email = userData.Email;

  user.Comment = userData.Comment;

  user.IsApproved = userData.IsApproved;

  if (user.IsLockedOut && !userData.IsLockedOut)

    user.UnlockUser();

  mp.UpdateUser(user);

  return RedirectToAction("Index");

}

Теперь, чтобы механизм MVC Framework смог произвести сопоставление параметров с помощью встроенного механизма Model Binding, нам необходимо модифицировать код представления Select так, как показано в следующем фрагменте:

<% using (Html.BeginForm("Update", "Admin")) { %>

<%= Html.Hidden("userData.UserId", (Guid)user.ProviderUserKey)%>

<%= Html.AntiForgeryToken() %>

  Данные

 

   

    <%=Html.TextBox("userData.Email", user.Email)%>

 

 

   

    <%=Html.TextArea("userData.Comment", user.Comment)%>

 

 

   

      <%=Html.CheckBox("userData.IsApproved", user.IsApproved)%>

        подтвержден

   

 

 

   

      <%=Html.CheckBox("userData.IsLockedOut", user.IsLockedOut)%>

        заблокирован

   

 

<% } %>

Обратите внимание на то, что для всех полей формы мы использовали наименование вида userData.Свойство. Например, поле email стало полем с именем userData.Email. Такое именование позволяет классу DefaultModelBinder, механизму Model Binding по умолчанию, сопоставить множественные параметры формы комплексному типу UserData.

Примечание

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

*********************************************

Важной частью MVC Framework является возможность определять собственные механизмы Model Binding. Эта возможность предоставляет разработчику определять то, как параметры запроса или значения формы поступают к действию контроллера для обработки. Для демонстрации работы этого механизма добавим к нашей модели UserData еще одно свойство CurrentMembershipUser, которое будет автоматически инициализироваться при сопоставлении параметров:

public class UserData {

public MembershipUser CurrentMembershipUser { get; set; }

}

Теперь реализуем наш собственный механизм Model Binding, создав класс UserDataBinder, реализующий интерфейс IModelBinder. Этот интерфейс содержит всего один метод BindModel, с помощью которого и выполняется вся работа по сопоставлению параметров:

public class UserDataBinder : IModelBinder

{

  public object BindModel(ControllerContext controllerContext,

         ModelBindingContext bindingContext)

  {

    UserData userData = new UserData();

    userData.UserId = new

       Guid(controllerContext.HttpContext.Request["UserId"]);

    userData.Email = controllerContext.HttpContext.Request["Email"];

    userData.Comment =

       controllerContext.HttpContext.Request["Comment"];

    userData.IsApproved =

       controllerContext.HttpContext.Request["IsApproved"] != "false";

    userData.IsLockedOut =

       controllerContext.HttpContext.Request["IsLockedOut"] != "false";

    MembershipProvider mp = Membership.Provider;

    userData.CurrentMembershipUser =

       mp.GetUser(userData.UserId, false);

    return userData;

  }

}

Обратите внимание на то, что при реализации своего механизма Model Binding мы сами указываем, какие параметры запроса и каким образом соответствуют ожидаемому комплексному типу вызываемого действия. Для того чтобы использовать эту реализацию интерфейса IModelBinder, мы должны зарегистрировать ее в Global.asax с помощью следующей конструкции:

protected void Application_Start()

{

  ...

  ModelBinders.Binders.Add(typeof(UserData), new UserDataBinder());

}

Здесь мы добавляем в коллекцию еще один вариант Model Binder, который призван выполнять сопоставление типа UserData для всех действий любого контроллера в приложении.

Другим вариантом подключения нашего класса UserDataBinder может стать использование атрибута ModelBinderAttribute, в этом случае мы сможем явно указать, для какого конкретного параметра нужно использовать свой вариант Model Binder. ModelBinderAttribute позволяет более гибко управлять тем, когда и как применяются пользовательские элементы Model Binder, что не редко может быть полезным. При этом регистрировать в Global.asax UserDataBinder не потребуется. Используется атрибут ModelBinderAttribute следующим способом:

public ActionResult Update(

  [ModelBinder(typeof(UserDataBinder))]  UserData userData)

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

В общем случае использование стандартного варианта Model Binding в виде класса DefaultModelBinder достаточно для осуществления сопоставления параметров запроса и параметров метода действия. Однако существует еще одна полезная функция механизма Model Binding в MVC Framework. Эта функция реализуется атрибутом BindAttribute и позволяет еще более гибко настраивать процесс сопоставления параметров по умолчанию. Атрибут BindAttribute имеет следующие параметры:

□ Prefix — позволяет переопределить префикс при сопоставлении по умолчанию;

□ Include — позволяет определить список допустимых параметров, которые будут участвовать в сопоставлении, остальные параметры, не входящие в этот список, рассматриваться не будут;

□ Exclude — позволяет определить "черный" список параметров, которые не должны участвовать в процессе сопоставления. Такие параметры будут игнорироваться.

Использование параметра Prefix позволяет применять в представлении префикс для элементов формы, отличный от имени параметра метода действия. Например, вместо префикса userData в рассмотренном ранее примере, мы могли бы использовать сокращенный префикс ud, определив все элементы управления формы в подобном виде:

<%= Html.Hidden("ud.UserId", (Guid)user.ProviderUserKey)%>

Чтобы механизм Model Binder по умолчанию узнал про наш новый префикс, необходимо задать атрибут BindAttribute в требуемом месте при определении параметров метода действия:

public ActionResult Update([Bind(Prefix = "ud")] UserData userData)

Параметры Include и Exclude атрибута BindAttribute могут быть полезны в тех случаях, когда необходимо избежать автоматического сопоставления в комплексном типе для каких-то определенных свойств. Это может потребоваться для обеспечения безопасности или по каким-то другим соображениям. Например, чтобы запретить сопоставление свойства IsLockedOut, мы можем указать атрибут BindAttribute следующим образом:

public ActionResult Update(

  [Bind(Exclude = "IsLockedOut")] UserData userData)

Иногда требуется задать определенный список разрешенных для сопоставления параметров. Для этого используется параметр Include, которому можно задать список разрешенных для сопоставления свойств. В следующем примере мы разрешаем для сопоставления только два свойства: Userid и Email:

public ActionResult Update(

  [Bind(Include = "UserId, Email")] UserData userData)

Механизм сопоставления комплексных параметров форм с параметрами методов действий в MVC Framework значительно упрощается с помощью встроенного средства DefaultModelBinder. Этот механизм может гибко настраиваться с помощью атрибута BindAttribute, который позволяет задавать списки допустимых и недопустимых для сопоставления свойств и, вдобавок к этому, переопределять префикс, используемый в представлении. Если же разработчику недостаточно функционала механизма Model Binding по умолчанию, он волен переопределить этот механизм своей реализацией и использовать его как глобально во всем приложении, так и определяя его для конкретного параметра определенного действия.

 

Советы по использованию контроллеров

 

Атрибуты ActionNameSelectorAttribute и ActionNameAttribute

В механизме MVC существует множество полезных функций. Одна из них — это атрибут ActionNameAttribute, являющийся реализацией атрибута ActionNameSelectorAttribute — механизма MVC Framework, который позволяет ограничить выбор методов класса контроллера при определении нужного.

Атрибут ActionNameSelectorAttribute содержит всего один метод IsValidName со следующим определением:

public abstract bool IsValidName(ControllerContext controllerContext,

                      string actionName, MethodInfo methodInfo);

При поиске необходимого для выполнения действия механизм MVC Framework, кроме всего прочего, проверит все действия на наличие атрибута, реализующего ActionNameSelectorAttribute. В случае, когда такой атрибут найден, у него вызывается метод IsValidName для проверки на соответствие действия требуемому имени.

Единственная реализация ActionNameSelectorAttribute, существующая в MVC Framework, — это атрибут ActionNameAttribute, который призван предоставить возможность создания псевдонимов для методов действий. Рассмотрим следующий фрагмент кода:

[ActionName("UserList")]

public ActionResult GetUserListFromCache()

Здесь методу GetuserListFromCache, который представляет собой действие контроллера, присваивается укороченный псевдоним userList. После этого в ответ на запрос действия UserList контроллером может быть вызван метод GetuserListFromCache.

 

Наследование контроллеров

При работе с контроллерами в MVC Framework полезной практикой является механизм наследования контроллеров. Так как контроллеры представляют собой классы, преимущества наследования контроллеров схожи с преимуществами наследования классов в C#. Основным таким преимуществом является возможность создания базового набора правил для некоторого количества контроллеров. Для этого в MVC Framework можно определить контроллер, который будет называться базовым, и наследовать все остальные контроллеры от него.

Далее перечислены примеры возможных функций базовых контроллеров:

□ хранение и предоставление информации о текущем пользователе и его правах;

□ предоставление информации о базовых настройках приложения, которые могут поставляться, например, из web.config;

□ экземпляры хранилищ кода для работы с разнообразным функционалом: от пользователей до специфических данных приложения;

□ вспомогательные статические или другие методы утилитарного характера;

□ определение набора атрибутов, которые будут наследовать потомки базового контроллера.

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

Листинг 4.7. Базовый контроллер

[HandleError(View = "AdminError")]

[GZipCompress]

public class BaseController : Controller

{

  public NameValueCollection Settings

  {

    get

    {

      return ConfigurationManager.AppSettings;

    }

  }

  public string UserNotFoundMessage

  {

    get

    {

      return Settings["userNotFoundMessage"];

    }

  }

  public readonly MembershipProvider MP = Membership.Provider;

  public virtual ActionResult Index()

  {

    return View();

  }

  public static MembershipUser GetUser(string userName)

  {

    MembershipProvider mp = Membership.Provider;

    return mp.GetUser(userName, false);

  }

  public static MembershipUser GetUser(Guid userId)

  {

    MembershipProvider mp = Membership.Provider;

    return mp.GetUser(userId, false);

  }

}

Базовый контроллер из листинга 4.7 обладает следующими свойствами:

□ определяет для контроллера действие по умолчанию Index;

□ определяет атрибуты по умолчанию для обработки ошибок и сжатия результатов через GZip;

□ определяет обертку Settings над секцией настроек appSettings файла web.config, для более прозрачного доступа к настройкам;

□ прямо определяет UserNotFoundMessage, одну из настроек секции appSettings для быстрого к ней доступа;

□ определяет упрощенный доступ к объекту Membership.Provider;

□ определяет статический метод для более простого доступа к данным пользователей.

Чтобы наделить этими свойствами любой контроллер, необходимо наследовать его от базового. Модифицируем контроллер AdminController согласно новым правилам так, как показано во фрагменте:

public class AdminController : BaseController {

  [AcceptVerbs(HttpVerbs.Get)]

  [Authorize(Users = "Admin")]

  public override ActionResult Index()

  {

    int userCount;

    var users = MP.GetAllUsers(0, Int32.MaxValue, out userCount);

    ViewData.Model = users;

    return View();

  }

  [AcceptVerbs(HttpVerbs.Get)]

  [Authorize(Users = "Admin")]

  public ActionResult Select(Guid? userId)

  {

    if (!userId.HasValue)

      throw new HttpException(404, UserNotFoundMessage);

    return View("Select", "Site", GetUser(userId.Value));

  }

}

Обратите внимание, класс контроллера наследует BaseController, в связи с этим действие Index переопределяется с помощью ключевого слова override. Кроме того, в Index используется новое свойство mp, определенное в базовом контроллере. В другом действии, Select, используются два других функционала базового контроллера: свойство UserNotFoundMessage и статический метод GetUser.

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

 

Асинхронное выполнение при работе с контроллерами

 

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

Выходом из такой ситуации может служить асинхронное выполнение запросов. При асинхронном выполнении тяжелая задача поручается для выполнения отдельному специально созданному потоку, а основной поток ASP.NET освобождается для обработки других пользовательских запросов. Для реализации такого функционала разработчиками MVC Framework был создан специальный механизм AsyncController, который хоть и не вошел в MVC Framework, но доступен в особой библиотеке MVC Framework Futures, которая представлена файлом Microsoft.Web.Mvc.dll.

Примечание

Саму библиотеку и документацию к ней на английском языке можно скачать с официальной страницы ASP.NET MVC на сайте Codeplex по следующему адресу:

******************************

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

routes.MapAsyncRoute(

  "Default",

  "{controller}/{action}/{id}",

  new { controller = "Home", action = "Index", id = "" }

);

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

После изменения регистрации маршрутов следует изменить обработчики для *.mvc, определенные ранее в web.config, следующим образом в разделах httpHandlers и handlers:

  type="System.Web.Mvc.MvcHttpHandler,

  System.Web.Mvc, Version=1.0.0.0,

  Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>

  verb="*" path="*.mvc" type="System.Web.Mvc.MvcHttpHandler, System.Web.Mvc,

  Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>

Эти определения обработчиков необходимо заменить на следующие:

  type="Microsoft.Web.Mvc.MvcHttpAsyncHandler, Microsoft.Web.Mvc"/>

  verb="*" path="*.mvc" type="Microsoft.Web.Mvc.MvcHttpAsyncHandler,

  Microsoft.Web.Mvc"/>

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

public class SomeAsyncController : AsyncController

{

  ...

}

После этого конструктор по умолчанию, унаследованный от AsyncController, определит новый вариант ActionInvoker в виде экземпляра класса AsyncControllerActionInvoker для того, чтобы выполнять асинхронные действия. Для реализации асинхронных действий механизм AsyncController предлагает три паттерна, которые вы вольны использовать по отдельности либо смешивать их друг с другом: IAsyncResult, Event, Delegate.

 

Паттерн IAsyncResult

Паттерн IAsyncResult предполагает, что разработчик сам создаст асинхронную операцию. Согласно этому паттерну, вместо одного метода действия с именем XXX создаются два метода, BeginXXX и EndXXX, со следующим определением параметров:

public IAsyncResult BeginXXX(Guid? userId, AsyncCallback callback, object state);

public ActionResult EndXXX(IAsyncResult asyncResult);

Этот паттерн работает следующим образом:

1. MVC принимает запрос на выполнение действия xxx.

2. Механизмы MVC и AsyncController вызовут BeginXXX точно так же, как и любое другое синхронное действие.

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

4. После выполнения асинхронной операции будет вызван второй метод Endxxx, которому будет передан результат выполнения Beginxxx в виде экземпляра IAsyncResult.

5. Метод Beginxxx, используя данные, полученные от Beginxxx, формирует обычный для всех действий результат в виде ActionResult или его производных.

 

Паттерн Event

Согласно этому паттерну, метод действия разделяется на два метода: запуска и завершения:

public void XXX(Guid? userId);

public ActionResult XXXCompleted(...);

Метод xxx соответствует обычному синхронному действию и вызывается стандартно. Полный механизм работы данного паттерна состоит из следующих действий:

1. MVC принимает запрос на выполнение действия xxx.

2. Механизмы MVC и AsyncController вызовут XXX точно так же, как и любое другое синхронное действие.

3. Разработчик определяет внутри метода xxx асинхронную операцию, после запуска которой метод завершает свое выполнение.

4. Чтобы механизм асинхронных контроллеров мог определить, когда следует вызвать XXXCompleted, разработчик должен воспользоваться свойством AsyncManager.OutstandingOperations, которое является стандартным для класса контроллера AsyncController.

5. Разработчик инкрементирует AsyncManager.OutstandingOperations при создании каждого асинхронного процесса и заботится о том, чтобы по завершению процесса свойство AsyncManager.OutstandingOperations было декрементировано.

6. Механизм AcyncControllerActionInvoker следит за свойством AsyncManager.OutstandingOperations и вызывает XXXCompleted, когда это свойство обнулится, что означает завершение работы всех асинхронных процессов.

7. Параметры для XXXCompleted определяет разработчик. Для того чтобы AcyncControllerActionInvoker мог правильно выполнить XXXCompleted и передать необходимые параметры, разработчик заполняет специальную структуру AsyncManager.Parameters, которая является частью класса AsyncController. Структура AsyncManager.Parameters заполняется при работе метода xxx.

8. После завершения работы XXX механизм AcyncControllerActionInvoker вызывает метод XXXCompleted с набором параметров на базе AsyncManager.Parameters.

9. Используя переданные параметры, метод XXXCompleted возвращает стандартный результат в виде ActionResult или его производных.

Следующий фрагмент кода демонстрирует реализацию паттерна Event:

public void SelectUser(Guid? userId)

{

  As yncManager.Parameters["userData''] = new UserData();

  As yncManager.OutstandingOperations.Increment();

  ThreadPool.QueueUserWorkItem(o =>

  {

    Thread.Sleep(2000);

    AsyncManager.OutstandingOperations.Decrement();

  }, null);

}

public ActionResult SelectUserCompleted(UserData userData)

{

  ...

}

Чтобы упростить процесс, существует альтернатива прямому инкрементированию и декрементированию. С помощью стандартной части AsyncController метода AsyncManager.RegisterTask можно использовать связку из паттернов IAsyncResult и Event. Рассмотрим на примере:

public void XXX(Guid? userId)

{

  AsyncManager.RegisterTask(

    callback => BeginXXX(userId, callback, null),

    asyncResult =>

    {

      UserData userData = EndXXX(asyncResult);

      AsyncManager.Parameters["userData"] = userData;

    }

  );

}

public ActionResult XXXCompleted(UserData userData)

{

// ...

}

Во фрагменте кода используется паттерн Event, согласно которому создается два метода: xxx и xxxCompleted. Метод xxx регистрирует асинхронную задачу с помощью механизма AsyncManager.RegisterTask, который принимает два параметра: анонимные функции, осуществляющие логику паттерна IAsyncResult. Первая функция вызывает метод Beginxxx, который выполняет некую асинхронную операцию. Вторая анонимная функция выполняется тогда, когда асинхронная операция заканчивается и ей передаются результаты вызова Beginxxx. Задача второй анонимной функции состоит в том, чтобы, используя метод Endxxx паттерна IAsyncResult, получить значения параметров для метода XXXCompleted.

Преимущество данной связки паттернов состоит в том, что разработчику предоставляется возможность создавать несколько следующих подряд вызовов механизма AsyncManager.RegisterTask , которые выполняют разные асинхронные операции и формируют результаты разных параметров для метода XXXCompleted. Так как механизм AsyncManager.RegisterTask самостоятельно отслеживает инкремент и декремент свойства AsyncManager.OutstandingOperations, разработчику предлагается более безопасный и простой механизм создания нескольких асинхронных операций в одном запросе.

 

Паттерн Delegate

Этот паттерн похож на паттерн Event с одним существенным отличием: отсутствует метод xxxComplete. Вместо этого метод xxx сам занимается возвращением результата ActionResult на основании данных, полученных от асинхронных операций. Так выглядит определение метода действия при использовании паттерна Delegate :

public Func Foo(Guid? userId)

Для демонстрации реализации данного паттерна перепишем пример паттерна Event по-другому:

public Func XXX(Guid userId)

{

  UserData userData = new UserData();

  AsyncManager.RegisterTask(

    callback => BeginXXX(userId, callback, null),

    asyncResult =>

    {

      userData = EndXXX(asyncResult);

    }

  );

  return () => {

    ViewData["userData"] = userData;

    return View() ;

  };

}

Главное отличие реализации паттерна Delegate в приведенном фрагменте от паттерна Event состоит в том, что для возвращения результата выполнения действия используется не ActionResult, а Func, который представляет собой анонимную функцию, возвращающую результат в виде ActionResult. По сравнению с паттерном Event данный паттерн имеет упрощенный единый механизм, не разделенный на несколько методов, и максимально напоминает работу действий в синхронных контроллерах. При использовании этого паттерна у разработчика нет необходимости заботиться ни об обработке AsyncManager.OutstandingOperations, ни о заполнении AsyncManager.Parameters.

 

Дополнительные сведения об асинхронных контроллерах

Для асинхронных операций важно понятие времени исполнения запроса, поэтому в стандартный механизм класса AsyncController входит свойство AsyncManager.Timeout, которое позволяет задавать максимальное время ожидания результата выполнения асинхронного действия. В случае, когда действие выполняется дольше, чем определено в AsyncManager.Timeout механизмом, будет вызвано исключение TimeoutException. Для более гибкого управления максимальным периодом ожидания ответа от действия механизм асинхронных контроллеров предлагает два атрибута: AsyncTimeoutAttribute и NoAsyncTimeoutAttribute. Первый устанавливает время ожидания для конкретного действия или контроллера, второй указывает, что ожидания ответа не должно вызвать исключения и ожидать ответа от асинхронного действия требуется без ограничения по времени.

Одним из ограничений механизма асинхронных контроллеров является ограничение на именование методов действий. Вы не можете называть методы действий с префиксами Begin, End и суффиксом Completed. Это ограничение призвано предотвратить прямой вызов методов типа BeginXXX, EndXXX или XXXCompleted вместо вызова XXX. Тем не менее вы можете воспользоваться атрибутом ActionNameAttribute для того, чтобы задать необходимый псевдоним методу действия. Следующий фрагмент демонстрирует это:

[ActionName("BeginProcess")]

public ActionResult DoProcess();

Еще одним требованием механизма асинхронных контроллеров является исключение Default.aspx из корня проекта в случае, когда запросы к корневому ресурсу будут асинхронными. Default.aspx, включенный в стандартный проект MVC Framework, может работать только с синхронными запросами.

 

Неизвестные действия и метод HandleUnknownAction

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

protected virtual void HandleUnknownAction(string actionName)

{

  throw new HttpException(404,

    String.Format(CultureInfo.CurrentUICulture,

      MvcResources.Controller_UnknownAction,

      actionName, GetType().FullName));

}

Как можно понять из определения, если разработчик не переопределит действие метода HandleUnknownAction, то по умолчанию, когда MVC Framework не сможет найти действие для выполнения клиентского запроса, будет вызвано исключение, которое приведет к ответу пользователю в виде 404 HTTP-ошибки.

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