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

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

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

Подсветка текущей категории

Сейчас мы не подсказываем пользователям, какую категорию они просматривают. Хотя пользователь может понять это по элементам в списке, мы все же предпочитаем обеспечить надежный визуальный индикатор. Для этого мы могли бы создать модель представления, которая содержит список категорий и выбранную категорию; в самом деле, именно это мы бы обычно и сделали. Но для разнообразия мы будем использовать 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"},

201

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#, чтобы избежать конфликта между ключевым словом HTML class (используется для присвоения стиля CSS к элементу) и того же слова C# (используется для обозначения класса .NET). Символ @

позволяет нам использовать зарезервированные ключевые слова C#, не запутывая компилятор. Если мы просто вызовем параметр class (без @), компилятор будет считать, что мы определяем новый тип C#. Когда мы будем использовать символ @,

компилятор поймет, что мы хотим создать параметр в анонимном типе под названием class, и мы получим нужный нам результат.

202

При запуске приложения будет виден эффект подсветки категории, которую вы также можете видеть на рисунке 8-5.

Рисунок 8-5: Подсветка выбранной категории

Корректируем количество страниц

Нам нужно исправить ссылки на страницы, чтобы они работали правильно, когда выбрана категория. На данный момент количество ссылок на страницы определяется общим количеством товаров в хранилище, а не количеством товаров в выбранной категории. Это означает, что клиент может кликнуть по ссылке на страницу 2 в категории Chess и попадет на пустую страницу, потому что для ее заполнения недостаточно товаров. Вы можете увидеть, как это выглядит, на рисунке 8-6.

203

Рисунок 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);

}

Если категория выбрана, мы возвращаем количество товаров в этой категории, если нет, мы возвращаем общее количество товаров.

204

Модульный тест: подсчет товаров по категориям

Тест, с помощью которого мы проверим текущий подсчет товаров в различных категориях, очень простой: мы создадим имитированное хранилище, которое содержит известное количество данных в различных категориях, а затем вызовем метод действия 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.

205

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

Создание корзины покупателя

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

Рисунок 8-8: Базовый поток корзины

Кнопка Add to cart будет отображаться рядом с каждым из продуктов в нашем каталоге. После нажатия этой кнопки будет отображена информация о товарах, которые клиент уже выбрал, и их общая стоимость. В этот момент пользователь может нажать кнопку Continue shopping, чтобы вернуться в каталог товаров, или нажать кнопку Checkout now, чтобы выполнить заказ и завершить сессию.

206

Определяем сущность корзины

Корзина является частью бизнес-логики нашего приложения, так что имеет смысл представить ее, создав сущность в нашей доменной модели. Добавьте файл класса под названием Cart в папку Entities проекта SportsStore.Domain и определите классы, показанные в листинге 8-13.

Листинг 8-13: Классы Cart и CartLine

using System;

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

using System.Text;

using System.Threading.Tasks; namespace SportsStore.Domain.Entities

{

public class Cart

{

private List<CartLine> lineCollection = new List<CartLine>();

public void AddItem(Product product, int quantity)

{

CartLine line = lineCollection

.Where(p => p.Product.ProductID == product.ProductID)

.FirstOrDefault();

if (line == null)

{

lineCollection.Add(new CartLine

{

Product = product, Quantity = quantity

});

}

else

{

line.Quantity += quantity;

}

}

public void RemoveLine(Product product)

{

lineCollection.RemoveAll(l => l.Product.ProductID == product.ProductID);

}

public decimal ComputeTotalValue()

{

return lineCollection.Sum(e => e.Product.Price * e.Quantity);

}

public void Clear()

{

lineCollection.Clear();

}

public IEnumerable<CartLine> Lines

{

get { return lineCollection; }

}

}

public class CartLine

{

public Product Product { get; set; } public int Quantity { get; set; }

}

}

207

Класс Cart использует CartLine, определенный в том же файле, чтобы представлять товар, выбранный покупателем, и количество данного товара. Мы определили методы, которые позволяют добавлять товар в корзину, удалять ранее добавленный товар, рассчитать общую стоимость товаров в корзине и очистить корзину, удалив все выбранное. Мы также предоставили свойство, которое дает доступ к содержимому корзины с помощью IEnumerble<CartLine>. Это все очень простые вещи, которые легко реализовать с помощью C# и немного LINQ.

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

Класс Cart относительно простой, но у него есть ряд важных линий поведения, и мы должны гарантировать, что они работают должным образом. Плохо функционирующая корзина подорвет все приложение SportsStore. Мы разобрали все функции и протестировали их индивидуально. Для этих тестов мы создали новый файл модульных тестов в проекте SportsStore.UnitTests под названием CartTests.cs. Первая линия поведения относится к добавлению элемента в корзину. Если данный объект Product добавляется в корзину в первый раз, то мы хотим, чтобы был добавлен новый объект CartLine. Ниже приведен тест и определение класса модульного тестирования:

