ASP.NET MVC 4
Адам Фриман
Добавление элементов навигации
Приложение SportsStore будет намного более удобным, если мы позволим пользователям просматривать каталог по категориям. Мы выполним эту работу в три этапа:
- Расширим модель действия
List
в классеProductController
так, чтобы она могла фильтровать объектыProduct
в хранилище. - Пересмотрим и улучшим нашу схему URL и исправим стратегию изменения маршрута.
- Создадим список категорий, который будет размещен в боковой панели сайта, подсветку текущей категории и ссылки на другие категории.
Фильтрация списка товаров
Мы начнем с расширения класса модели представления, ProductsListViewModel
, который мы добавили в проект SportsStore.WebUI
в предыдущей главе. Мы должны обеспечить связь выбранной на данный момент категории с представлением, чтобы визуализировать боковую панель, так что начнем с этого. Изменения показаны в листинге 8-1.
Листинг 8-1: Расширяем класс ProductsListViewModel
using System.Collections.Generic;
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Models
{
public class ProductsListViewModel
{
public IEnumerable<Product> Products { get; set; }
public PagingInfo PagingInfo { get; set; }
public string CurrentCategory { get; set; }
}
}
Мы добавили новое свойство под названием CurrentCategory
. Далее мы обновим класс ProductController
, чтобы метод действия List
отфильтровывал объекты Product
по категориям и использовал новое свойство, которое мы добавили к модели представления, чтобы указывать выбранную категорию. Изменения показаны в листинге 8-2.
Листинг 8-2: Добавляем поддержку категорий в метод действия List
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers
{
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); 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: Использование строки запроса для фильтрации по категориям
Модульный тест: фильтрация категорий
Нам нужно тщательно протестировать функцию фильтрации по категориям, чтобы гарантировать, что фильтрация проводится корректно, и мы получаем только продукты из указанной категории. Вот тест:
... [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
{
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 ). |
/Soccer/Page2 |
Показывает указанную страницу (в данном случае 2) товаров из указанной категории (в данном случае Soccer ). |
/Anything/Else |
Вызывает метод действия Else контроллера Anything . |
Система маршрутизации 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 есть концепция дочерних действий, которые идеально подходят для создания таких элементов, как элемент управления навигацией многократного использования. Дочернее действие полагается на вспомогательный метод 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">
@{ 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;
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); 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
})
}
Мы добавили ссылку под названием 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: Ссылки на категории
Подсветка текущей категории
Сейчас мы не подсказываем пользователям, какую категорию они просматривают. Хотя пользователь может понять это по элементам в списке, мы все же предпочитаем обеспечить надежный визуальный индикатор.
Для этого мы могли бы создать модель представления, которая содержит список категорий и выбранную категорию; в самом деле, именно это мы бы обычно и сделали. Но для разнообразия мы будем использовать ViewBag
, о которой говорилось в главе 2. Этот объект позволяет передавать данные из контроллера в представление, не используя модель представления. Листинг 8-10 показывает изменения в методе действия Menu
контроллера Nav
.
Листинг 8-10: Использование ViewBag
using SportsStore.Domain.Abstract;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace SportsStore.WebUI.Controllers
{
public class NavController : Controller
{
private IProductRepository repository;
public NavController(IProductRepository repo)
{
repository = repo;
}
public PartialViewResult Menu(string category = null)
{
ViewBag.SelectedCategory = category;
IEnumerable<string> categories = repository.Products
.Select(x => x.Category)
.Distinct()
.OrderBy(x => x);
return PartialView(categories);
}
}
}
Мы добавили в метод действия Menu
параметр под названием category
. Значение этого параметра будет предоставлено автоматически конфигурацией маршрутизации. В теле метода мы динамически создали свойство SelectedCategory
в объекте ViewBag
и приравняли его значение к значению параметра category
. Как мы уже объясняли в главе 2, ViewBag
является динамическим объектом, и мы создаем новые свойства, просто устанавливая для них значения.
Модульный тест: Указание выбранной категории
Чтобы проверить, что метод действия
Menu
правильно добавляет информацию о выбранной категории, проверим в модульном тесте значение свойстваViewBag
, которое доступно через классViewResult
. Вот этот тест:[TestMethod] public void Indicates_Selected_Category() { // 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 = 4, Name = "P2", Category = "Oranges"}, }.AsQueryable()); // Arrange - create the controller NavController target = new NavController(mock.Object); // Arrange - define the category to selected string categoryToSelect = "Apples"; // Action string result = target.Menu(categoryToSelect).ViewBag.SelectedCategory; // Assert Assert.AreEqual(categoryToSelect, result); }
Обратите внимание, что мы не должны приводить значение свойства из
ViewBag
. Это одно из преимуществ использования объектаViewBag
передViewData
.
Теперь, когда мы предоставляем информацию о выбранной категории, можно обновить представление и добавить класс CSS к якорному HTML-элементу, который представляет выбранную категорию. Листинг 8-11 показывает изменения в частичном представлении Menu.cshtml
.
Листинг 8-11: Подсветка выбранной категории
@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
},
new {
@class = link == ViewBag.SelectedCategory ? "selected" : null
})
}
Мы воспользовались перегруженной версией метода RouteLink
, что позволяет нам предоставить объект, свойства которого будут добавлены в якорный HTML-элемент как атрибуты. В этом случае ссылке, которая представляет выбранную категорию, присваивается CSS-класс selected
.
Примечание
Обратите внимание, что мы использовали
@class
в анонимном объекте, который мы передали как новый параметр в вспомогательный методRouteLink
. Это не тег Razor. Мы используем стандартную функцию языка C#, чтобы избежать конфликта между ключевым словом HTMLclass
(используется для присвоения стиля CSS к элементу) и того же слова C# (используется для обозначения класса .NET). Символ@
позволяет нам использовать зарезервированные ключевые слова C#, не запутывая компилятор. Если мы просто вызовем параметрclass
(без@
), компилятор будет считать, что мы определяем новый тип C#. Когда мы будем использовать символ@
, компилятор поймет, что мы хотим создать параметр в анонимном типе под названиемclass
, и мы получим нужный нам результат.
При запуске приложения будет виден эффект подсветки категории, которую вы также можете видеть на рисунке 8-5.
Рисунок 8-5: Подсветка выбранной категории
Корректируем количество страниц
Нам нужно исправить ссылки на страницы, чтобы они работали правильно, когда выбрана категория. На данный момент количество ссылок на страницы определяется общим количеством товаров в хранилище, а не количеством
товаров в выбранной категории. Это означает, что клиент может кликнуть по ссылке на страницу 2 в категории Chess
и попадет на пустую страницу, потому что для ее заполнения недостаточно товаров. Вы можете увидеть, как это выглядит, на рисунке 8-6.
Рисунок 8-6: Отображение неправильных ссылок на страницы, когда выбрана категория
Мы можем исправить это, обновив метод действия List
в ProductController
так, чтобы к информации о нумерации страниц были добавлены сведения о категории. Необходимые изменения показаны в листинге 8-12.
Листинг 8-12: Объединяем данные о нумерации страниц и категории
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 = category == null ?
repository.Products.Count() :
repository.Products.Where(e => e.Category == category).Count()
},
CurrentCategory = category
};
return View(viewModel);
}
Если категория выбрана, мы возвращаем количество товаров в этой категории, если нет, мы возвращаем общее количество товаров.
Модульный тест: подсчет товаров по категориям
Тест, с помощью которого мы проверим текущий подсчет товаров в различных категориях, очень простой: мы создадим имитированное хранилище, которое содержит известное количество данных в различных категориях, а затем вызовем метод действия
List
, запрашивая каждую категорию по очереди. Вот модульный тест:[TestMethod] public void Generate_Category_Specific_Product_Count() { // 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 target = new ProductController(mock.Object); target.PageSize = 3; // Action - test the product counts for different categories int res1 = ((ProductsListViewModel)target .List("Cat1").Model).PagingInfo.TotalItems; int res2 = ((ProductsListViewModel)target .List("Cat2").Model).PagingInfo.TotalItems; int res3 = ((ProductsListViewModel)target .List("Cat3").Model).PagingInfo.TotalItems; int resAll = ((ProductsListViewModel)target .List(null).Model).PagingInfo.TotalItems; // Assert Assert.AreEqual(res1, 2); Assert.AreEqual(res2, 2); Assert.AreEqual(res3, 1); Assert.AreEqual(resAll, 5); }
Обратите внимание, что мы также вызываем метод
List
, не указывая категорию, чтобы убедиться, мы получаем правильный подсчет всех товаров.
Теперь, когда мы просматриваем какую-либо категорию, ссылки в нижней части страницы отражают правильное количество товаров в ней, как показано на рисунке 8-7.
Рисунок 8-7: Отображается правильное количество страниц в категории