Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

.pdf
Скачиваний:
25
Добавлен:
19.03.2016
Размер:
17.66 Mб
Скачать

В листинге 13-7 показано, как создать роут, используя пример URL паттерна из предыдущего раздела, в методе RegisterRoutes файла RouteConfig.cs (Мы удалили из метода все другие выражения, чтобы можно было сосредоточиться на примере).

Листинг 13-7: Регистрация роута

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 = new Route("{controller}/{action}", new MvcRouteHandler()); routes.Add("MyRoute", myRoute);

}

}

}

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

Совет

Именование маршрутов не является обязательным, и есть мнение, что таким образом мы жертвуем чистым разделением понятий, которое в ином случае может предоставить роутинг. Мы не сильно переживаем по поводу именования, но мы объясним, почему это может быть проблемой, в разделе "Создание URL из конкретного роута" далее в этой главе.

Более удобным способом регистрации роутов является использование метода MapRoute, определенного в классе RouteCollection. Листинг 13-8 показывает, как мы можем использовать этот метод, чтобы зарегистрировать роут. Результат этого такой же, как и в предыдущем примере, но синтаксис тут чище.

Листинг 13-8: Регистрация роута при помощи метода MapRoute

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)

{

311

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

}

}

}

Такой подход является чуть более компактным, в основном потому, что мы не должны создавать экземпляр класса MvcRouteHandler. Метод MapRoute предназначен исключительно для использования с MVC приложениями. Приложения ASP.NET Web Forms могут использовать метод

MapPageRoute, также определяемый в классе RouteCollection.

Использование простого роута

Вы можете увидеть результат изменений, которые мы сделали в маршрутизации, запустив приложение. Когда браузер пытается перейти к корневому URL приложения, вы увидите сообщение об ошибке, но если перейти к роуту, который соответствует нашему паттерну {controller}/{action}, вы увидите результат, показанный на рисунке 13-3, где проиллюстрирован

переход к /Admin/Index.

Рисунок 13-3: Навигация при помощи простого роута

Наш простой роут не говорит MVC фреймворку, как реагировать на запросы для корневого URL, и поддерживает только один очень конкретный URL паттерн. Мы сейчас временно сделали шаг назад от функционала, который Visual Studio добавляет в файл RouteConfig.cs при создании MVC проекта. Мы покажем вам, как создавать более сложные паттерны и роуты, в остальной части этой главы.

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

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

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

Для проверки роутов нам нужно использовать mock-технологию для трех классов из MVC

