Главная страница   /   13.6. Определение пользовательских переменных для сегментов (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

13.6. Определение пользовательских переменных для сегментов

Сегментные переменные controller и action имеют особое значение для MVC и, очевидно, что они соответствуют контроллеру и методу действия, которые будут использоваться для обработки запроса. Мы не ограничены этими встроенными сегментными переменными: мы также можем определить наши собственные переменные, как показано в листинге 13-15. (Мы удалили все существующие роуты из предыдущего раздела, чтобы начать все сначала).

Листинг 13-15: Определение дополнительных переменных в URL паттерне
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
	public class RouteConfig
	{
		public static void RegisterRoutes(RouteCollection routes)
		{
			routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
			new
			{
				controller = "Home",
				action = "Index",
				id = "DefaultId"
			});
		}
	}
}

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

Внимание

Некоторые имена зарезервированы и не доступны для имен пользовательских переменных сегмента. Это controller, action и area. Смысл первых двух очевиден, и мы объясним роль областей в следующей главе.

Мы можем получить доступ к любой сегментной переменной в методе действий с помощью свойства RouteData.Values. Чтобы продемонстрировать это, мы добавили метод действия CustomVariable в класс HomeController, как показано в листинге 13-16.

Листинг 13-16: Доступ к пользовательской переменной сегмента в методе действия
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers
{
	public class HomeController : Controller
	{
		public ActionResult Index()
		{
			ViewBag.Controller = "Home";
			ViewBag.Action = "Index";
			return View("ActionName");
		}
		public ActionResult CustomVariable()
		{
			ViewBag.Controller = "Home";
			ViewBag.Action = "CustomVariable";
			ViewBag.CustomVariable = RouteData.Values["id"];
			return View();
		}
	}
}

Этот метод получает значение пользовательской переменной в роутовом URL паттерне и передает его представлению при помощи ViewBag. Щелкните правой кнопкой мыши по новому методу действия в редакторе кода и выберите Add View. Назовите представление CustomVariable и нажмите кнопку Add. Visual Studio создаст новый файл CustomVariable.cshtml в папке /Views/Home. Измените представление так, чтобы оно соответствовало содержанию, показанному в листинге 13-17.

Листинг 13-17: Отображение значения пользовательской сегментной переменной
@{
	Layout = null;
}
<!DOCTYPE html>
<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>Custom Variable</title>
</head>
<body>
	<div>The controller is: @ViewBag.Controller</div>
	<div>The action is: @ViewBag.Action</div>
	<div>The custom variable is: @ViewBag.CustomVariable</div>
</body>
</html>

Чтобы увидеть результат использования пользовательской переменной сегмента, запустите приложение и перейдите по URL /Home/CustomVariable/Hello. Вызывается метод действия CustomVariable в контроллере Home, и значение пользовательской переменной сегмента извлекается из ViewBag и передается представлению. Вы можете увидеть результат на рисунке 13-7.

Рисунок 13-7: Отображение значения пользовательской сегментной перменной

Мы предоставили значение по умолчанию для переменной сегмента id, что означает, что вы увидите результаты, показанные на рисунке 13-8, если вы перейдете на /Home/CustomVariable.

Рисунок 13-8: Значение по умолчанию для пользовательской сегментной переменной

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

Мы включили поддержку для тестирования пользовательских переменных сегмента во вспомогательные методы тестирования. Метод TestRouteMatch имеет дополнительный параметр, который принимает анонимный тип, содержащий имена свойств, которые мы хотим протестировать, и ожидаемые значения. Вот изменения, которые мы сделали для тестового метода TestIncomingRoutes, чтобы проверить роут, определенный в листинге 13-15:

...
[TestMethod]
public void TestIncomingRoutes() {
	TestRouteMatch("~/", "Home", "Index", new { id = "DefaultId" });
	TestRouteMatch("~/Customer", "Customer", "index", new { id = "DefaultId" });
	TestRouteMatch("~/Customer/List", "Customer", "List",
		new { id = "DefaultId" });
	TestRouteMatch("~/Customer/List/All", "Customer", "List", new { id = "All" });
	TestRouteFail("~/Customer/List/All/Delete");
}
...

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

Использование свойства RouteData.Values является только одним способом доступа к пользовательским переменным роута. Другой способ гораздо более элегантный. Если мы определим параметры нашего метода действия именами, которых соответствуют переменным URL паттерна, MVC передаст значения, полученные из URL, в качестве параметров методу действия. Например, пользовательская переменная, которая была определена в роуте в листинге 13-15, называется id. Мы можем изменить метод действия CustomVariable, так чтобы он имел соответствующий параметр, как показано в листинге 13-18.

