Генерация маршрутизированных URL

162

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

Изменение текущего примера

Мы продолжим пользоваться проектом UrlsAndRoutes из предыдущих статей, но внесем в него пару изменений. Во-первых, нужно удалить папку AdditionalControllers вместе с содержащимся в ней файлом HomeController.cs. Чтобы выполнить удаление, щелкните правой кнопкой мыши на папке AdditionalControllers и выберите в контекстном меню пункт Delete.

Во-вторых, необходимо упростить маршруты в приложении. Отредактируйте файл App_Start/RouteConfig.cs согласно примеру:

using System.Web.Mvc;
using System.Web.Routing;

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapMvcAttributeRoutes();

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new
                {
                    controller = "Home",
                    action = "Index",
                    id = UrlParameter.Optional
                });
        }
    }
}

В проект модульного тестирования понадобится внести два изменения. Первое из них предусматривает удаление метода TestIncomingRoutes(), который в этой статье не применяется, т.к. она посвящена генерации исходящих маршрутов. Чтобы не получить тесты, которые не прошли, просто удалите указанный метод из файла RouteTests.cs.

Второе изменение заключается в добавлении ссылки на пространство имен System.Web.Mvc, для чего в проект модульного тестирования устанавливается NuGet-пакет MVC. Введите в окне консоли NuGet следующую команду:

Install-Package Microsoft.Aspnet.Mvc -version 5.0.0 -projectname UrlsAndRoutes.Tests

Пакет MVC 5 необходим для того, чтобы можно было пользоваться вспомогательными методами для генерации исходящих URL. В предыдущих статьях это не требовалось, поскольку поддержка для работы с входящими URL обеспечивается пространствами имен System.Web и System.Web.Routing.

Генерация исходящих URL в представлениях

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

Было бы заманчиво просто добавить статический элемент <a>, в атрибуте href которого указан нужный метод действия, например:

<а href="/Home/CustomVariable">Простая ссылка</a>

При стандартной конфигурации маршрутизации этот HTML-элемент создает ссылку, которая будет нацелена на метод действия CustomVariable() в контроллере Home. Вручную определяемые URL вроде показанного выше создаются быстро и просто. Однако они также чрезвычайно опасны; изменив схему URL для приложения, вы разрушите все жестко закодированные URL. После этого придется пройтись по всем представлениям в приложении и обновить все ссылки на контроллеры и методы действий - утомительный процесс, чреватый ошибками и сложный в проверке.

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

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

Простейший способ генерации исходящего URL в представлении предусматривает вызов вспомогательного метода Html.ActionLink(), как это сделано в примере ниже, в котором показано дополнение, внесенное в представление /Views/Shared/ActionName.cshtml:

@{
    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("Сгенерированный URL для ссылки", "CustomVariable")
    </div>
</body>
</html>

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

Добавление исходящего URL в представление

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

<a href="/Home/CustomVariable">Сгенерированный URL для ссылки</a>

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

using System.Web.Mvc;
using System.Web.Routing;

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapMvcAttributeRoutes();

            routes.MapRoute("NewRoute", "App/Do{action}",
                new {controller = "Home"});

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new
                {
                    controller = "Home",
                    action = "Index",
                    id = UrlParameter.Optional
                });
        }
    }
}

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

<a href="/App/DoCustomVariable">Сгенерированный URL для ссылки</a>

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

Результатом щелчка на ссылке является выдача исходящим URL входящего запроса

Сопоставление исходящих URL с маршрутами

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

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

<a href="">Сгенерированный URL для ссылки</a>

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

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

Направление на другие контроллеры

В стандартной версии метода ActionLink() предполагается, что целью является метод действия в том же самом контроллере, который визуализировал представление. Для создания исходящего URL, который указывает на другой контроллер, можно воспользоваться перегруженной версией метода ActionLink(), позволяющей задать имя контроллера, как показано в примере ниже:

