Активаторы действий

86

После того как фабрика контроллеров (которую мы обсуждали в предыдущей статье) создала экземпляр класса, инфраструктуре нужен способ вызова действия на этом экземпляре. Если контроллер является производным от класса Controller, то ответственность за это несет активатор действий, который рассматривается в этой статье.

В случае создания контроллера за счет непосредственной реализации интерфейса IController ответственность за вызов действий возлагается на вас. Активаторы действий являются частью функциональности, предлагаемой классом Controller.

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

Активатор действий реализует интерфейс IActionInvoker, который показан в примере ниже:

namespace System.Web.Mvc
{
    public interface IActionInvoker
    {
        bool InvokeAction(ControllerContext controllerContext, string actionName);
    }
}

Этот интерфейс имеет единственный метод InvokeAction(). В качестве параметров InvokeAction() получает объект ControllerContext (описанный ранее) и строку, содержащую имя вызываемого действия. Возвращаемым типом является bool: значение true указывает на то, что действие найдено и вызвано, a false - что контроллер не содержит запрошенного действия.

Обратите внимание, что в этом описании не используется слово метод. Ассоциация между действиями и методами совершенно не обязательна. Хотя этот подход применяется встроенным активатором действий, вы можете обрабатывать действия любым желаемым способом. В примере ниже приведена реализация интерфейса IActionInvoker, в которой используется другой подход. Она определена в файле класса по имени CustomActionInvoker.cs внутри папки Infrastructure:

using System.Web.Mvc;

