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

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

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

{

public class ProductController : Controller

{

private IProductRepository repository; public int PageSize = 4;

public ProductController(IProductRepository productRepository)

{

this.repository = productRepository;

}

public ViewResult List(string category, int page = 1)

{

ProductsListViewModel viewModel = new ProductsListViewModel

{

Products = repository.Products

.Where(p => category == null || p.Category == category)

.OrderBy(p => p.ProductID)

.Skip((page - 1) * PageSize)

.Take(PageSize), PagingInfo = new PagingInfo

{

CurrentPage = page, ItemsPerPage = PageSize,

TotalItems = repository.Products.Count() },

CurrentCategory = category

};

return View(viewModel);

}

}

}

Мы сделали три изменения в методе действия. Во-первых, мы добавили новый параметр под названием category. Этот параметр используется вторым изменением, которое представляет собой расширение запроса LINQ: теперь если category не содержит null, будут выбраны только те объекты Product, которые соответствуют свойству Category. Последнее изменение заключается в том, что мы установили значение свойства CurrentCategory, добавленного в класс

ProductsListViewModel. Однако, эти изменения означают, что значение PagingInfo.TotalItems

рассчитываются неправильно, что мы скоро исправим.

Модульный тест: обновление существующих модульных тестов

Мы изменили сигнатуру метода действия List, из-за чего некоторые из наших существующих модульных тестов не будут скомпилированы. Чтобы решить эту проблему, передайте null в качестве первого параметра в метод List в те модульные тесты, которые работают с контроллером. Например, в тесте Can_Paginate раздел действия станет таким:

...

[TestMethod]

public void Can_Paginate()

{

// Arrange

Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new Product[] {

new Product {ProductID = 1, Name = "P1"}, new Product {ProductID = 2, Name = "P2"}, new Product {ProductID = 3, Name = "P3"}, new Product {ProductID = 4, Name = "P4"}, new Product {ProductID = 5, Name = "P5"} }.AsQueryable());

// create a controller and make the page size 3 items ProductController controller = new ProductController(mock.Object);

191

controller.PageSize = 3; // Act

ProductsListViewModel result

=(ProductsListViewModel)controller.List(null, 2).Model;

//Assert

Product[] prodArray = result.Products.ToArray(); Assert.IsTrue(prodArray.Length == 2); Assert.AreEqual(prodArray[0].Name, "P4"); Assert.AreEqual(prodArray[1].Name, "P5");

}

...

Используя null, мы получаем все объекты Product, которые контроллер получает из хранилища, что полностью повторяет ситуацию, которая была раньше, пока мы не добавили новый параметр.

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

http://localhost:61576/?category=Soccer

Вы увидите только товары в категории Soccer, как показано на рисунке 8-1.

Рисунок 8-1: Использование строки запроса для фильтрации по категориям

192

Модульный тест: фильтрация категорий

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

...

[TestMethod]

public void Can_Filter_Products()

{

//Arrange

//- create the mock repository

Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new Product[] {

new Product {ProductID = 1, Name = "P1", Category = "Cat1"}, new Product {ProductID = 2, Name = "P2", Category = "Cat2"}, new Product {ProductID = 3, Name = "P3", Category = "Cat1"}, new Product {ProductID = 4, Name = "P4", Category = "Cat2"}, new Product {ProductID = 5, Name = "P5", Category = "Cat3"} }.AsQueryable());

//Arrange - create a controller and make the page size 3 items ProductController controller = new ProductController(mock.Object); controller.PageSize = 3;

//Action

Product[] result = ((ProductsListViewModel)controller.List("Cat2", 1).Model)

.Products.ToArray();

// Assert Assert.AreEqual(result.Length, 2);

Assert.IsTrue(result[0].Name == "P2" && result[0].Category == "Cat2"); Assert.IsTrue(result[1].Name == "P4" && result[1].Category == "Cat2");

}

...

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

Уточняем схему URL

Никому не нужны страшные URL вроде /?category=Soccer. Чтобы это исправить, мы вернемся к схеме маршрутизации и изменим ее таким образом, чтобы она лучше подходила нам (и нашим пользователям). Для реализации нашей новой схемы, измените метод RegisterRoutes в файле App_Start/RouteConfig.cs так, чтобы он соответствовал листингу 8-3, заменяя содержимое метода, который мы использовали в предыдущей главе.

Листинг 8-3: Новая схема URL

using System;

using System.Collections.Generic; using System.Linq;

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

using System.Web.Routing; namespace SportsStore.WebUI

{

public class RouteConfig

{

193

public static void RegisterRoutes(RouteCollection routes)

{

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(null,

"", new

{

controller = "Product", action = "List", category = (string)null, page = 1

}

);

routes.MapRoute(null,

"Page{page}",

new { controller = "Product", action = "List", category = (string)null }, new { page = @"\d+" }

);

routes.MapRoute(null,

"{category}",

new { controller = "Product", action = "List", page = 1 } );

routes.MapRoute(null,

"{category}/Page{page}",

new { controller = "Product", action = "List" }, new { page = @"\d+" }

);

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

}

}

}