Листинг 13-8: Изменение метода действия так, чтобы сегментная переменная соответствовала его параметру
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers
{
	public class HomeController : Controller
	{
		public ActionResult Index()
		{
			ViewBag.Controller = "Home";
			ViewBag.Action = "Index";
			return View("ActionName");
		}
		public ActionResult CustomVariable(string id)
		{
			ViewBag.Controller = "Home";
			ViewBag.Action = "CustomVariable";
			ViewBag.CustomVariable = id;
			return View();
		}
	}
}

Когда система маршрутизации находит соответствие URL с определенным нами в листинге 13-15 роутом, значение третьего сегмента в URL присваивается пользовательской переменной id. MVC сравнивает список сегментных переменных со списком параметров метода действия, и если имена совпадают, передает значения из URL в метод.

Мы определили параметр id как string, но MVC попытается преобразовать URL значение в тот тип параметра, который мы определим. Если мы объявили параметр id как int или DateTime, то мы получим из URL значение, разобранное как экземпляр этого типа. Это элегантная и полезная возможность, которая устраняет необходимость самим проводить преобразование типов.

Примечание

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

Определение дополнительных URL сегментов

Дополнительный сегмент URL это тот, который пользователь не должен указывать, но для которого не задано значение по умолчанию. В листинге 13-19 показан пример, и мы указываем, что переменная сегмента не является обязательной, установив значение по умолчанию UrlParameter.Optional.

Листинг 13-19: Указание дополнительного URL сегмента
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
	public class RouteConfig
	{
		public static void RegisterRoutes(RouteCollection routes)
		{
			routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
			new
			{
				controller = "Home",
				action = "Index",
				id = UrlParameter.Optional
			});
		}
	}
}

Этот роут будет соответствовать URL, независимо от того, был ли указан id сегмент. В таблице 13-3 показано, как это работает с различными URL.

Таблица 13-3: Соответствие URL с дополнительной сегментной переменной
Число сегментов Пример Соответствие
0 mydomain.com controller = Home, action = Index
1 mydomain.com/Customer controller = Customer, action = Index
2 mydomain.com/Customer/List controller = Customer, action = List
3 mydomain.com/Customer/List/All controller = Customer, action = List, id = All
4 mydomain.com/Customer/List/All/Delete Нет соответствия: слишком много сегментов

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

Листинг 13-20: Проверка на то, было ли предоставлено значение для дополнительной сегментной переменной
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace UrlsAndRoutes.Controllers
{
	public class HomeController : Controller
	{
		public ActionResult Index()
		{
			ViewBag.Controller = "Home";
			ViewBag.Action = "Index";
			return View("ActionName");
		}
		public ActionResult CustomVariable(string id)
		{
			ViewBag.Controller = "Home";
			ViewBag.Action = "CustomVariable";
			ViewBag.CustomVariable = id == null ? "<no value>" : id;
			return View();
		}
	}
}

Вы можете увидеть результат, если запустите приложение и перейдете по /Home/CustomVariable (здесь не определено значение сегментной переменной id), на рисунке 13-9.

Рисунок 13-9: Определение того, что URL не содержит значение для дополнительной сегментной переменной

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

Некоторые разработчики, которые очень сосредоточены на разделении понятий в MVC паттерне, не любят устанавливать значения по умолчанию для переменных сегмента в роутах для приложения. Если это касается и вас, вы можете использовать дополнительные параметры C# наряду с дополнительной переменной сегмента в роуте, чтобы определить значения по умолчанию для параметров метода действия. В качестве примера в листинге 13-21 показано, как мы изменили метод действия CustomVariable, чтобы определить значение по умолчанию для параметра id, который будет использоваться, если URL не содержит значения.

Листинг 13-21: Определение значения по умолчанию для параметра метода действия
...
public ActionResult CustomVariable(string id = "DefaultId") {
	ViewBag.Controller = "Home";
	ViewBag.Action = "CustomVariable";
	ViewBag.CustomVariable = id;
	return View();
}
...

Для параметра id всегда будет значение (либо из URL, либо по умолчанию), поэтому мы удалили код, который имеет дело со значением null. Этот метод действия совместно с роутом, который мы определили в листинге 13-19, функционально эквивалентен роуту, который мы определили в листинге 13-15:

...
routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
	new { controller = "Home", action = "Index", id = "DefaultId" });
...

Разница заключается в том, что значение по умолчанию для сегментной переменной id обозначается в коде контроллера, а не в определении роута.

Юнит тестирование: дополнительные URL сегменты

Единственный вопрос, который необходимо учитывать при тестировании дополнительных URL сегментов, заключается в том, что сегментная переменная не будет добавлена в коллекцию RouteData.Values, если значение не было найдено в URL. Это означает, что вам не следует включать переменную в анонимный тип, если вы тестируете URL, который содержит дополнительный сегмент. Вот наши изменения тестового метода TestIncomingRoutes для роута, определенного в листинге 13-19.

