Главная страница   /   14.2. Создание исходящих URL в представлениях (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

14.2. Создание исходящих URL в представлениях

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

Так заманчиво просто добавить статический элемент a, атрибут href которого нацелен на метод действия:

<a href="/Home/CustomVariable">This is an outgoing URL</a>

Этот HTML элемент создает ссылку, которая будет обработана как запрос для метода действия CustomVariable контроллера Home, с дополнительной переменной сегмента Hello. Определенные вручную URL, как этот, быстро и просто создавать. Они также чрезвычайно опасны, и вам нужно будет ломать все жестко закодированные URL при изменении URL схемы вашего приложения. Затем вам нужно будет пройти по всем представлениям в приложении и обновить все ссылки для контроллеров и методов действий: это утомительный, подверженный ошибкам и плохо тестируемый процесс.

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

Использование роутинговой системы для генерации исходящих URL

Самый простой способ генерации исходящих URL в представлении заключается в вызове вспомогательного метода Html.ActionLink, как показано на листинге 14-2, где продемонстрированы дополнения, которые мы внесли в файл представления /Views/Shared/ActionName.cshtml.

Листинг 14-2: Использование вспомогательного метода Html.ActionLink для генерации исходящих URL
@{
	Layout = null;
}
<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>ActionName</title>
</head>
<body>
	<div>The controller is: @ViewBag.Controller</div>
	<div>The action is: @ViewBag.Action</div>
	<div>
		@Html.ActionLink("This is an outgoing URL", "CustomVariable")
	</div>
</body>
</html>

Параметрами метода ActionLink являются текст для ссылки и имя метода действия, на который должна быть нацелена ссылка. Вы можете увидеть результат этого дополнения, запустив приложение и позволив браузеру перейти в корневой URL, как показано на рисунке 14-1.

Рисунок 14-1: Добавление в представление исходящего URL

HTML, который генерирует метод ActionLink, основывается на текущей конфигурации маршрутизации. Например, при использовании схемы, определенной в листинге 14-1 (и предполагая, что представление создается запросом к контроллеру Home), мы получаем этот HTML:

<a href="/Home/CustomVariable">This is an outgoing URL</a>

Может показаться, что мы прошли длинный путь, чтобы воссоздать определенный вручную URL, который мы показали вам раньше, но преимущество такого подхода заключается в том, что он автоматически реагирует на изменение конфигурации маршрутизации. В качестве демонстрации мы добавили новый роут в файл RouteConfig.cs, как показано в листинге 14-3.

Листинг 14-3: Добавление роута в приложение
public static void RegisterRoutes(RouteCollection routes) {
	routes.MapRoute("NewRoute", "App/Do{action}",
		new { controller = "Home" });
	routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
		new { controller = "Home", action = "Index",
		id = UrlParameter.Optional });
}

Новый роут меняет URL схему для запросов, нацеленных на контроллер Home. Если вы запустите приложение, вы увидите, что это изменение будет отражено в HTML, который создается вспомогательным методом ActionLink:

<a href="/App/DoCustomVariable">This is an outgoing URL</a>

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

Рисунок 14-2: Результат нажатия на ссылку заключается в том, чтобы сделать исходящий URL входящим запросом

Понимание того, как выбираются роуты для генерации URL

Вы видели, как изменение роутов, которые определяют URL схему, меняет способ, которым создаются исходящие URL. Приложения, как правило, определяют несколько роутов, и важно понимать, как роуты выбираются для генерирования URL. Система маршрутизации обрабатывает роуты в том порядке, в котором они были добавлены в объект RouteCollection, который передается методу RegisterRoutes. Каждый роут проверяется на соответствие, и для этого должны быть выполнены три условия:

  • Значение должно быть доступно для каждой сегментной переменной, определенной в URL паттерне. Чтобы найти значения для каждой сегментной переменной, система маршрутизации прежде всего рассматривает значения, которые предоставили мы (используя свойства анонимного типа), затем значения переменных для текущего запроса, и, наконец, значения по умолчанию, определенные в роуте. (Мы вернемся ко второму виду этих значений далее в этой главе).
  • Ни одно из значений, которые мы предоставили для сегментных переменных, не может конфликтовать с переменными «только по умолчанию» (default-only), определенными в роуте. Это переменные, для которых были предоставлены значения по умолчанию, но которые не появляются в URL паттерне. Например, в этом определении роута переменная myVar является default-only:
