Настройка системы маршрутизации
144ASP.NET --- ASP.NET MVC 5 --- Настройка системы маршрутизации
Вы уже могли оценить, насколько гибкой и конфигурируемой является система маршрутизации, но если она не удовлетворяет существующим требованиям, ее поведение можно настроить. В этой статье будут продемонстрированы два способа такой настройки.
Создание специальной реализации RouteBase
Если способ, которым стандартные объекты Route сопоставляются с URL, не устраивает или нужно реализовать что-то необычное, можно создать альтернативный класс, производный от RouteBase. Это обеспечит контроль над тем, как происходят сопоставления с URL, каким образом извлекаются параметры и как генерируются URL. При наследовании класса от RouteBase понадобится реализовать два метода:
- GetRouteData
Это механизм, посредством которого работает сопоставление входящих URL. Инфраструктура вызывает этот метод для каждого элемента RouteTable.Routes по очереди, пока какой-то из них не возвратит отличное от null значение.
- GetVirtualPath
Это механизм, посредством которого работает генерация исходящих URL.
Для демонстрации такой настройки мы создадим класс RouteBase, который будет обрабатывать запросы к унаследованным URL. Предположим, что какое-то существующее приложение переехало на MVC Framework, но некоторые пользователи в прошлом создали закладки на URL из предыдущей версии или жестко закодировали их в сценариях. Мы по-прежнему хотим поддерживать эти старые URL. Данную ситуацию можно было бы решить с помощью обычной системы маршрутизации, но сейчас нам интересно применить подход с использованием RouteBase.
Первым делом понадобится создать контроллер, который будет получать запросы к унаследованным URL. Этот контроллер называется LegacyController.cs, а его код показан в примере ниже:
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers
{
public class LegacyController : Controller
{
public ActionResult GetLegacyURL(string legacyURL)
{
return View((object)legacyURL);
}
}
}
Метод действия GetLegacyURL() в этом простом контроллере получает параметр и передает его в качестве модели представления конкретному представлению. При реализации данного контроллера в реальном проекте этот метод использовался бы для извлечения запрошенных файлов, но в рассматриваемом примере мы просто отображаем URL в представлении.
Обратите внимание в примере выше на приведение параметра к object в вызове метода View(). Одна из перегруженных версий метода View() принимает параметр типа string, указывающий имя представления для визуализации, и без такого приведения компилятор C# выберет именно эту перегруженную версию. Во избежание этого мы выполняем приведение к object, чтобы однозначно вызывалась перегруженная версия, которая принимает модель представления и использует стандартное представление. Это также можно было бы решить с помощью перегруженной версии, принимающей как имя представления, так и модель представления, но предпочтительнее не делать явных ассоциаций между методами действий и представлениями.
Создайте в папке Views/Legacy файл представления по имени GetLegacyURL.cshtml с содержимым, показанным в примере ниже:
@model string
@{
ViewBag.Title = "Представление GetLegacyURL";
Layout = null;
}
<h2>Представление GetLegacyURL</h2>
Значение URL из запроса: @Model
Мы хотим продемонстрировать специальное поведение маршрутизации, поэтому не собираемся тратить время на создание сложных действий и представлений, а просто отображаем значение модели. Сейчас мы добрались до точки, где можно создать производный от RouteBase класс.
Маршрутизация входящих URL
В папку Infrastructure (где размещаются классы поддержки, не принадлежащие чему-либо еще) был добавлен файл класса по имени LegacyRoute.cs, содержимое которого приведено в примере ниже:
using System;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes.Infrastructure
{
public class LegacyRoute : RouteBase
{
private string[] urls;
public LegacyRoute(params string[] targetUrls)
{
urls = targetUrls;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
string requestedURL =
httpContext.Request.AppRelativeCurrentExecutionFilePath;
if (urls.Contains(requestedURL, StringComparer.OrdinalIgnoreCase))
{
result = new RouteData(this, new MvcRouteHandler());
result.Values.Add("controller", "Legacy");
result.Values.Add("action", "GetLegacyURL");
result.Values.Add("legacyURL", requestedURL);
}
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext,
RouteValueDictionary values)
{
return null;
}
}
}
Конструктор этого класса принимает строковый массив, представляющий индивидуальные URL, которые этот класс маршрутизации будет поддерживать. Мы укажем их позже при регистрации маршрута. В примере следует особо отметить метод GetRouteData(), в котором производится обращение к системе маршрутизации для определения, может ли экземпляр класса LegacyRoute соответствовать входящему URL.
Если экземпляр класса LegacyRoute не соответствует запросу, то просто возвращается значение null, и система маршрутизации переходит на следующий маршрут в списке, после чего процесс повторяется. Если же экземпляр класса может соответствовать запросу, возвращается экземпляр класса RouteData, содержащий значения для переменных controller и action, а также всю остальную информацию, которая должна быть передана методу действия.
При создании объекта RouteData необходимо передать обработчик, который будет иметь дело с генерируемыми значениями. Мы будем использовать стандартный класс MvcRouteHandler, привносящий смысл значениям controller и action:
result = new RouteData(this, new MvcRouteHandler());
В подавляющем большинстве приложений MVC этот класс необходим, т.к. он подключает систему маршрутизации к модели "контроллер/действие" приложения MVC. Однако, можно реализовать замену для MvcRouteHandler.
В этой специальной реализации RouteBase мы будем сопоставлять любой запрос к URL, переданный конструктору. При получении такого URL мы добавляем к объекту RouteValueDictionary жестко закодированные значения для контроллера и метода действия. Мы также передаем запрошенный URL в свойстве legacyURL. Обратите внимание, что имя этого свойства соответствует имени параметра в методе действия контроллера Legacy, гарантируя, что генерируемое значение будет передано методу действия через этот параметр.
Последний шаг заключается в регистрации нового маршрута, который использует производный от RouteBase класс. Регистрация показана в примере ниже, где приведено содержимое файла RouteConfig.cs:
using System.Web.Mvc;
using System.Web.Routing;
using UrlsAndRoutes.Infrastructure;
namespace UrlsAndRoutes
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapMvcAttributeRoutes();
routes.Add(new LegacyRoute(
"~/articles/About_ASPNET_MVC",
"~/old/NET_Framework_4"
));
routes.MapRoute("MyRoute", "{controller}/{action}");
routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" });
}
}
}
Мы создаем новый экземпляр класса LegacyRoute и передаем URL, которые он должен маршрутизировать. Затем мы добавляем объект в коллекцию RouteCollection, используя метод Add(). Если теперь запустить приложение и запросить один из унаследованных URL, запрос маршрутизируется классом LegacyRoute и направляется контроллеру Legacy:
Генерация исходящих URL
Для поддержки генерации исходящих URL мы должны реализовать метод GetVirtualPath() в классе LegacyRoute. Если этот метод не может сгенерировать конкретный URL, мы уведомляем об этом систему маршрутизации, возвращая null. В противном случае мы возвращаем экземпляр класса VirtualPathData. Реализация этого метода показана в примере ниже:
using System;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes.Infrastructure
{
public class LegacyRoute : RouteBase
{
// ...
public override VirtualPathData GetVirtualPath(RequestContext requestContext,
RouteValueDictionary values)
{
VirtualPathData result = null;
if (values.ContainsKey("legacyURL") &&
urls.Contains((string)values["legacyURL"], StringComparer.OrdinalIgnoreCase))
{
result = new VirtualPathData(this,
new UrlHelper(requestContext)
.Content((string)values["legacyURL"]).Substring(1));
}
return result;
}
}
}
В предыдущих статьях переменные сегментов и другие детали передавались с использованием анонимных типов. Однако "за кулисами" система маршрутизации преобразует их в объекты RouteValueDictionary, так что они могут быть обработаны реализациями RouteBase. В примере ниже показано дополнение файла представления ActionName.cshtml, которое генерирует исходящий URL с применением специального класса маршрута:
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Представление ActionName</title>
</head>
<body>
<div>Контроллер: <strong>@ViewBag.Controller</strong></div>
<div>Метод действия: <strong>@ViewBag.Action</strong></div>
<div>
@Html.ActionLink("Ссылка", "GetLegacyURL",
new { legacyURL = "~/old/NET_Framework_4" })
</div>
</body>
</html>
Как и можно было ожидать, при визуализации этого представления вспомогательный метод ActionLink() генерирует следующую HTML-разметку:
<a href="/old/NET_Framework_4">Ссылка</a>
Созданный объект анонимного типа со свойством legacyURL преобразуется в экземпляр класса RouteValueDictionary, который содержит ключ с таким именем. В данном примере мы решили иметь дело с запросом исходящего URL, если существует ключ по имени legacyURL со значением одного из URL, переданных конструктору. Можно было бы также дополнительно проверить значения controller и action, но для простого примера этого вполне достаточно.
Если совпадение получено, мы создаем новый экземпляр VirtualPathData, передавая ссылку на текущий объект и исходящий URL. С помощью метода Content() класса UrlHelper выполняется преобразование URL, относящегося к приложению, в URL-который может быть передан браузерам. Система маршрутизации добавляет в начало URL дополнительный символ /, так что мы должны позаботиться об удалении ведущего символа из сгенерированного URL.
Создание специального обработчика маршрутов
В наших маршрутах мы полагаемся на обработчик MvcRouteHandler, поскольку он подключает систему маршрутизации к инфраструктуре MVC Framework. Несмотря на это, система маршрутизации позволяет определить собственный обработчик маршрутов за счет реализации интерфейса IRouteHandler.
В примере ниже показано содержимое файла CustomRouteHandler.cs, добавленного в папку Infrastructure примера проекта:
using System.Web;
using System.Web.Routing;
namespace UrlsAndRoutes.Infrastructure
{
public class CustomRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new CustomHttpHandler();
}
}
public class CustomHttpHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
context.Response.Write("Привет");
}
}
}
Назначение интерфейса IRouteHandler заключается в том, чтобы предоставить средства для генерирования реализаций интерфейса IHttpHandler, который отвечает за обработку запросов. В реализации MVC этих интерфейсов контроллеры обнаруживаются, методы действий вызываются, представления визуализируются, а результаты записываются в ответ. Наша реализация намного проще: она лишь выводит слово "Привет" для клиента (не HTML-документ, содержащий это слово, а только сам текст).
Интерфейс IHttpHandler определен платформой ASP.NET и является частью стандартной системы обработки запросов. Написание приложений MVC Framework не требует понимания способа обработки запросов платформой ASP.NET, но следует знать о наличии возможностей для настройки и расширению этой системы, которые могут оказаться полезными при построении сложных приложений.
Специальный обработчик можно зарегистрировать при определении маршрута, как показано в примере ниже:
using System.Web.Mvc;
using System.Web.Routing;
using UrlsAndRoutes.Infrastructure;
namespace UrlsAndRoutes
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapMvcAttributeRoutes();
routes.Add(new Route("SayHello", new CustomRouteHandler()));
routes.Add(new LegacyRoute(
"~/articles/About_ASPNET_MVC",
"~/old/NET_Framework_4"
));
routes.MapRoute("MyRoute", "{controller}/{action}");
routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" });
}
}
}
Когда поступает запрос URL вида /SayHello, для его обработки используется специальный обработчик маршрутов. Результат показан на рисунке ниже:
Реализация специальной обработки маршрутов означает взятие на себя ответственности за обычные функции, такие как распознавание контроллеров и действий. Однако она также предоставляет невероятную свободу: вы можете взаимодействовать с одними частями инфраструктуры MVC Framework и игнорировать другие, или даже реализовать совершенно новый архитектурный шаблон.