...
[TestMethod]
public void TestIncomingRoutes() {
	TestRouteMatch("~/", "Home", "Index");
	TestRouteMatch("~/Customer", "Customer", "index");
	TestRouteMatch("~/Customer/List", "Customer", "List");
	TestRouteMatch("~/Customer/List/All", "Customer", "List", new { id = "All" });
	TestRouteFail("~/Customer/List/All/Delete");
}
...

Определение роутов с разным числом сегментов

Другой способ изменить стандартный консерватизм URL паттернов – это принять число переменных URL сегментов. Это позволяет принимать URL произвольной длины в одном роуте. Вы определяете поддержку переменных сегмента, обозначив одну из сегментных переменных как catchall, что делается при помощи добавления звездочки (*), как показано в листинге 13-22.

Листинг 13-22: Назначение catchall переменной
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
	public class RouteConfig
	{
		public static void RegisterRoutes(RouteCollection routes)
		{
			routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
			new
			{
				controller = "Home",
				action = "Index",
				id = UrlParameter.Optional
			});
		}
	}
}

Мы расширили роут из предыдущего примера, чтобы добавить переменную сегмента catchall, которую мы образно назвали catchall. Этот роут теперь будет соответствовать любому URL, независимо от количества сегментов, которые он содержит, или значения любого из этих сегментов. Первые три сегмента используются для установки значений для переменных controller, action и id соответственно. Если URL содержит дополнительные сегменты, все они назначаются переменной catchall, как показано в таблице 13-4.

Таблица 13-4: Соответствие URL с сегментной переменной catchall
Число сегментов Пример Соответствие
0 / controller = Home, action = Index
1 /Customer controller = Customer, action = Index
2 /Customer/List controller = Customer, action = List
3 /Customer/List/All controller = Customer, action = List, id = All
4 /Customer/List/All/Delete controller = Customer, action = List, id = All, catchall = Delete
5 /Customer/List/All/Delete/Perm controller = Customer, action = List, id = All, catchall = Delete/Perm

Здесь нет верхнего ограничения по числу сегментов, поэтому URL паттерн данного роута будет совпадать с любым подходящим URL. Обратите внимание, что сегменты, находящиеся в catchall, представлены в форме segment/segment/segment. Мы должны обрабатывать строку, чтобы получить отдельные сегменты.

Юнит тест: тестирование сегментных переменных catchall

Мы можем обрабатывать переменную catchall как пользовательскую переменную. Разница лишь в том, что мы должны ожидать несколько сегментов, которые связываются в одно значение, например, segment/segment/segment. Обратите внимание, что мы не получим начальный или конечный знаки /. Вот изменения метода TestIncomingRoutes, которые демонстрируют тестирование сегмента catchall. Тут используется роут, определенный в листинге 13-22, и URL, показанные в таблице 13-4:

...
[TestMethod]
public void TestIncomingRoutes() {
	TestRouteMatch("~/", "Home", "Index");
	TestRouteMatch("~/Customer", "Customer", "Index");
	TestRouteMatch("~/Customer/List", "Customer", "List");
	TestRouteMatch("~/Customer/List/All", "Customer", "List", new { id = "All" });
	TestRouteMatch("~/Customer/List/All/Delete", "Customer", "List",
		new { id = "All", catchall = "Delete" });
	TestRouteMatch("~/Customer/List/All/Delete/Perm", "Customer", "List",
		new { id = "All", catchall = "Delete/Perm" });
}
...

Определение приоритета контроллера по пространству имен

Если входящий URL соответствует роуту, MVC фреймворк берет значение переменной controller и ищет соответствующее имя. Например, если значение переменной controller равно Home, то MVC ищет контроллер HomeController. Это неполное имя класса, то есть MVC не знает, что делать, если есть два или более класса HomeController в разных пространствах имен.

Чтобы понять эту проблему, создайте новую папку в корневом каталоге проекта, назовите ее AdditionalControllers и добавьте новый контроллер Home. Установите содержание, чтобы оно соответствовало листингу 13-23.

Листинг 13-23: Добавление второго контроллера Home в проект
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace UrlsAndRoutes.AdditionalControllers
{
	public class HomeController : Controller
	{
		public ActionResult Index()
		{
			ViewBag.Controller = "Additional Controllers - Home";
			ViewBag.Action = "Index";
			return View("ActionName");
		}
	}
}

Если вы запустите приложение, вы увидите ошибку, показанную на рисунке 13-10.

Рисунок 13-10: Возникает ошибка, если есть два контроллера с одним и тем же именем