фреймворка: HttpRequestBase, HttpContextBase и HttpResponseBase (последний класс требуется

312

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

HttpContextBase:

using System;

using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Web;

using System.Web.Routing; using Moq;

using System.Reflection; namespace UrlsAndRoutes.Tests

{

[TestClass]

public class RouteTests

{

private HttpContextBase CreateHttpContext(string targetUrl = null, string httpMethod = "GET")

{

// создать mock-запрос

Mock<HttpRequestBase> mockRequest = new Mock<HttpRequestBase>(); mockRequest.Setup(m => m.AppRelativeCurrentExecutionFilePath)

.Returns(targetUrl);

mockRequest.Setup(m => m.HttpMethod).Returns(httpMethod);

// создать mock-response

Mock<HttpResponseBase> mockResponse = new Mock<HttpResponseBase>(); mockResponse.Setup(m => m.ApplyAppPathModifier(It.IsAny<string>()))

.Returns<string>(s => s);

//создать mock-контекст, используя запрос и ответ

Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(); mockContext.Setup(m => m.Request).Returns(mockRequest.Object); mockContext.Setup(m => m.Response).Returns(mockResponse.Object);

//вернуть mock-контекст

return mockContext.Object;

}

}

}

Здесь все проще, чем кажется. Мы раскрываем URL, который мы хотим проверить, используя свойство AppRelativeCurrentExecutionFilePath класса HttpRequestBase, и раскрываем HttpRequestBase благодаря свойству Request mock-класса HttpContextBase. Наш следующий вспомогательный метод позволяет нам протестировать роут:

...

private void TestRouteMatch(string url, string controller, string action, object routeProperties = null, string httpMethod = "GET")

{

// Arrange

RouteCollection routes = new RouteCollection(); RouteConfig.RegisterRoutes(routes);

//Act - обрабатывает роут

RouteData result

=routes.GetRouteData(CreateHttpContext(url, httpMethod));

//Assert

Assert.IsNotNull(result); Assert.IsTrue(TestIncomingRouteResult(result, controller,

action, routeProperties));

}

...

313

Параметры этого метода позволяют задавать URL для тестирования, ожидаемые значения сегментных переменных controller и action, а также object, который содержит ожидаемые значения для любых дополнительных переменных, которые мы определили. Мы покажем вам, как создавать такие переменные, далее в этой главе. Мы также определили параметр для HTTP метода, которые мы объясним в разделе "Ограничение роутов".

Метод TestRouteMatch полагается на другой метод, TestIncomingRouteResult, чтобы сравнить результаты, полученные от системы маршрутизации, с ожидаемыми значениями сегментных переменных. Этот метод использует .NET рефлексию, поэтому мы можем использовать анонимный тип, чтобы выразить любые дополнительные переменные сегмента. Не волнуйтесь, если этот метод вам непонятен, так как он нужен, чтобы сделать тестирование более удобным, но не является обязательным требованием для понимания MVC. Вот метод TestIncomingRouteResult:

private bool TestIncomingRouteResult(RouteData routeResult, string controller, string action, object propertySet = null)

{

Func<object, object, bool> valCompare = (v1, v2) => { return StringComparer.InvariantCultureIgnoreCase

.Compare(v1, v2) == 0;

};

bool result = valCompare(routeResult.Values["controller"], controller) && valCompare(routeResult.Values["action"], action);

if (propertySet != null) {

PropertyInfo[] propInfo = propertySet.GetType().GetProperties(); foreach (PropertyInfo pi in propInfo) {

if (!(routeResult.Values.ContainsKey(pi.Name) && valCompare(routeResult.Values[pi.Name],

pi.GetValue(propertySet, null))))

{

result = false; break;

}

}

}

return result;

}

Нам также нужен метод, чтобы проверить, что URL не работает. Как вы увидите, это может быть важной частью определения URL схемы.

...

private void TestRouteFail(string url)

{

// Arrange

RouteCollection routes = new RouteCollection(); RouteConfig.RegisterRoutes(routes);

// Act - обработка роута

RouteData result = routes.GetRouteData(CreateHttpContext(url)); // Assert

Assert.IsTrue(result == null || result.Route == null);

}

...

TestRouteMatch и TestRouteFail содержат вызов метода Assert, который генерирует исключение, если утверждение не выполняется. Поскольку в C# исключения передаются вверх по стеку вызовов, мы можем создавать простые тестовые методы, которые проверяют набор URL-адресов и получают нужное нам тестовое поведение. Вот тестовый метод, проверяющий роут, который мы определили в листинге 13-8:

314

...

[TestMethod]

public void TestIncomingRoutes() {

//проверка на URL, который мы надеемся получить

TestRouteMatch("~/Admin/Index", "Admin", "Index");

//проверка на то, что значения были получены из сегментов

TestRouteMatch("~/One/Two", "One", "Two");

//гарантия того, что слишком много или слишком мало сегментов не подходят

TestRouteFail("~/Admin/Index/Segment");

TestRouteFail("~/Admin");

}

...

Этот тест использует метод TestRouteMatch для проверки URL, который мы ожидаем. Он также проверяет URL в том же формате, чтобы убедиться, что значения controller и action получены надлежащим образом с помощью URL сегментов. Мы также используем метод TestRouteFail, чтобы убедиться, что наше приложение не будет принимать URL, которые имеют другое количество сегментов. При тестировании мы должны поставить перед URL тильду (~), потому что таким образом ASP.NET Framework представляет URL системе маршрутизации.

Обратите внимание, что нам не нужно в тестовых методах определять роуты. Это потому что мы загружаем их напрямую с помощью метода RegisterRoutes в классе RouteConfig.

Определение значений по умолчанию

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

Мы объяснили ранее, что URL паттерны носят консервативный характер в том, что они будут соответствовать только URL с указанным количеством сегментов. Мы также сказали, что это поведение по умолчанию. Один из способов изменить это поведение – это использовать значения по умолчанию. Значение по умолчанию применяется тогда, когда URL не содержит сегмент, который может соответствовать значению. В листинге 13-9 показан пример роута, который содержит значение по умолчанию.

Примечание

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

RouteConfig.

Листинг 13-9: Добавление значения по умолчанию в роут

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

315

{

public static void RegisterRoutes(RouteCollection routes)

{

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

}

}

}