routes.MapRoute("MyRoute", "{controller}/{action}",
	new { myVar = "true" });

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

  • Значения для всех сегментных переменных должны удовлетворять ограничению роутов. См. "Ограничение роутов" в предыдущей главе для информации по различным видам ограничений.

Чтобы внести ясность: система маршрутизации не пытается найти роут, который обеспечивает наиболее подходящее совпадение. Она находит только первое совпадение, и в этот момент она использует роут для генерации URL; любые последующие роуты игнорируются. По этой причине вы должны определить первыми наиболее конкретные роуты. Важно также тестировать генерирование исходящих URL. Если вы попытаетесь создать URL, для которых не может быть найден соответствующий роут, вы создадите ссылку, которая содержит пустой атрибут href:

<a href="">About this application</a>

Ссылка будет показывать представление должным образом, но не будет функционировать, как нужно, когда пользователь нажмет на нее. Если вы создаете только URL (мы покажем вам, как то делается, далее в этой главе), то результат будет null, который отображается в представлениях как пустая строка. Вы можете контролировать соответствие роутов с помощью именованных роутов. См. раздел "Создание URL из конкретного роута" далее в этой главе.

Первый объект Route, отвечающий этим критериям, создаст не-null URL, и это завершит процесс генерирования URL. Выбранные значения параметров будут заменены для каждого параметра сегмента. Если вы установили явные параметры, которые не соответствуют параметрам сегмента или параметрам по умолчанию, тогда метод будет добавлять их в виде набора пар имя/значение строки запроса.

Юнит тест: тестирование исходящих URL

Самый простой способ проверить генерацию исходящего URL – то использовать статический метод UrlHelper.GenerateUrl, который имеет параметры для всех способов, которыми вы можете направлять генерирование роута, например, указав имя роута, контроллер, действие, значения сегментов и так далее. Вот тестовый метод, который проверяет генерирование URL по отношению к роуту, определенному в листинге 14-3. Мы добавили его в файл RouteTests.cs тестового проекта (потому что он использует тестовые методы, которые мы определили в предыдущей главе):

[TestMethod]
public void TestIncomingRoutes() {
// ...код удален, чтобы предотвратить ошибки теста...
}
[TestMethod]
public void TestOutgoingRoutes() {
	// Arrange
	RouteCollection routes = new RouteCollection();
	RouteConfig.RegisterRoutes(routes);
	RequestContext context = new RequestContext(CreateHttpContext(), new RouteData());
	// Act - сгенерировать URL
	string result = UrlHelper.GenerateUrl(null, "Index", "Home", null,
		routes, context, true);
	// Assert
	Assert.AreEqual("/App/DoIndex", result);
}
...

Мы генерируем URL, а не ссылку, потому нам не нужно беспокоиться о тестировании окружающего HTML. Метод UrlHelper.GenerateUrl требует объект RequestContext, который мы создаем с помощью mock-объекта HttpContextBase вспомогательного метода тестирования CreateHttpContext. Прочитайте о юнит тестировании входящих URL в предыдущей главе, чтобы увидеть полный исходный код CreateHttpContext.

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

Нацеленность на другие контроллеры

Версия по умолчанию метода ActionLink предполагает, что вы хотите работать с методом действия в том же контроллере, который вызвал отображение представления. Чтобы создать исходящий URL, нацеленный на другой контроллер, вы можете использовать другой перегруженный вариант, который позволяет указать имя контроллера, как показано в листинге 14-4. Здесь мы внесли изменение в представление ActionName.cshtml.

Внимание

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