Внимание

Важно добавлять новые роуты из листинга 8-3 по очереди, как они показаны в листинге. Роуты применяются в том порядке, в котором они определены, и если вы его измените, то получите другой результат.

Таблица 8-1 описывает схему URL, которую представляют эти роуты. Мы расскажем о системе маршрутизации подробно в главе 13.

Таблица 8-1: Информация о роутах

URL Результат

/Выводит список товаров из всех категорий для первой страницы.

Выводит список товаров из всех категорий для указанной страницы (в данном

/Page2

случае страницы 2).

Показывает первую страницу товаров из определенной категории (в данном случае

/Soccer

категории Soccer).

Показывает указанную страницу (в данном случае 2) товаров из указанной

/Soccer/Page2

категории (в данном случае Soccer).

/Anything/ElseВызывает метод действия Else контроллера Anything.

194

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

Заметка

Мы покажем, как создавать модульные тесты для конфигурации маршрутизации в главе 13.

Метод Url.Action является наиболее удобным способом генерации исходящих ссылок. В предыдущей главе мы использовали этот вспомогательный метод в представлении List.cshtml, чтобы отображать ссылки на страницы. Теперь, когда мы добавили поддержку фильтрации по категориям, мы должны вернуться к нему и передать эту информацию, как показано в листинге 8-4.

Листинг 8-4: Добавляем информацию о категории к ссылкам на страницы

@model SportsStore.WebUI.Models.ProductsListViewModel

@{

ViewBag.Title = "Products";

}

@foreach (var p in Model.Products)

{

Html.RenderPartial("ProductSummary", p);

}

<div class="pager">

@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x, category = Model.CurrentCategory }))

</div>

До этого изменения ссылки на страницы выглядели так:

http://<myserver>:<port>/Page2

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

http://<myserver>:<port>/Chess/Page2

Когда пользователь переходит по такой ссылке, текущая категория будут передана в метод действия List, и фильтрация будет сохранена. После внесения этих изменений, вы можете перейти по таким ссылкам, как /Chess или /Soccer, и увидите, что ссылка внизу страницы включает в себя правильную категорию.

Создаем меню навигации по категориям

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

В ASP.NET MVC Framework есть концепция дочерних действий, которые идеально подходят для создания таких элементов, как элемент управления навигацией многократного использования.

195

Дочернее действие полагается на вспомогательный метод HTML под названием RenderAction, который позволяет включить вывод из произвольного метода действия в текущее представление. В этом случае мы можем создать новый контроллер (назовем его NavController) с методом действия (в данном случае Menu), который визуализирует меню навигации и внедряет вывод из данного метода в макет.

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

Создаем контроллер навигации

Щелкните правой кнопкой мыши папку Controllers в проекте SportsStore.WebUI и выберите пункт Add Controller из контекстного меню. Назовите новый контроллер NavController, выберите опцию Empty MVC controller из меню Template и нажмите кнопку Add to create the class.

Удалите метод Index, который Visual Studio создает по умолчанию, и добавьте метод действия Menu, показанный в листинге 8-5.

Листинг 8-5: Метод действия Menu

using System;

using System.Collections.Generic; using System.Linq;

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

namespace SportsStore.WebUI.Controllers

{

public class NavController : Controller

{

public string Menu()

{

return "Hello from NavController";

}

}

}

Этот метод возвращает статическую строку сообщения, но, пока мы интегрируем дочернее действие в приложение, этого для нас достаточно. Мы хотим, чтобы список категории появлялся на всех страницах, так что мы собирается визуализировать дочернее действие в макете, а не в определенном представлении. Отредактируйте файл Views/Shared/_Layout.cshtml так, чтобы он вызывал вспомогательный метод RenderAction, как показано в листинге 8-6.

Листинг 8-6: Добавляем вызов к RenderAction в макет Razor

<!DOCTYPE html> <html>

<head>

<meta charset="utf-8" />

<meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title>

<link href="~/Content/Site.css" type="text/css" rel="stylesheet" /> </head>

<body>

<div id="header">

<div class="title">SPORTS STORE</div> </div>

<div id="categories">

196

@{ Html.RenderAction("Menu", "Nav"); }

</div>

<div id="content"> @RenderBody()

</div>

</body>

</html>

Мы удалили замещающий текст, который добавили в главе 7, и заменили его на вызов метода RenderAction. Параметрами этого метода являются метод действия, который мы хотим вызвать (Menu), и контроллер, который мы хотим использовать (Nav).

Примечание