namespace ControllerExtensibility.Infrastructure
{
    public class CustomActionInvoker : IActionInvoker
    {
        public bool InvokeAction(ControllerContext controllerContext,
                string actionName)
        {
            if (actionName == "Index")
            {
                controllerContext.HttpContext.
                    Response.Write("Это вывод данных из метода действия Index");
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}

Этот активатор действий не заботит то, какие методы определены в классе контроллера. В сущности, он имеет дело с самими действиями. Когда поступает запрос действия Index, активатор записывает сообщение прямо в Response. Если запрос касается любого другого действия, активатор возвращает значение false, которое приводит к отображению для пользователя ошибки 404 - Not found (не найдено).

Активатор действий, связанный с контроллером, может быть получен с помощью свойства Controller.ActionInvoker. Это значит, что различные контроллеры в одном и том же приложении могут использовать разные активаторы действий. Чтобы продемонстрировать сказанное, мы добавили в пример проекта новый контроллер по имени ActionInvoker, определение которого приведено в примере ниже:

using System.Web.Mvc;
using ControllerExtensibility.Infrastructure;

namespace ControllerExtensibility.Controllers
{
    public class ActionInvokerController : Controller
    {
        public ActionInvokerController()
        {
            this.ActionInvoker = new CustomActionInvoker();
        }
    }
}

В этом контроллере нет ни одного метода действия. Для обработки запросов применяется активатор действий. Чтобы посмотреть, как это работает запустите приложение и перейдите на URL вида /ActionInvoker/Index. Специальный активатор действий сгенерирует ответ, показанный на рисунке ниже. Если перейти на URL, указывающий на другое действие в том же контроллере, вы увидите страницу ошибки 404.

Результат работы специального активатора действий

Я не предполагаю, что вы займетесь реализацией собственного активатора действий. Более того, я не рекомендую использовать продемонстрированный выше подход. Почему? Во-первых, как вскоре будет показано, встроенная поддержка обладает рядом очень полезных средств. Во-вторых, с приведенным примером связано несколько проблем: недостаточная расширяемость, плохое разделение ответственности и отсутствие поддержки представлений любого вида.

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

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

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

Первые два критерия достаточно просты. Что касается третьего критерия, то исключение метода, который присутствует в классе Controller и его базовых классах, означает исключение таких методов, как ToString() и GetHashCode(), поскольку они реализуют интерфейс IController. Это имеет смысл, т.к. внутреннее функционирование контроллеров не должно демонстрироваться внешнему миру.

Последний критерий означает исключение конструкторов, свойств и средств доступа к событиям. В действительности члены класса, которые имеют флаг IsSpecialName, определенный в классе System.Reflection.MethodBase, не будут использоваться для обработки действия.

Методы, которые имеют обобщенные параметры (вроде MyMethod<T>()), удовлетворяют всем критериям, но MVC Framework сгенерирует исключение при попытке вызвать такой метод для обработки запроса.

По умолчанию ControllerActionInvoker ищет метод, имеющий то же самое имя, что и запрошенное действие. Так, например, если значением action, которое порождает система маршрутизации, является Index, то ControllerActionInvoker вызовет метод по имени Index(), удовлетворяющий критериям действия. Если такой метод обнаруживается, он вызывается для обработки запроса. Именно такое поведение требуется большую часть времени, однако, как и можно было ожидать, инфраструктура MVC Framework предлагает некоторые возможности по настройке данного процесса.

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

Обычно имя метода действия определяет действие, которое метод представляет. Метод действия Index() обслуживает запросы к действию Index. Это поведение можно переопределить с использованием атрибута ActionName, который в примере ниже применяется к контроллеру Customer:

using System.Web.Mvc;
using ControllerExtensibility.Models;

namespace ControllerExtensibility.Controllers
{
    public class CustomerController : Controller
    {
        // ...

        [ActionName("Enumerate")]
        public ViewResult List()
        {
            return View("Result", new Result
            {
                ControllerName = "Customer",
                ActionName = "List"
            });
        }
    }
}

В этом примере к методу List() применяется атрибут, параметру которого передается значение Enumerate. Когда активатор действий получает запрос действия Enumerate, он будет использовать метод List() для его обслуживания. Запустив приложение и перейдя на URL вида /Customer/Enumerate, можно увидеть конечный эффект. Как показано на рисунке ниже, браузер получает результаты из метода List():

Эффект от применения атрибута ActionName

Применение этого атрибута приводит к переопределению имени действия. Это означает, что URL, напрямую нацеленные на метод List(), больше работать не будут, что и можно видеть на рисунке:

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

Существуют две основных причины, по которым может потребоваться такое переопределение имени метода:

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

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

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

Вы уже видели пример селектора метода действия при построении приложения GameStore, когда действие ограничивалось с использованием атрибута HttpPost. Мы имели два метода по имени Checkout() в контроллере Cart и применяли атрибут HttpPost для указания, что один из них должен использоваться только для HTTP-запросов POST:

using System.Linq;
using System.Web.Mvc;
using GameStore.Domain.Entities;
using GameStore.Domain.Abstract;
using GameStore.WebUI.Models;

namespace GameStore.WebUI.Controllers
{
    public class CartController : Controller
    {
        private IGameRepository repository;
        private IOrderProcessor orderProcessor;

        public CartController(IGameRepository repo, IOrderProcessor processor)
        {
            repository = repo;
            orderProcessor = processor;
        }
        
        public ViewResult Checkout() {
            // ...
        }

        [HttpPost]
        public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails)
        {
            // ...
        }

        // ...
    }
}

Активатор действий использует селектор метода действия для разрешения неоднозначности при выборе действия. В примере выше есть два кандидата на действие Checkout. Активатор отдает предпочтение действиям, которые имеют селекторы. В данном случае оценивается селектор HttpPost, чтобы увидеть, может ли запрос быть обработан. Если может, этот метод и будет использоваться. В противном случае будет применяться другой метод, который не имеет атрибута.

Существуют встроенные атрибуты, которые работают как селекторы для различных типов HTTP-запросов: HttpPost для запросов POST, HttpGet для запросов GET, HttpPut для запросов PUT и т.д. Еще одним встроенным атрибутом является NonAction, который указывает активатору действий, что метод, который иначе бы трактовался как допустимый метод действия, не должен использоваться. Применение атрибута NonAction демонстрируется в примере ниже, где определен новый метод действия в контроллере Customer:

using System.Web.Mvc;
using ControllerExtensibility.Models;

namespace ControllerExtensibility.Controllers
{
    public class CustomerController : Controller
    {
        // ...