Листинг 14-4: Нацеленность на другой контроллер при помощи вспомогательного метода ActionLink
@{
	Layout = null;
}
<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>ActionName</title>
</head>
<body>
	<div>The controller is: @ViewBag.Controller</div>
	<div>The action is: @ViewBag.Action</div>
	<div>
		@Html.ActionLink("This is an outgoing URL", "CustomVariable")
	</div>
	<div>
		@Html.ActionLink("This targets another controller", "Index", "Admin")
	</div>
</body>
</html>

Когда вы отобразите представление, вы увидите следующий сгенерированный HTML:

<a href="/Admin">This targets another controller</a>

Наш запрос для URL, предназначенного для метода действия Index контроллера Admin, был выражен методом ActionLink как /Admin. Система маршрутизации довольно умна, и она знает, что роут, определенный в приложении, будет использовать по умолчанию метод действия Index, что позволяет опускать ненужные сегменты.

Передача особых значений

Вы можете передать значения для сегментных переменных, используя анонимный тип, при помощи свойств, представляющих сегменты. В листинге 14-5 приведен пример, который мы добавили в файл представления ActionName.cshtml.

Листинг 14-5: Предоставление значений для сегментных переменных
<body>
	<div>The controller is: @ViewBag.Controller</div>
	<div>The action is: @ViewBag.Action</div>
	<div>
		@Html.ActionLink("This is an outgoing URL",
			"CustomVariable", new { id = "Hello" })
	</div>
</body>

В этом примере мы предоставили значение для сегментной переменной id. Если наше приложение использует роут, показанный в листинге 14-3, то мы получим следующий HTML, когда отобразим представление:

<a href="/App/DoCustomVariable?id=Hello">This is an outgoing URL</a>

Обратите внимание, что значение, которое мы передали, было добавлено в качестве части строки запроса, чтобы вписаться в URL паттерн, описанный роутом, который мы добавили в листинге 14-3. Это потому что нет никакой сегментной переменной, которая соответствует id в этом роуте. В листинге 14-6 мы отредактировали роуты в файле RouteConfig.cs, чтобы использовался лишь тот роут, который имеет сегмент id.