MVC фреймворк искал класс HomeController и нашел два: один в нашем исходном проекте и один в нашем проекте AdditionalControllers. Если вы прочтете текст ошибки, показанной на рисунке 13-10, вы увидите, что MVC фреймворк услужливо сообщает нам, какие классы он нашел.

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

Чтобы решить эту проблему, мы можем сказать MVC фреймворку отдавать предпочтение определенным пространствам имен при попытке выбрать имя класса контроллера, как показано в листинге 13-24.

Листинг 13-24: Указание порядка приоритета пространств имен
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
	public class RouteConfig
	{
		public static void RegisterRoutes(RouteCollection routes)
		{
			routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
			new
			{
				controller = "Home",
				action = "Index",
				id = UrlParameter.Optional
			},
			new[] { "URLsAndRoutes.AdditionalControllers" });
		}
	}
}

Мы выражаем пространства имен как массив строк, и в листинге мы сказали MVC смотреть сначала на пространство имен URLsAndRoutes.AdditionalControllers, прежде чем искать в другом месте.

Если подходящий контроллер не может быть найден в этом пространстве имен, тогда MVC по вернется к своему поведению по умолчанию и будет искать совпадение в доступных пространствах имен. Если вы запустите приложение после этого дополнения, вы увидите результат, продемонстрированный на рисунке 13-11. Здесь показан запрос для корневого URL, который переводится в запрос для метода действия Index контроллера Home. Это запрос был отправлен контроллеру, который мы определили в пространстве имен AdditionalControllers.

Рисунок 13-11: Приоритет контроллера в определенном пространстве имен

Пространства имен, добавленные в роут, обладают равным приоритетом. MVC не проверяет первое пространство имен, прежде чем перейти ко второму и так далее. Например, предположим, что мы добавили оба наших пространства имен в роут:

...
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
	new { controller = "Home", action = "Index",
		id = UrlParameter.Optional
	},
	new[] { "URLsAndRoutes.AdditionalControllers", "UrlsAndRoutes.Controllers" });
...

Мы увидели бы ту же ошибку, что показана на рисунке 13-10, потому что MVC пытается найти имя класса контроллера во всех пространствах имен, которые мы добавили к роуту. Если мы хотим отдать предпочтение одному контроллеру в одном пространстве имен, но и не хотим иметь проблемы с контроллерами в других пространствах имен, нам нужно создать несколько роутов, как показано в листинге 13-25.

Листинг 13-25: Использование нескольких роутов для выбора нужного пространства имен
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
	public class RouteConfig
	{
		public static void RegisterRoutes(RouteCollection routes)
		{
			routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}",
			new
			{
				controller = "Home",
				action = "Index",
				id = UrlParameter.Optional
			},
			new[] { "URLsAndRoutes.AdditionalControllers" });
			routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
			new
			{
				controller = "Home",
				action = "Index",
				id = UrlParameter.Optional
			},
			new[] { "URLsAndRoutes.Controllers" });
		}
	}
}

Наш первый роут применяется тогда, когда пользователь явно запрашивает URL, первым сегментом которого является Home, и будет нацелен на контроллер Home в папке AdditionalControllers. Все другие запросы, в том числе те, где не указан первый сегмент, будут обработаны контроллерами в папке Controllers.

Мы можем сказать MVC фреймворку искать только в пространствах имен, которые мы указали. Если соответствующий контроллер не может быть найден, то фреймворк не будет искать в другом месте. Листинг 13-26 показывает, как используется эта функция.

Листинг 13-26: Отключение не используемых пространств имен
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace UrlsAndRoutes
{
	public class RouteConfig
	{
		public static void RegisterRoutes(RouteCollection routes)
		{
			Route myRoute = routes.MapRoute("AddContollerRoute",
			"Home/{action}/{id}/{*catchall}",
			new
			{
				controller = "Home",
				action = "Index",
				id = UrlParameter.Optional
			},
			new[] { "URLsAndRoutes.AdditionalControllers" });
			myRoute.DataTokens["UseNamespaceFallback"] = false;
		}
	}
}

Метод MapRoute возвращает объект Route. Мы игнорировали это в предыдущих примерах, потому что нам не нужно было вносить любые изменения в роуты, которые были созданы. Чтобы отключить поиск контроллеров в других пространствах имен, мы берем объект Route и устанавливаем ключ UseNamespaceFallback в свойстве коллекции DataTokens на false.

Эта настройка будет передана компоненту, отвечающему за поиск контроллеров, который известен как фабрика контроллеров и который мы подробно обсудим в главе 17. Результат этого дополнения заключается в том, что запросы, которые не могут быть удовлетворены контроллером Home в папке AdditionalControllers, не сработают.