        [NonAction]
        public ActionResult MyAction()
        {
            return View();
        }
    }
}

Метод MyAction() в этом примере не будет рассматриваться как метод действия, несмотря на то, что он удовлетворяет всем критериям, которые ожидает активатор. Это полезно, когда требуется скрыть внутренние детали классов контроллеров. Разумеется, обычно такие методы должны просто помечаться как private, что предотвратит их вызов в качестве действий; однако атрибут NonAction позволяет сделать это, если по ряду причин методы должны быть объявлены как public.

Запросы URL, которые нацелены на методы, помеченные с помощью NonAction, будут генерировать ошибки 404 - Not Found (не найдено), как показано на рисунке:

Результат запрашивания URL, нацеленного на метод, который помечен как NonAction

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

Селекторы методов действий являются производными от класса ActionMethodSelectorAttribute, который показан в примере ниже:

namespace System.Web.Mvc
{
    using System.Reflection;
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public abstract class ActionMethodSelectorAttribute : Attribute
    {
        public abstract bool IsValidForRequest(ControllerContext controllerContext,
            MethodInfo methodlnfo);
    }
}

Класс ActionMethodSelectorAttribute является абстрактным и определяет один абстрактный метод IsValidForRequest(). В качестве параметров этот метод принимает объект ControllerContext, позволяющий инспектировать запрос, и объект MethodInfo, предназначенный для получения информации о методе, к которому был применен селектор.

Метод IsValidForRequest() возвращает true, если метод действия имеет возможность обработать запрос, и false - в противном случае. В файле класса LocalAttribute.cs, добавленном в папку Infrastructure, создан простой специальный селектор метода действия, код которого показан в примере:

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

namespace ControllerExtensibility.Infrastructure
{
    public class LocalAttribute : ActionMethodSelectorAttribute
    {
        public override bool IsValidForRequest(ControllerContext controllerContext,
                MethodInfo methodInfo)
        {
            return controllerContext.HttpContext.Request.IsLocal;
        }
    }
}

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

using System.Web.Mvc;
using ControllerExtensibility.Infrastructure;
using ControllerExtensibility.Models;

namespace ControllerExtensibility.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View("Result", new Result
            {
                ControllerName = "Home",
                ActionName = "Index"
            });
        }

        [ActionName("Index")]
        public ActionResult LocalIndex()
        {
            return View("Result", new Result
            {
                ControllerName = "Home",
                ActionName = "LocalIndex"
            });
        }
    }
}

С помощью атрибута ActionName создана ситуация, при которой присутствуют два метода действия Index(). В этот момент активатор действий никак не может выяснить, какой из методов должен использоваться, когда поступает запрос URL вида /Home/Index, и сгенерирует страницу ошибки, показанную на рисунке ниже:

Страница ошибки, отображаемая в случае наличия неоднозначных имен методов

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

using System.Web.Mvc;
using ControllerExtensibility.Infrastructure;
using ControllerExtensibility.Models;

namespace ControllerExtensibility.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View("Result", new Result
            {
                ControllerName = "Home",
                ActionName = "Index"
            });
        }

        [Local]
        [ActionName("Index")]
        public ActionResult LocalIndex()
        {
            return View("Result", new Result
            {
                ControllerName = "Home",
                ActionName = "LocalIndex"
            });
        }
    }
}

Перезапустив приложение и перейдя на корневой URL в браузере, выполняющемся на локальной машине, можно увидеть, что инфраструктура MVC Framework учитывает атрибут селектора методов для разрешения неоднозначности между методами в классе контроллера:

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

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

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

  2. Затем активатор отбрасывает методы, которые имеют атрибут селектора метода действия, возвращающий false для текущего запроса.

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

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

Обработка неизвестных действий

Если активатору действий не удается найти метод действия для вызова, он возвращает false из своего метода InvokeAction(). Когда это происходит, класс Controller вызывает свой метод HandleUnknownAction(). По умолчанию данный метод возвращает клиенту ответ 404- Not Found (не найдено). Для большинства приложений такое поведение контроллера вполне приемлемо, но вы можете при желании переопределить этот метод в своем классе контроллера и сделать что-нибудь специфическое.

В примере ниже приведен пример переопределения метода HandleUnknownAction() в контроллере Home:

using System.Web.Mvc;
using ControllerExtensibility.Infrastructure;
using ControllerExtensibility.Models;

namespace ControllerExtensibility.Controllers
{
    public class HomeController : Controller
    {
        // ...

        protected override void HandleUnknownAction(string actionName)
        {
            Response.Write(string.Format("Вы запросили метод действия <b>{0}</b>", actionName));
        }
    }
}

Запустив приложение и перейдя на URL, нацеленный на несуществующий метод Действия, вы увидите результат, показанный на рисунке:

Обработка запросов к несуществующим методам действий
Пройди тесты
Лучший чат для C# программистов