Листинг 14-6: Отключение роута
...
public static void RegisterRoutes(RouteCollection routes) {
	// Это выражения под комментариями
	//routes.MapRoute("NewRoute", "App/Do{action}",
		// new { controller = "Home" });
	routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
		new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
...

Если мы запустим приложение еще раз, URL в представлении ActionName.cshtml создаст следующий HTML элемент:

<a href="/Home/CustomVariable/Hello">This is an outgoing URL</a>

На этот раз значение, которое мы назначили свойству id, включено в качестве URL сегмента, в соответствии с активным роутом в конфигурации приложения. Мы ведь говорили, что система маршрутизации довольно умна?

Повторное использование сегментных переменных

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

Представьте, что у нашего приложения есть единственный роут:

routes.MapRoute("MyRoute", "{controller}/{action}/{color}/{page}");

Теперь представьте, что пользователь на данный момент переходит по URL /Catalog/List/Purple/123, и мы отображаем ссылку следующим образом:

@Html.ActionLink("Click me", "List", "Catalog", new {page=789}, null)

Можно было бы ожидать, что система маршрутизации не в состоянии найти соответствие с роутом, потому что мы не предоставили значение переменной сегмента color, и для нее нет значения по умолчанию. Однако, это не так. Система маршрутизации найдет соответствие с роутом, который мы определили. Она сгенерирует следующий HTML:

<a href="/Catalog/List/Purple/789">Click me</a>

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

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

@Html.ActionLink("Click me", "List", "Catalog", new {color="Aqua"}, null)

Мы предоставили значение для color, но не для page. Но color появляется перед page в URL паттерне, и поэтому система маршрутизации не будет повторно использовать значения из входящего URL, и роут не будет совпадать.

Лучший способ борьбы с таким поведением – это не дать ему случиться. Мы настоятельно рекомендуем вам не полагаться на такое поведение, а также чтобы вы задавали значения для всех переменных сегмента в URL паттерне. Такое поведение не только сделает ваш код трудно читаемым, но вы в конечном итоге только и будете что сидеть и думать о том, каким же образом ваши пользователи делают запросы, что приведет к неоправданным временным и трудовым расходам.

Указание HTML атрибутов

Мы сосредоточили свое внимание на URL, которые генерирует вспомогательный метод ActionLink, но помните, что метод создает полный якорь HTML элемента (a). Мы можем установить атрибуты для этого элемента, предоставляя анонимный тип, свойства которого соответствуют требуемым атрибутам. Листинг 14-7 показывает, как мы изменили представление ActionName.cshtml с целью установить атрибут id и присвоить класс HTML элементу a.

Листинг 14-7: Генерирование якорного элемента с атрибутами
<body>
	<div>The controller is: @ViewBag.Controller</div>
	<div>The action is: @ViewBag.Action</div>
	<div>
		@Html.ActionLink("This is an outgoing URL",
			"Index", "Home", null, new {id = "myAnchorID", @class = "myCSSClass"})
	</div>
</body>

Мы создали новый анонимный тип, который имеет свойства id и class, и передали его в качестве параметра методу ActionLink. Мы передали null для дополнительных значений сегментных переменных, указывая, что у нас нет никаких значений, которые мы можем предоставить.

Совет

Обратите внимание, что мы поставили перед свойством class символ @. Это возможность языка C#, которая позволяет нам использовать зарезервированные ключевые слова в качестве имен членов класса.

Когда отображается ActionLink, мы получаем следующий HTML:

<a class="myCSSClass" href="/" id="myAnchorID">This is an outgoing URL</a>

Создание полных URL в ссылках

Все ссылки, которые мы генерировали, содержали относительные URL, но мы также можем использовать вспомогательный метод ActionLink, чтобы генерировать полные (абсолютные) ссылки, как показано в листинге 14-8.

Листинг 14-8: Создание абсолютного URL
<body>
	<div>The controller is: @ViewBag.Controller</div>
	<div>The action is: @ViewBag.Action</div>
	<div>
		@Html.ActionLink("This is an outgoing URL", "Index", "Home",
			"https", "myserver.mydomain.com", " myFragmentName",
			new { id = "MyId"},
			new { id = "myAnchorID", @class = "myCSSClass"})
	</div>
</body>

Это перегруженный метод ActionLink с большинством параметров, что позволяет нам предоставлять значения для протокола (https, в нашем примере), имя целевого сервера (myserver.mydomain.com) и URL фрагмент (myFragmentName), а также все другие опции, которые вы видели раньше. При отображении в представлении вызов метода генерирует следующий HTML:

<a class="myCSSClass"
	href="https://myserver.mydomain.com/Home/Index/MyId#myFragmentName"
	id="myAnchorID">This is an outgoing URL</a>

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

Генерация URL (а не ссылок)

Вспомогательный метод Html.ActionLink создает полные HTML элементы <a>, а это именно то, чего мы хотим практически всегда. Тем не менее, бывают случаи, когда нам просто нужен URL. Это может случиться, потому что мы хотим отобразить URL, построить HTML для ссылки вручную, отобразить значение URL или включить URL в качестве элемента данных в HTML страницу, подлежащую отображению.

При таких обстоятельствах мы можем использовать метод Url.Action для создания только URL, а не окружающего HTML. Листинг 14-9 показывает изменения, которые мы внесли в файл ActionName.cshtml, чтобы создать URL при помощи Url.Action.

Листинг 14-9: Создание URL без окружающего HTML
@{
	Layout = null;
}
<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>ActionName</title>
</head>
<body>
	<div>The controller is: @ViewBag.Controller</div>
	<div>The action is: @ViewBag.Action</div>
	<div>
		This is a URL:
		@Url.Action("Index", "Home", new { id = "MyId" })
	</div>
</body>
</html>

Метод Url.Action работает таким же образом, как и метод Html.ActionLink, за исключением того, что он генерирует только URL. Перегруженные версии метода и параметры, которые они принимают, одинаковы для обоих методов, и вы можете сделать все то же самое с Url.Action, что мы продемонстрировали для Html.ActionLink в предыдущих разделах. На рисунке 14-3 показано, как отображается URL из листинга 14-9.

Рисунок 14-3: Отображение URL в представлении

Создание исходящих URL в методах действия

В большинстве случаев мы хотим генерировать исходящие URL в представлениях, но бывают случаи, когда мы хотим сделать нечто подобное внутри метода действия. Если нам просто нужно создать URL, мы можем использовать тот же вспомогательный метод, который мы использовали в представлении, как показано в листинге 14-10. Здесь продемонстрирован новый метод действия, который мы добавили в контроллер Home.

Листинг 14-10: Создание исходящего URL в методе действия
public ViewResult MyActionMethod() {
	string myActionUrl = Url.Action("Index", new { id = "MyID" });
	string myRouteUrl = Url.RouteUrl(new { controller = "Home", action = "Index" });
	//... сделать что-то с URL...
	return View();
}

Для роутинга в нашем примере приложения переменной myActionUrl будет присвоено значение /Home/Index/MyID, а переменной myRouteUrl будет присвоено значение /, что согласуется с результатами, которые получаются из вызова этих вспомогательных методов в представлении.

Более общим требованием является перенаправление браузера клиента на другой URL. Мы можем сделать это, возвращая результат вызова метода RedirectToAction, как показано в листинге 14-11.

Листинг 14-11: Перенаправление на другое действие
public RedirectToRouteResult MyActionMethod() {
	return RedirectToAction("Index");
}

Результатом метода RedirectToAction является RedirectToRouteResult, который указывает MVC выполнять инструкцию перенаправления к URL, который будет вызывать определенное действие. Есть обычные перегруженные версии метода RedirectToAction, которые указывают контроллер и значения для сегмента переменных в генерируемых URL.

Если вы хотите сделать перенаправление, используя URL, сгенерированный только из свойств объекта, вы можете использовать метод RedirectToRoute, как показано в листинге 14-12.

Листинг 14-12: Перенаправление на URL, который сгенерирован из свойств анонимного типа
public RedirectToRouteResult MyActionMethod() {
	return RedirectToRoute(new {
		controller = "Home",
		action = "Index",
		id = "MyID" });
}

Этот метод также возвращает объект RedirectToRouteResult и имеет тот же результат, что и вызов метода RedirectToAction.

Генерирование URL из конкретного роута

В наших предыдущих примерах система маршрутизации выбирала роут, который использовался для генерации URL или ссылки. В этом разделе мы покажем вам, как контролировать этот процесс и выбирать конкретные роуты. В листинге 14-13 изменили информацию в файле RouteConfig.cs, чтобы лучше продемонстрировать эту возможность.

Листинг 14-13: Изменение роутинговой конфигурации
public static void RegisterRoutes(RouteCollection routes) {
	routes.MapRoute("MyRoute", "{controller}/{action}");
	routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" });
}

Мы указали имена для обоих роутов: MyRoute и MyOtherRoute. Есть две причины именования роутов:

  • В качестве напоминания о целях роута
  • Чтобы вы могли выбрать конкретный роут для генерации исходящего URL

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

@Html.ActionLink("Click me", "Index", "Customer")

исходящая ссылка всегда будет генерироваться при помощи MyRoute:

<a href="/Customer/Index">Click me</a>

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

@Html.RouteLink("Click me", "MyOtherRoute","Index", "Customer")

В результате этого ссылка, сгенерированная вспомогательным методом, выглядит вот так:

<a Length="8" href="/App/Index?Length=5">Click me</a>

В данном случае контроллер, который мы указали, Customer, переписан роутом, и ссылка вместо этого нацелена на контроллер Home.

Проблема именованных роутов

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

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