Значения по умолчанию поставляются как свойства в анонимном типе. В листинге 13-9 мы предоставили значение по умолчанию Index для переменной action. Этот роут будет соответствовать всем двухсегментным URL, как это было ранее. Например, если запрашивается URL http://mydomain.com/Home/Index, роут будет извлекать Home как значение для controller и Index

в качестве значения для action.

Но теперь, когда мы указали значение по умолчанию для сегмента action, роут будет также соответствовать односегментному URL. При обработке односегментного URL система маршрутизации извлекает значение controller из одного URL сегмента и использует значение по умолчанию для переменной action. Таким образом, мы можем запросить URL http://mydomain.com/Home и вызвать метод действия Index контроллера Home.

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

Листинг 13-10: Добавление в роут значений по умолчанию для действия и контроллера

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}", new { controller = "Home", action = "Index" });

}

}

}

Поскольку мы предоставили значения по умолчанию для переменных controller и action, мы создали роут, который будет соответствовать URL, имеющим ноль, один или два сегмента, как показано в таблице 13-2.

Таблица 13-2: Подходящие URL

 

 

 

 

 

Число сегментов

 

Пример

Соответствие

 

 

 

 

 

 

 

 

0

 

mydomain.com

controller = Home, action = Index

 

 

 

 

316

Число сегментов Пример

1

 

mydomain.com/Customer

 

 

 

Соответствие

controller = Customer, action = Index

2

 

mydomain.com/Customer/List

controller = Customer, action = List

 

 

 

 

 

 

 

3

 

mydomain.com/Customer/List/All

Нет соответствия: слишком много сегментов

 

 

 

 

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

13-4.

Рисунок 13-4: Использование значений по умолчанию, чтобы расширить сферу использования роута

Юнит тестирование: значения по умолчанию

Нам не нужно предпринимать никаких специальных действий, если мы используем вспомогательные методы для проверки роутов, которые определяют значения по умолчанию. Вот мы сделали изменения в тестовом методе TestIncomingRoutes в файле RouteTests.cs для роута, который мы определили в листинге 13-10:

...

[TestMethod]

public void TestIncomingRoutes() { TestRouteMatch("~/", "Home", "Index"); TestRouteMatch("~/Customer", "Customer", "Index");

TestRouteMatch("~/Customer/List", "Customer", "List"); TestRouteFail("~/Customer/List/All");

}

...

Единственное замечание состоит в том, что мы должны указать URL по умолчанию как ~/, так как таким образом ASP.NET представляет URL системе маршрутизации. Если указать пустую строку (""), которую мы использовали для определения роута, или /, система маршрутизации выбросит исключение, и тест будет провален.

317

Использование статических URL сегментов

Не все сегменты в URL паттерне должны быть переменными. Вы также можете создавать паттерны, которые имеют статические сегменты. Предположим, нам нужно соответствие с URL вроде этого для поддержки URL, которые начинаются с Public:

http://mydomain.com/Public/Home/Index

Мы можем сделать это с помощью такого паттерна, как тот, что показан в листинге 13-11.

Листинг 13-11: 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}", new { controller = "Home", action = "Index" }); routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });

}

}

}

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

Мы также можем создавать URL паттерны, которые имеют сегменты, содержащие как статические элементы, так и элементы переменных, как показанный в листинге 13-12.

Листинг 13-12: 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("", "X{controller}/{action}"); routes.MapRoute("MyRoute", "{controller}/{action}", new { controller = "Home", action = "Index" }); routes.MapRoute("", "Public/{controller}/{action}", new { controller = "Home", action = "Index" });

318

}

}

}

Паттерн в этом роуте подходит любому двухсегментному URL, где первый сегмент начинается на букву X. Значение для controller взято из первого сегмента, за исключением X. Значение action берется из второго сегмента. Вы можете увидеть результат использования этого роута, если запустите приложение и перейдите по /XHome/Index. Результат показан на рисунке 13-5.

Рисунок 13-5: Статические элементы и элементы переменных в одном сегменте

Порядок роутов

В листинге 13-12 мы определили новый роут и разместили его перед всеми другими в методе RegisterRoutes. Мы сделали это, потому что роуты применяются в том порядке, в котором они появляются в объекте RouteCollection. Метод MapRoute добавляет роут в конец коллекции, что обозначает, что роуты обычно применяются в том порядке, в котором их добавляют. Мы говорим, "обычно", потому что есть методы, которые позволяют нам вставлять роуты в определенные места. Мы, как правило, не используем эти методы, потому что применение роутов в том порядке, в котором они определены, упрощает понимание маршрутизации для приложения. Система маршрутизации пытается сопоставить входящий URL с URL паттерном роута, который был определен первым, и переходит к следующему роуту, только если совпадения нет. И так система проходит по очереди по роутам, пока не будет найдено совпадение или пока не закончатся роуты. Поэтому сперва мы должны определять самые конкретные роуты. Роут, который мы добавили в листинге 13-12, является более конкретным, чем роут, который за ним следует. Предположим, что мы отменили порядок роутов, например:

...

routes.MapRoute("MyRoute", "{controller}/{action}",

new { controller = "Home", action = "Index" });

routes.MapRoute("", "X{controller}/{action}");

...

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

http://mydomain.com/XHome/Index

будут нацелены на контроллер XHome, который не существует, и это приведет к ошибке 404—Not Found, которая будет отправлена пользователю.

319

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

Мы можем объединять статические URL сегменты и значения по умолчанию для создания алиаса для конкретного URL. Это может быть полезно, если вы опубликовали URL схему публично, и с ней работает пользователь. Если вы в этой ситуации вы проводите рефакторинг приложения, вы должны сохранить прежний формат URL так, чтобы любые избранные URL или макро скрипты, которые создал пользователь, продолжали работать. Давайте представим, что у нас был контроллер Shop, который в настоящее время заменен контроллером Home. В листинге 13-13 показано, как мы можем создать роут, чтобы сохранить старую URL схему.

Листинг 13-13: Смешивание статических 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("ShopSchema", "Shop/{action}", new { controller = "Home" }); routes.MapRoute("", "X{controller}/{action}");

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

}

}

}

Роут, который мы добавили, соответствует любому двухсегментному URL, где первым сегментом является Shop. Значение action берется из второго сегмента URL. URL паттерн не содержит сегмента для controller, поэтому используется данное нами значение по умолчанию. Это означает, что запрос для действии контроллера Shop переводится в запрос для контроллера Home. Вы можете увидеть результат использования этого роута, если запустите приложение и перейдете по URL /Shop/Index. Как показано на рисунке 13-6, роут, который мы добавили, заставляет MVC нацеливаться на действие Index в контроллере Home.

Рисунок 13-6: Создание алиаса для сохранения URL схемы

320

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]