@{
    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("Сгенерированный URL для ссылки", "Index", "Admin")
    </div>
</body>
</html>

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

После визуализации представления вы увидите, что сгенерирована следующая HTML-разметка:

<a href="/Admin">Сгенерированный URL для ссылки</a>

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

Система маршрутизации включает маршруты, которые были определены с использованием атрибута Route, когда выясняет, каким образом нацеливаться на заданный метод действия. В примере ниже демонстрируется изменение имени контроллера в вызове ActionLink(), чтобы он направлялся на действие Index контроллера Customer (в котором ранее мы использовали атрибуты маршрутизации):

...
@Html.ActionLink("Сгенерированный URL для ссылки", "Index", "Customer")
...

В результате генерируется следующая ссылка:

<a href="/Test">Сгенерированный URL для ссылки</a>

Это соответствует атрибуту Route, который применялся к методу действия Index() контроллера Customer:

// ...
[Route("~/Test")]
public ActionResult Index()
{
    ViewBag.Controller = "Customer";
    ViewBag.Action = "Index";
    return View("ActionName");
}
// ...

Передача дополнительных значений

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

...
@Html.ActionLink("Сгенерированный URL для ссылки", "CustomVariable", new { id = "Hi" })
...

В этом примере мы предоставляем значение для переменной сегмента по имени id. При визуализации представления мы получим следующую HTML-разметку:

<a href="/App/DoCustomVariable?id=Hi">Сгенерированный URL для ссылки</a>

Обратите внимание, что предоставленное значение было добавлено как часть строки запроса, чтобы соответствовать шаблону URL маршрута, который описан маршрутом. Причина заключается в отсутствии переменной сегмента, соответствующей id в этом маршруте. В примере ниже мы отредактировали маршруты в файле RouteConfig.cs, оставив только маршрут, имеющий сегмент id:

using System.Web.Mvc;
using System.Web.Routing;

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new
                {
                    controller = "Home",
                    action = "Index",
                    id = UrlParameter.Optional
                });
        }
    }
}

Запустив приложение еще раз, вы увидите, что вызов вспомогательного метода ActionLink() в представлении ActionName.cshtml генерирует следующий HTML-элемент:

<a href="/Home/CustomVariable/Hi">Сгенерированный URL для ссылки</a>

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

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

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

...
@Html.ActionLink("Сгенерированный URL для ссылки", "Index", "Home", null,
            new
            {
                id = "myAhrefID",
                @class = "myCSSClass"
            })
...

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

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

В результате показанного выше вызова ActionLink() получается следующая HTML-разметка:

<a class="myCSSClass" href="/" id="myAhrefID">
   Сгенерированный URL для ссылки
</a>

Генерация полностью определенных URL в ссылках

Все ссылки, сгенерированные до сих пор, содержали относительные URL, но вспомогательный метод ActionLink() можно также использовать для генерации полностью определенных URL, как показано в примере ниже:

@Html.ActionLink("Сгенерированный URL для ссылки", "Index", "Home",
            "https", "google.com", "fragmentName",
            new { id = "MyId" },
            new { id = "myAhrefID", @class = "myCSSClass" })

Здесь вызывается перегруженная версия метода ActionLink() с максимальным количеством параметров, которая принимает значения для протокола (https в этом примере), имени целевого сервера (google.com), фрагмента URL (fragmentName), а также все другие опции, упомянутые ранее. При визуализации представления метод ActionLink() генерирует следующую HTML-разметку:

<a class="myCSSClass" id="myAhrefID"
    href="https://google.com/Home/Index/MyId#fragmentName">
    Сгенерированный URL для ссылки
</a>

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

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

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

В примере ниже показаны изменения, внесенные в файл ActionName.cshtml для создания URL с помощью вспомогательного метода Url.Action():

@{
    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>
        URL: @Url.Action("Index", "Home", new { id = "MyId" })
    </div>
</body>
</html>

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

Визуализация URL (как противоположность ссылке) в представлении

Генерация исходящих URL в методах действий

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

using System.Web.Mvc;

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

        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(), как показано в примере ниже:

// ...
public RedirectToRouteResult MyActionMethod()
{
    return RedirectToAction("Index");
}
// ...

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

Чтобы отправить инструкцию перенаправления с использованием URL, сгенерированного только из свойств объекта, можно применить метод RedirectToRoute(). Этот метод также возвращает объект RedirectToRouteResult и обеспечивает в точности тот же самый эффект, как и вызов метода RedirectToAction():

// ...
public RedirectToRouteResult MyActionMethod()
{
    return RedirectToRoute(new { controller = "Home", action = "Index", id = "MyID" });
}
// ...

Генерация URL из специфического маршрута

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

using System.Web.Mvc;
using System.Web.Routing;

namespace UrlsAndRoutes
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            // routes.MapMvcAttributeRoutes();
            routes.MapRoute("MyRoute", "{controller}/{action}");
            routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" });
        }
    }
}

В конфигурации определены два маршрута, которым назначены имена - MyRoute и MyOtherRoute. Существуют две причины именования маршрутов:

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

...
@Html.ActionLink("Ссылка", "Index", "Customer")
...

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

<a href="/Customer/Index">Ссылка</a>

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

...
@Html.RouteLink("Ссылка", "MyOtherRoute", "Index", "Customer")
...

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

<a Length="8" href="/App/Index?Length=5">Ссылка</a>

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

Можно также назначать имена маршрутам, которые определяются с помощью атрибута Route. В примере ниже демонстрируется именование такого маршрута в контроллере Customer:

// ...
[Route("Add/{user}/{id:int}", Name="AddRoute")]
public string Create(string user, int id)
{
    return string.Format("Пользователь: {0}, Id: {1}", user, id);
}
// ...

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

Аргумент против именования маршрутов

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

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

Пройди тесты
Лучший чат для C# программистов