Метод RenderAction записывает свое содержание непосредственно в поток ответа, как и метод RenderPartial, о котором мы упоминали в главе 5. Это означает, что метод возвращает void, и поэтому его нельзя использовать с регулярным тегом Razor @. Вместо этого мы должны заключить вызов метода в блок кода Razor (и не забудьте поставить точку с запятой в конце оператора). Если вам не нравится синтаксис блока кода, можно использовать метод Action в качестве альтернативы.

Если вы запустите приложение, то увидите, что вывод метода действия Menu включен в каждую страницу, как показано на рисунке 8-2.

Рисунок 8-2: Отображение результата метода действия Menu

Создаем списки категорий

Теперь мы можем вернуться к контроллеру и создать реальный набор категорий. Мы не хотим генерировать категории URL в контроллере. Для этого мы собираемся использовать вспомогательный метод в представлении. В методе действия Menu нужно только создать список категорий, что мы сделали в листинге 8-7.

Листинг 8-7: Реализация метода Menu

using SportsStore.Domain.Abstract; using System;

using System.Collections.Generic; using System.Linq;

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

197

namespace SportsStore.WebUI.Controllers

{

public class NavController : Controller

{

private IProductRepository repository;

public NavController(IProductRepository repo)

{

repository = repo;

}

public PartialViewResult Menu()

{

IEnumerable<string> categories = repository.Products

.Select(x => x.Category)

.Distinct()

.OrderBy(x => x);

return PartialView(categories);

}

}

}

Сначала мы добавляем конструктор, который принимает реализацию IProductRepository как аргумент - после создания экземпляра контроллера ее предоставит Ninject, используя привязки, которые мы создали в предыдущей главе.

Далее мы изменяем метод действия Menu, который теперь использует запрос LINQ, чтобы получить список категорий из хранилища и передать их в представление. Обратите внимание, что, так как в этом контроллере мы работаем с частичным представлением, здесь мы вызываем метод PartialView, и что результатом является объект PartialViewResult.

Модульный тест: создание списка категорий

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

...

[TestMethod]

public void Can_Create_Categories()

{

//Arrange

//- create the mock repository

Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new Product[] {

new Product {ProductID = 1, Name = "P1", Category = "Apples"}, new Product {ProductID = 2, Name = "P2", Category = "Apples"}, new Product {ProductID = 3, Name = "P3", Category = "Plums"}, new Product {ProductID = 4, Name = "P4", Category = "Oranges"}, }.AsQueryable());

// Arrange - create the controller

NavController target = new NavController(mock.Object);

// Act = get the set of categories

string[] results = ((IEnumerable<string>)target.Menu().Model).ToArray();

// Assert Assert.AreEqual(results.Length, 3);

198

Assert.AreEqual(results[0], "Apples"); Assert.AreEqual(results[1], "Oranges"); Assert.AreEqual(results[2], "Plums");

}

...

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

Создаем частичное представление

Так как список категорий является всего лишь частью страницы, имеет смысл создать частичное представление для метода действия Menu. Кликните правой кнопкой мыши метод Menu в классе NavController и выберите Add View из контекстного меню.

Оставьте представлению имя Menu, отметьте флажком опцию Сreate a strongly typed view, и введите IEnumerable<string> как тип класса модели, как показано на рисунке 8-3.

Рисунок 8-3 : Создаем частичное представление Menu

Отметьте флажком опцию Create as a partial view и нажмите кнопку Add, чтобы создать представление. Измените содержание представления так, чтобы оно соответствовало листингу 8-8.

Листинг 8-8: Частичное представление Menu

@model IEnumerable<string>

@Html.ActionLink("Home", "List", "Product")

@foreach (var link in Model)

{

@Html.RouteLink(link, new

{

controller = "Product", action = "List", category = link,

page = 1 })

}

199

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

Затем мы перечислили имена категорий и создали ссылки на каждую из них с помощью метода RouteLink. Он похож на ActionLink, но позволяет нам поставлять набор пар имя/значение, которые учитываются при генерации URL на основе конфигурации маршрутизации. Не беспокойтесь, если вы еще ничего не знаете о маршрутизации – мы подробно объясним все в главе 13.

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

8-9, в конец файла Content/Site.css в проекте SportsStore.WebUI.

Листинг 8-9: CSS для ссылок на категории

...

DIV#categories A

{

font: bold 1.1em "Arial Narrow","Franklin Gothic Medium",Arial; display: block; text-decoration: none; padding: .6em; color: Black;

border-bottom: 1px solid silver;

}

DIV#categories A.selected { background-color: #666; color: White; } DIV#categories A:hover { background-color: #CCC; }

DIV#categories A.selected:hover { background-color: #666; }

...

Если вы запустите приложение, то увидите ссылки на категории, как показано на рисунке 8-4. Если вы кликните по категории, список элементов обновится и будет отображать только элементы из выбранной категории.

Рисунок 8-4: Ссылки на категории

200

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