Главная страница   /   9.3. Определение маршрутов в ASP.NET MVC (ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

9.3. Определение маршрутов в ASP.NET MVC

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

Схема URL для интернет-магазина

Задача нашего интернет-магазина – составление перечней товаров и их продажа. Используя принципы, изложенные ранее в этой главе, мы разработали схему URL, которая представлена в таблице 9-3.

Таблица 9-3: Схема URL для интернет-магазина
Номер роута URL Описание
1 http://example.com/ Главная страница; перенаправляет к списку виджетов каталога
2 http://example.com/privacy Отображает статическую страницу, которая содержит информацию о политике конфиденциальности сайта
3 http://example.com/products/<product code> Показывает страницу с информацией о товаре для указанного товарного кода
4 http://example.com/products/<product code>/buy Добавляет выбранный товар в корзину покупателя
5 http://example.com/basket Показывает корзину текущего пользователя
6 http://example.com/checkout Запускает процесс контроля для текущего пользователя

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

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

Добавляем пользовательские статические роуты

Наконец пришло время для реализации роутов, которые мы разработали. Давайте для начала рассмотрим статические роуты, которым соответствуют первые два из перечисленных в таблице 9-3. Роут 1 является нашим роутом по умолчанию, поэтому мы можем оставить его таким, как есть.

Первым мы создадим роут 2, который является чисто статическим. Это можно сделать путем вызова метода MapRoute для RouteCollection в методе RegisterRoutes файла Global.asax:

routes.MapRoute("privacy_policy", "privacy",
	new {controller = "Home", action = "Privacy"});

Этот роут только соотносит полностью статический URL с действием и контроллером. Практически он соотносит http://example.com/privacy с действием Privacy контроллера HomeController.

Предупреждение

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

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

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

В этом разделе мы добавим четыре динамических роута (последние четыре в таблице 9-3). Рассмотрим их в группах по два.

Роуты 3 и 4 созданы с использованием двух параметров роута:

routes.MapRoute("product", "products/{productCode}/{action}",
	new { controller = "Catalog", action = "Show" });

Два заполнителя будут соответствовать сегментам в URL, разделенным слешами. Параметр productCode является обязательным, но действие обязательным не является. Если действие не указано, роут выберет по умолчанию действие Show контроллера CatalogController и передаст ProductCode в качестве параметра.

Метод routes.MapRoute против routes.Add

Метод MapRoute, который мы постоянно используем, на самом деле является методом расширения, который содержит вызов метода Add для RouteCollection.

RouteCollection содержит коллекцию объектов Route (или точнее, экземпляры базового класса RouteBase). Вы можете добавлять экземпляры Routes непосредственно, не используя MapRoute, но синтаксис будет более сложным. К примеру, роут каталога будет определен следующим образом:

routes.Add(new Route("{action}",
	new RouteValueDictionary(new{ controller = "Catalog" }),
	new RouteValueDictionary(new{ action=@"basket|checkout" }),
	new MvcRouteHandler()));

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

Листинг 9-2: Действие контроллера обрабатывает динамические роуты
public class CatalogController : Controller
{
	private ProductRepository _productRepository = new ProductRepository();

	public ActionResult Show(string productCode)
	{
		var product = _productRepository.GetByCode(productCode);

		if (product == null)
		{
			return new NotFoundResult();
		}
		return View(product);
	}
}

Строка 7: Получает продукт, используя код продукта

Строка 11: Возвращает 404, если продукт не найден

Листинг 9-2 показывает реализацию действия в контроллере для роута товара. Хотя это и упрощенный пример по сравнению реальным приложением, в нем все просто до тех пор, пока мы не получим несуществующий товар. Это проблема. Товар не существует, но мы уже заверили механизм маршрутизации, что позаботимся о таких запросах. Поскольку управление теперь передается прямой системой поиска ресурсов, спецификация HTTP указывает, что если ресурса не существует, мы должны вернуть HTTP 404 Not Found. К счастью, это не проблема: мы можем создать пользовательский результат действия, который по выполнении генерирует 404:

public class NotFoundResult : ActionResult
{
	public override void ExecuteResult(ControllerContext context)
	{
		context.HttpContext.Response.StatusCode = 404;
		new ViewResult { ViewName = "NotFound" }.ExecuteResult(context);
	}
}

Создать NotFoundResult очень просто - путем наследования от ActionResult мы должны предоставить реализацию метода ExecuteResult. Этот метод устанавливает для ответа код статуса 404, а затем выводит представление под названием NotFound, которое находится в каталоге Views/Shared.

Примечание

ASP.NET MVC поставляется с подобным результатом действия для генерации ошибки 404 - HttpNotFoundResult. К сожалению, этот результат действия очень ограничен. Хоть он и устанавливает для ответа код статуса 404, он не предоставляет механизма для отображения пользовательской страницы ошибки, так что конечный пользователь увидит пустой экран.

Наконец, мы можем добавить роуты 5 и 6 из схемы:

routes.MapRoute("catalog", "{action}",
	new { controller = "Catalog" },
	new { action = @"basket|checkout" });

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

Четвертый параметр метода MapRoute содержит ограничения роута. Параметр ограничения представляет собой словарь в виде анонимного типа, который можно использовать для определения того, как должны быть ограничены параметры конкретного роута. В данном случае мы используем регулярное выражение для указания того, что соответствие для параметра действия будет найдено, только если указанный сегмент соответствует одной из строк - basket или checkout. Это ограничение необходимо, чтобы предотвратить передачу в контроллер неизвестных действий.

Примечание

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

Теперь мы добавили статические и динамические роуты, которые будут обрабатывать различные URL на нашем сайте. Но если поступит запрос, который не соответствует ни одному роуту – что тогда? В этом случае будет показано исключение, и вряд ли вы хотите увидеть его в реальном приложении. Чтобы исправить это, мы можем использовать catchall роут в сочетании с инфраструктурой обработки ошибок ASP.NET.

Обработчики роутов

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

Обработчики роутов представляют собой классы, которые реализуют интерфейс IRouteHandler и отвечают за создание соответствующего обработчика HTTP, который обработает запрос для выбранного роута.

По умолчанию в приложениях MVC используется обработчик роутов MvcRouteHandler, в то время как для страниц Web Forms используется PageRouteHandler. Мы рассмотрим маршрутизацию для Web Forms далее.

Catchall роуты

Сейчас мы добавим в пример приложения catchall роут, предназначенный для URL, которые не соответствует никакому другому роуту. Цель этого роута - показать сообщение об ошибке HTTP 404. Глобальные catchall роуты будут соответствовать любому роуту, и они должны быть определены в последнюю очередь:

routes.MapRoute("404-catch-all", "{*catchall}",
	new { controller = "Error", action = "NotFound" });

Значение catchall заменяет значение, которое было передано в catchall роут. В отличие от обычных параметров роутов, catchall параметры (с префиксом звездочкой) охватывают целую часть URL, включая слеши, которые обычно используются для разделения параметров роута. В этом случае роут будет соответствовать действию NotFound контроллера ErrorController:

public class ErrorController : Controller
{
	public ActionResult NotFound()
	{
		return new NotFoundResult();
	}
}

После вызова действия NotFound мы возвращаем экземпляр NotFoundResult, который мы создали ранее. Этот результат действия устанавливает для ответа код статуса 404, а затем показывает пользовательскую страницу ошибки.

Этот пример представляет собой корректный catchall роут, который буквально соответствует любому URL, для которого правила с более высоким приоритетом не нашли совпадений. Допустимо использовать другие catchall параметры в регулярных роутах, как в /events/{*info}, который будет соответствовать любому URL, который начинается с /events/. Но будьте осторожны с этими catchall параметрами, потому что они будут включать в себя любой другой текст URL, в том числе слэши и точки (которые обычно зарезервированы как разделители для сегментов роута). Это хорошая идея - использовать регулярные выражения в качестве параметра везде, где возможно, потому что вы по-прежнему контролируете данные, которые передаются в действие контроллера, а не используете все подряд. Другой интересный пример - это применение catchall роутов для динамических иерархий, таких как категории товаров. Когда вы достигаете предела системы маршрутизации, вы можете создать catchall роут самостоятельно.

Дружелюбный интерфейс при отображении ошибок HTTP

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

В данный момент роут по умолчанию {controller}/{action}/{id} можно удалить, потому что мы полностью настроили роуты в соответствии с нашей схемой URL. Мы также можем сохранить его как стандартный роут для доступа к другим контроллерам.

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

Соотнесение запросов с контроллерами - только одна часть работы, мы также должны уметь использовать систему маршрутизации в нашем приложении для генерации URL. В примере с интернет-магазином нам нужна возможность отображать ссылки на различные товары, доступные для покупки. Решение будет рассматриваться в следующем разделе.