ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p
.pdfSportsStore: администрирование
В этой главе мы продолжим строить приложение SportsStore и создадим средства управления каталогом товаров для администратора сайта. Мы добавим поддержку создания, редактирования и удаления объектов из хранилища, а также загрузки изображений и их отображения вместе с описаниями товаров в каталоге.
Управление каталогом
По соглашению для управления коллекциями товаров мы должны предоставить пользователю два типа страниц: страницу со списком и страницу редактирования, как показано на рисунке 10-1.
Рисунок 10-1: Эскиз пользовательского интерфейса CRUD для каталога товаров
Вместе эти страницы позволяют пользователю создавать, читать, обновлять и удалять элементы в коллекции. В совокупности эти действия называются CRUD (Create-Read-Update-Delete – Создать- Прочитать-Обновить-Удалить). Разработчикам очень часто приходится реализовывать CRUD, так что Visual Studio предлагает сгенерировать MVC-контроллеры с готовыми методами действий для операций CRUD и шаблоны представлений, которые их поддерживают.
Создаем контроллер CRUD
Мы создадим новый контроллер для обработки функций администрирования. Кликните правой кнопкой мыши папку Controllers в проекте SportsStore.WebUI и выберите Add - Controller из контекстного меню. Назовите контроллер AdminController и выберите из выпадающего списка
Template пункт Empty MVC Controller, как показано на рисунке 10-2.
Примечание
В Visual Studio есть несколько шаблонов для классов контроллеров, которые включают методы CRUD. Как мы уже говорили, они нам не нравятся и мы предпочитаем создавать классы контроллеров с нуля.
241
Рисунок 10-2: Создание контроллера с помощью диалогового окна Add Controller
Нажмите кнопку Add, чтобы создать контроллер. Для поддержки страницы со списком, показанной на рисунке 10-1, нужно добавить метод действия, который будет отображать все товары в хранилище. Следуя соглашениям MVC Framework, мы назовем этот метод Index. Измените содержимое класса контроллера в соответствии с листингом 10-1.
Листинг 10-1: Метод действия Index
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 AdminController : Controller
{
private IProductRepository repository;
public AdminController(IProductRepository repo)
{
repository = repo;
}
public ViewResult Index()
{
return View(repository.Products);
}
}
}
242
Модульный тест: Действие Index
Поведение метода Index, которое нас интересует, состоит в том, правильно ли он возвращает объекты Product, которые находятся в хранилище. Мы можем протестировать его, создав имитацию реализации хранилища и сравнив тестовые данные с данными, возвращенными методом действия. Вот модульный тест, который мы добавили в новый файл тестов под названием AdminTests.cs:
using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq;
using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.WebUI.Controllers; using System;
using System.Collections.Generic; using System.Linq;
using System.Web.Mvc;
namespace SportsStore.UnitTests
{
[TestClass]
public class AdminTests
{
[TestMethod]
public void Index_Contains_All_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"}, new Product {ProductID = 2, Name = "P2"}, new Product {ProductID = 3, Name = "P3"},
}.AsQueryable());
//Arrange - create a controller
AdminController target = new AdminController(mock.Object); // Action
Product[] result = ((IEnumerable<Product>)target.Index().ViewData.Model).ToArray();
// Assert Assert.AreEqual(result.Length, 3); Assert.AreEqual("P1", result[0].Name); Assert.AreEqual("P2", result[1].Name); Assert.AreEqual("P3", result[2].Name);
}
}
}
Создаем новый макет
Мы собираемся создать новый макет для представлений администрирования SportsStore. Эта простая разметка будет представлять собой единственную точку, в которой мы сможем применить изменения ко всем представлениям администрирования.
Чтобы создать макет, щелкните правой кнопкой мыши папку Views/Shared в проекте
SportsStore.WebUI и выберите Add - New Item. Выберите шаблон MVC 4 Layout Page (Razor) и
назовите его _AdminLayout.cshtml, как показано на рисунке 10-3. Нажмите кнопку Add, чтобы создать новый файл.
243
Рисунок 10-3: Создаем новый макет Razor
Как мы уже объясняли ранее, по соглашению имя макета начинается с символа подчеркивания (_). Razor также используется другой технологией Microsoft под названием WebMatrix, в которой символ подчеркивания нужен для того, чтобы предотвратить отправку страниц макетов в браузеры. В MVC такая защита не требуется, но соглашение об именах макетов так или иначе переносится на приложения MVC.
Мы хотим создать в макете ссылку на файл CSS, как показано в листинге 10-2.
Листинг 10-2: Файл _AdminLayout.cshtml
<!DOCTYPE html> <html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/Content/Admin.css" rel="stylesheet" type="text/css" />
<title></title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
Дополнение (выделено жирным шрифтом) является ссылкой на файл CSS под названием Admin.css в папке Content. Чтобы создать файл Admin.css, кликните правой кнопкой мыши папку Content,
Add - New Item, шаблон Style Sheet и укажите имя Admin.css, как показано на
244
Рисунок 10-4: Создание файла Admin.css
Замените содержимое файла Admin.css на стили, показанные в листинге 10-3.
Листинг 10-3: CSS-стили для представлений администрирования
BODY, TD { font-family: Segoe UI, Verdana }
H1 { padding: .5em; padding-top: 0; font-weight: bold; font-size: 1.5em; border-bottom: 2px solid gray; }
DIV#content { padding: .9em; }
TABLE.Grid TD, TABLE.Grid TH { border-bottom: 1px dotted gray; text-align:left; } TABLE.Grid { border-collapse: collapse; width:100%; }
TABLE.Grid TH.NumericCol, Table.Grid TD.NumericCol { text-align: right; padding-right: 1em; }
FORM {margin-bottom: 0px; }
DIV.Message { background: gray; color:White; padding: .2em; margin-top:.25em; }
.field-validation-error { color: red; display: block; }
.field-validation-valid { display: none; }
.input-validation-error { border: 1px solid red; background-color: #ffeeee; }
.validation-summary-errors { font-weight: bold; color: red; }
.validation-summary-valid { display: none; }
Реализация представлений для страниц со списком
Теперь, когда новый макет готов, мы можем добавить в проект представление для метода действия Index контроллера Admin. Кликните правой кнопкой мыши по методу Index и выберите Add View из контекстного меню. Назовите представление Index, как показано на рисунке 10-5.
245
Рисунок 10-5: Создаем представление Index
Мы собираемся использовать заготовку (scaffold view) – то есть, представление, для которого Visual Studio сама создаст разметку в зависимости от того, какой мы выберем класс для строго типизированного представления. Чтобы ее создать, выберите Product из списка классов модели и шаблон заготовки List, как показано на рисунке 10-5.
Примечание
Когда вы используете заготовку List, Visual Studio предполагает, что вы работаете с последовательностью IEnumerable типа модели представления, так что вы можете просто выбрать одиночную форму класса из списка.
Мы хотим применить наш вновь созданный макет, так что отметьте флажком опцию Use a layout и выберите файл _AdminLayout.cshtml из папки Views/Shared. Нажмите кнопку Add, чтобы создать представление. Заготовка, которую создаст Visual Studio, показана в листинге 10-4 (мы ее немного подчистили, чтобы сделать более компактной и читабельной).
246
Листинг 10-4: Заготовка представления для страниц со списком
@model IEnumerable<SportsStore.Domain.Entities.Product>
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create") </p>
<table>
<tr>
<th>@Html.DisplayNameFor(model => model.Name)</th> <th>@Html.DisplayNameFor(model => model.Description)</th> <th>@Html.DisplayNameFor(model => model.Price)</th> <th>@Html.DisplayNameFor(model => model.Category)</th> <th></th>
</tr>
@foreach (var item in Model) { <tr>
<td>@Html.DisplayFor(modelItem => item.Name)</td> <td>@Html.DisplayFor(modelItem => item.Description)</td> <td>@Html.DisplayFor(modelItem => item.Price)</td> <td>@Html.DisplayFor(modelItem => item.Category)</td> <td>
@Html.ActionLink("Edit", "Edit", new { id = item.ProductID }) | @Html.ActionLink("Details", "Details", new { id = item.ProductID }) | @Html.ActionLink("Delete", "Delete", new { id = item.ProductID })
</td>
</tr>
}
</table>
Visual Studio смотрит на тип объекта модели представления и генерирует в таблице элементы, которые соответствуют его свойствам. Вы можете увидеть, как визуализируется это представление, если запустите приложение и перейдете по ссылке /Admin/Index. Результаты показаны на рисунке
10-6.
Рисунок 10-6: Визуализация представления для страниц со списком
247
Заготовка во многом помогает нам с настройками. У нас имеются столбцы для каждого из свойств класса Product и ссылки на другие операции CRUD, которые ведут к методам действий того же контроллера. Но стоит отметить, что она содержит излишнюю разметку. Кроме того, мы хотим использовать в представлении CSS, который мы создали ранее. Отредактируйте файл Index.cshtml в соответствии с листингом 10-5.
Листинг 10-5: Изменяем представление Index.cshtml
@model IEnumerable<SportsStore.Domain.Entities.Product>
@{
ViewBag.Title = "Admin: All Products";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h1>All Products</h1> <table class="Grid">
<tr>
<th>ID</th>
<th>Name</th>
<th class="NumericCol">Price</th> <th>Actions</th>
</tr>
@foreach (var item in Model) { <tr>
<td>@item.ProductID</td>
<td>@Html.ActionLink(item.Name, "Edit", new { item.ProductID })</td> <td class="NumericCol">@item.Price.ToString("c")</td>
<td>
@using (Html.BeginForm("Delete", "Admin")) { @Html.Hidden("ProductID", item.ProductID) <input type="submit" value="Delete" />
}
</td>
</tr>
}
</table>
<p>@Html.ActionLink("Add a new product", "Create")</p>
Это представление визуализирует информацию более компактно, оно опускает некоторые свойства из класса Product и по-другому представляет ссылки на конкретные товары. Вы можете увидеть, как оно выглядит, на рисунке 10-7.
Рисунок 10-7: Визуализация измененного представления Index
248
Теперь у нас готова страница со списком. Администратор может просматривать товары в каталоге, а также использовать ссылки или кнопки для добавления, удаления и просмотра элементов. В следующих разделах мы добавим функциональность для поддержки каждого из этих действий.
Редактирование товаров
Чтобы обеспечить поддержку создания и обновления, мы добавим страницу редактирования товаров, как показано на рисунке 10-1. Эту работу мы выполним в два этапа:
Отобразим страницу, которая позволит администратору изменять значения свойств товара.
Добавим метод действия, который будет обрабатывать изменения после их отправки.
Создаем метод действия Edit
В листинге 10-6 показан метод Edit, который мы добавили к классу AdminController. Это метод действия, который мы указали в вызовах к вспомогательному методу действия Html.ActionLink в представлении Index.
Листинг 10-6: Метод Edit
using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System;
using System.Collections.Generic; using System.Linq;
using System.Web; using System.Web.Mvc;
namespace SportsStore.WebUI.Controllers
{
public class AdminController : Controller
{
private IProductRepository repository;
public AdminController(IProductRepository repo)
{
repository = repo;
}
public ViewResult Index()
{
return View(repository.Products);
}
public ViewResult Edit(int productId)
{
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId); return View(product);
}
}
}
Этот простой метод находит товар с ID, который соответствует параметру productId, и передает его в качестве объекта модели представления.
249
Модульный тест: метод действия Edit
Мы хотим протестировать два вида поведения в методе действия Edit. Первое состоит в том, получаем ли мы правильный товар, когда предоставляем действительное значение ID. Очевидно, мы хотим убедиться, что отредактируем именно тот товар, который собирались. Второй вид поведения заключается в том, что мы не получаем никакого товара вообще, когда мы предоставляем недействительное значение ID. Вот тестовые методы, которые мы добавили в файл AdminTests.cs:
[TestMethod]
public void Can_Edit_Product()
{
//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"}, new Product {ProductID = 2, Name = "P2"}, new Product {ProductID = 3, Name = "P3"},
}.AsQueryable());
//Arrange - create the controller
AdminController target = new AdminController(mock.Object); // Act
Product p1 = target.Edit(1).ViewData.Model as Product; Product p2 = target.Edit(2).ViewData.Model as Product; Product p3 = target.Edit(3).ViewData.Model as Product; // Assert
Assert.AreEqual(1, p1.ProductID); Assert.AreEqual(2, p2.ProductID); Assert.AreEqual(3, p3.ProductID);
}
[TestMethod]
public void Cannot_Edit_Nonexistent_Product()
{
//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"}, new Product {ProductID = 2, Name = "P2"}, new Product {ProductID = 3, Name = "P3"},
}.AsQueryable());
//Arrange - create the controller
AdminController target = new AdminController(mock.Object); // Act
Product result = (Product)target.Edit(4).ViewData.Model; // Assert
Assert.IsNull(result);
}
Создаем представление редактирования
Теперь, когда у нас готов метод действия, мы можем создать представление, которое он будет визуализировать. Кликните правой кнопкой мыши по методу действия Edit и выберите пункт Add View. Оставьте имя представления Edit, отметьте флажком опцию Create a strongly-typed view и убедитесь, что в качестве класса модели выбран класс Product, как показано на рисунке 10-8.
250