using System;

using Microsoft.VisualStudio.TestTools.UnitTesting; using SportsStore.Domain.Entities;

using System.Linq;

namespace SportsStore.UnitTests

{

[TestClass]

public class CartTests

{

[TestMethod]

public void Can_Add_New_Lines()

{

// Arrange

- create some test products

 

Name

= "P1"

};

Product

p1

= new Product {

ProductID = 1,

Product

p2

= new Product {

ProductID =

2,

Name

= "P2"

};

//Arrange - create a new cart Cart target = new Cart();

//Act

target.AddItem(p1, 1); target.AddItem(p2, 1);

CartLine[] results = target.Lines.ToArray();

// Assert Assert.AreEqual(results.Length, 2); Assert.AreEqual(results[0].Product, p1); Assert.AreEqual(results[1].Product, p2);

}

}

}

Однако, если Product уже есть в корзине, мы хотим увеличить количество в соответствующем объекте CartLine и не создавать новый. Вот тест:

[TestMethod]

public void Can_Add_Quantity_For_Existing_Lines()

{

// Arrange

- create some test products

 

"P1" };

Product

p1

= new Product {

ProductID

= 1, Name =

Product

p2

= new Product {

ProductID

=

2, Name =

"P2" };

// Arrange - create a new cart

208

Cart target = new Cart();

//Act target.AddItem(p1, 1); target.AddItem(p2, 1); target.AddItem(p1, 10);

CartLine[] results = target.Lines.OrderBy(c => c.Product.ProductID).ToArray();

//Assert

Assert.AreEqual(results.Length, 2); Assert.AreEqual(results[0].Quantity, 11); Assert.AreEqual(results[1].Quantity, 1);

}

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

[TestMethod]

public void Can_Remove_Line()

{

// Arrange

- create some test products

Product p1

= new Product { ProductID = 1, Name = "P1" };

Product

p2

= new Product {

ProductID = 2, Name = "P2" };

Product

p3

= new Product {

ProductID = 3, Name = "P3" };

//Arrange - create a new cart Cart target = new Cart();

//Arrange - add some products to the cart target.AddItem(p1, 1);

target.AddItem(p2, 3); target.AddItem(p3, 5); target.AddItem(p2, 1);

//Act

target.RemoveLine(p2);

// Assert

Assert.AreEqual(target.Lines.Where(c => c.Product == p2).Count(), 0); Assert.AreEqual(target.Lines.Count(), 2);

}

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

[TestMethod]

public void Calculate_Cart_Total()

{

// Arrange

- create some test products

 

Name

= "P1",

Price =

100M };

Product

p1

= new Product {

ProductID = 1,

Product

p2

= new Product {

ProductID =

2,

Name

= "P2",

Price =

50M };

//Arrange - create a new cart Cart target = new Cart();

//Act

target.AddItem(p1, 1); target.AddItem(p2, 1); target.AddItem(p1, 3);

decimal result = target.ComputeTotalValue();

// Assert Assert.AreEqual(result, 450M);

}

209

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

[TestMethod]

public void Can_Clear_Contents()

{

// Arrange

- create some test products

 

Name

= "P1",

Price =

100M };

Product

p1

= new Product {

ProductID = 1,

Product

p2

= new Product {

ProductID =

2,

Name

= "P2",

Price =

50M };

//Arrange - create a new cart Cart target = new Cart();

//Arrange - add some items target.AddItem(p1, 1); target.AddItem(p2, 1);

//Act - reset the cart target.Clear();

//Assert Assert.AreEqual(target.Lines.Count(), 0);

}

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

Добавляем кнопку Add to Cart

Чтобы добавить кнопки к спискам товаров, нам нужно изменить частичное представление

Views/Shared/ProductSummary.cshtml. Изменения показаны в листинге 8-14.

Листинг 8-14: Добавляем кнопки в частичное представление ProductSummary

@model SportsStore.Domain.Entities.Product

<div class="item"> <h3>@Model.Name</h3> @Model.Description

@using (Html.BeginForm("AddToCart", "Cart")) { @Html.HiddenFor(x => x.ProductID) @Html.Hidden("returnUrl", Request.Url.PathAndQuery) <input type="submit" value="+ Add to cart" />

}

<h4>@Model.Price.ToString("c")</h4>

</div>

Мы добавили блок Razor, который создает небольшую HTML-форму для каждого товара в списке. Отправка этой форма вызовет метод действия AddToCart в контроллере Cart (мы скоро реализуем этот метод).

Примечание

По умолчанию вспомогательный метод BeginForm создает форму, которая использует метод HTTP POST. Вы можете изменить это так, чтобы формы использовали метод GET, но об этом нужно хорошо подумать. Спецификация HTTP

210

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