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

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

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

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

Для CRUD-операции Edit имеется заготовка, которую вы можете выбрать, чтобы посмотреть, что создает Visual Studio. Мы же снова будем использовать свою разметку, поэтому выбрали из списка опций для заготовок Empty. Не забудьте отметить флажком опцию Use a layout и в качестве представления выберите _AdminLayout.cshtml. Нажмите кнопку Add, чтобы создать представление, которое будет размещено в папке Views/Admin. Изменить его содержимое в соответствии с листингом 10-7.

Листинг 10-7: Представление Edit

@model SportsStore.Domain.Entities.Product

@{

ViewBag.Title = "Admin: Edit " + @Model.Name; Layout = "~/Views/Shared/_AdminLayout.cshtml";

}

<h1>Edit @Model.Name</h1> @using (Html.BeginForm())

{

@Html.EditorForModel()

<input type="submit" value="Save" /> @Html.ActionLink("Cancel and return to List", "Index")

}

251

Чтобы не писать разметку для каждой метки и поля ввода вручную, мы вызвали вспомогательный метод Html.EditorForModel. Этот метод сообщает MVC Framework создать интерфейс редактирования, для чего она проверит тип модели, в данном случае, класс Product.

Чтобы увидеть страницу, которая создается представлением Edit, запусите приложение и перейдите по ссылке /Admin/Index. Кликните по одному из товаров, и вы увидите страницу, показанную на рисунке 10-9.

Рисунок 10-9: Страница, сгенерированная с помощью вспомогательного метода EditorForModel

Давайте будем честными - метод EditorForModel удобен, но дает не самые привлекательные результаты. К тому же, мы не хотим, чтобы администратор мог видеть или редактировать атрибут ProductID, а текстовое поле для свойства Description слишком мало.

Мы можем дать MVC Framework указания касательно того, как создавать редакторы для свойств, с помощью метаданных модели. Это позволит нам применять атрибуты к свойствам нового класса модели, которые повлияют на вывод метода Html.EditorForModel. В листинге 10-8 показано, как использовать метаданные в классе Product в проекте SportsStore.Domain.

252

Листинг 10-8: Используем метаданные модели

using System.ComponentModel.DataAnnotations; using System.Web.Mvc;

namespace SportsStore.Domain.Entities

{

public class Product

{

[HiddenInput(DisplayValue = false)] public int ProductID { get; set; }

public string Name { get; set; }

[DataType(DataType.MultilineText)] public string Description { get; set; }

public decimal Price { get; set; }

public string Category { get; set; }

}

}

Атрибут HiddenInput сообщает MVC Framework, что свойство нужно визуализировать как скрытый элемент формы, а атрибут DataType позволяет указать, как значение должно отображаться и редактироваться. В данном случае мы выбрали опцию MultilineText. Атрибут HiddenInput является частью пространства имен System.Web.Mvc, и атрибут DataType - частью пространства имен System.ComponentModel.DataAnnotations, что объясняет, почему нам нужно было добавить ссылки на коллекции этих имен в проект SportsStore.Domain в главе 7.

Рисунок 10-10 показывает страницу Edit после применения метаданных. Вы больше не можете видеть или редактировать свойство ProductId, и у вас есть многострочное текстовое поле для ввода описания. Однако UI по-прежнему выглядит довольно скромно.

Рисунок 10-10: Эффект применения метаданных

253

Мы можем несколько улучшить страницу с помощью CSS. Когда MVC Framework создает поле ввода для каждого свойства, она присваивает им различные классы CSS. Если вы посмотрите на исходный код страницы, показанной на рисунке 10-10, вы увидите, что элементу текстового поля для описания товара был присвоен CSS-класс text-box-multi-line:

<textarea class="text-box multi-line" id="Description" name="Description"> Give your playing field a professional touch

</textarea>

Другим классам присваиваются подобные элементы HTML, и мы можем улучшить внешний вид представления Edit, добавив стили из листинга 10-9 в файл Admin.css из папки Content проекта SportsStore.WebUI. Эти стили назначаются разным классам, которые добавляются к элементам HTML вспомогательным методом EditorForModel.

Листинг 10-9: CSS-стили для элементов редактирования

.editor-field { margin-bottom: .8em; }

.editor-label { font-weight: bold; }

.editor-label:after { content: ":" }

.text-box { width: 25em; }

.multi-line { height: 5em; font-family: Segoe UI, Verdana; }

Рисунок 10-11 показывает эффект применения этих стилей в представлении Edit. Визуализированное представление выглядит все еще довольно незамысловато, но оно функционально и удовлетворяет нашим запросам.

Рисунок 10-11: Применение CSS к элементам редактирования

254

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

Обновляем хранилище Product

Прежде чем мы сможем обрабатывать изменения, нам нужно обновить хранилище Product так, чтобы можно было их сохранять. Во-первых, мы добавим в интерфейс IProductRepository новый метод, который показан в листинге 10-10. (Напомним, что этот интерфейс можно найти в папке

Abstract проекта SportsStore.Domain).

Листинг 10-10: Добавляем метод в интерфейс хранилища

using System.Linq;

using SportsStore.Domain.Entities; namespace SportsStore.Domain.Abstract

{

public interface IProductRepository

{

IQueryable<Product> Products { get; } void SaveProduct(Product product);

}

}

Затем мы можем добавить этот метод в реализацию хранилища Entity Framework, в класс

Concrete/EFProductRepository, как показано в листинге 10-11.

Листинг 10-11: Реализация метода SaveProduct

using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System.Linq;

namespace SportsStore.Domain.Concrete

{

public class EFProductRepository : IProductRepository

{

private EFDbContext context = new EFDbContext();

public IQueryable<Product> Products

{

get { return context.Products; }

}

public void SaveProduct(Product product)

{

if (product.ProductID == 0)

{

context.Products.Add(product);

}

else

{

Product dbEntry = context.Products.Find(product.ProductID); if (dbEntry != null)

{

dbEntry.Name = product.Name; dbEntry.Description = product.Description; dbEntry.Price = product.Price; dbEntry.Category = product.Category;

}

}

context.SaveChanges();

}

}

255

}

Реализация метода SaveChanges добавляет товар в хранилище, если ProductID равен 0; в противном случае она применяет изменения к существующей записи в базе данных.

Мы не хотим подробно разбирать Entity Framework, потому что, как мы объясняли ранее, это очень обширная тема, но метод SaveProduct имеет некоторое отношение к дизайну приложения MVC.

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

Это необходимо потому, что Entity Framework отслеживает объекты, которые он создает из базы данных. Объект, который передается в метод SaveChanges, создается MVC Framework с помощью стандартной модели связывания, что означает, что Entity Framework ничего не узнает об объекте параметра и не применит обновления к базе данных. Есть много путей решения этой проблемы, и мы выбрали самый простой: размещение соответствующего объекта, о котором будет знать Entity Framework, и явное его обновление.

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

Недостаток такого подхода заключается в том, что мы начали бы добавлять функциональность к абстрактному определению хранилища для того, чтобы обойти ограничения конкретной реализации. Если мы будем переключаться между реализациями хранилища в будущем, есть риск того, что придется реализовать возможность поиска, для которой нет готовой поддержки в новой технологии хранения. Гибкость MVC Framework, как, к примеру, метод SaveProduct, может показаться хорошей возможностью, чтобы не использовать обходные пути, но она поставит под удар дизайн вашего приложения.

Обработка запросов Edit POST

На данный момент мы готовы реализовать перегруженный метод действия Edit в контроллере Admin, который будет обрабатывать запросы POST, которые отправляются нажатием кнопки Save. Новый метод показан в листинге 10-12.

Листинг 10-12: Добавляем метод действия Edit для обработки запросов POST

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()

256

{

return View(repository.Products);

}

public ViewResult Edit(int productId)

{

Product product = repository.Products

.FirstOrDefault(p => p.ProductID == productId); return View(product);

}

[HttpPost]

public ActionResult Edit(Product product)

{

if (ModelState.IsValid)

{

repository.SaveProduct(product);

TempData["message"] = string.Format("{0} has been saved", product.Name); return RedirectToAction("Index");

}

else

{

// there is something wrong with the data values return View(product);

}

}

}

}

Мы убеждаемся, что механизм связывания провел валидацию представленных пользователем данных, прочитав значение свойства ModelState.IsValid. Если все в порядке, мы сохраняем изменения в хранилище, а затем вызываем метод действия Index, чтобы вернуть пользователя к списку товаров. Если есть проблема с данными, мы снова визуализируем представление Edit, чтобы пользователь мог внести исправления.

После сохранения изменений в хранилище, мы сохраняем сообщение с помощью объекта TempData. Этот набор пар ключ/значение похож на данные сессии и ViewBag, которые мы использовали ранее. Его основное отличие от данных сессии заключается в том, что TempData удаляется в конце запроса

HTTP.

Обратите внимание, что мы возвращаем тип ActionResult из метода Edit. До сих пор мы использовали тип ViewResult. ViewResult наследует от ActionResult и используется в тех случаях, когда платформа должна визуализировать представление. Тем не менее, доступны другие типы ActionResult, и один из них возвращается методом RedirectToAction. Мы используем его в методе действия Edit для вызова метода действия Index.

В этой ситуации мы не можем использовать ViewBag, так как нам нужно перенаправить пользователя. ViewBag передает данные между контроллером и представлением, и он не может хранить данные дольше, чем длится текущий запрос HTTP. Мы могли бы использовать данные сессии, но тогда сообщение будет храниться, пока мы явно его не удалим, а нам не хочется этого делать. Таким образом, TempData нам идеально подходит. Данные ограничиваются сессией одного пользователя (так что пользователи не видят другие TempData) и будут сохранены до тех пор, пока мы их не прочитаем. Они понадобятся нам в представлении, визуализированном тем методом действия, к которому мы перенаправили пользователя.

Модульный тест: получение данных от метода Edit

Для обработки запросов POST метода действия Edit мы должны убедиться, что для сохранения в хранилище передаются только действительные обновления объекта Product, созданного механизмом

257

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

[TestMethod]

public void Can_Save_Valid_Changes()

{

// Arrange - create mock repository

Mock<IProductRepository> mock = new Mock<IProductRepository>(); // Arrange - create the controller

AdminController target = new AdminController(mock.Object); // Arrange - create a product

Product product = new Product { Name = "Test" };

//Act - try to save the product ActionResult result = target.Edit(product);

//Assert - check that the repository was called mock.Verify(m => m.SaveProduct(product));

//Assert - check the method result type Assert.IsNotInstanceOfType(result, typeof(ViewResult));

}

[TestMethod]

public void Cannot_Save_Invalid_Changes()

{

// Arrange - create mock repository

Mock<IProductRepository> mock = new Mock<IProductRepository>(); // Arrange - create the controller

AdminController target = new AdminController(mock.Object); // Arrange - create a product

Product product = new Product { Name = "Test" };

//Arrange - add an error to the model state target.ModelState.AddModelError("error", "error");

//Act - try to save the product

ActionResult result = target.Edit(product);

// Assert - check that the repository was not called

mock.Verify(m => m.SaveProduct(It.IsAny<Product>()), Times.Never()); // Assert - check the method result type Assert.IsInstanceOfType(result, typeof(ViewResult));

}

Отображаем сообщение с подтверждением

Мы будем отображать сохраненное с помощью TempData сообщение в файле макета _AdminLayout.cshtml. Обрабатывая сообщение в шаблоне, мы сможем создавать сообщения в любом представлении, которое использует этот шаблон, не создавая для них дополнительные блоки Razor. В листинге 10-13 показаны изменения в файле.

Листинг 10-13: Обработка сообщения ViewBag в макете

<!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>

@if (TempData["message"] != null) {

<div class="Message">@TempData["message"]</div>

}

@RenderBody()

</div>

</body>

</html>

258

Примечание

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

Теперь у нас готовы все элементы, которые нужны для тестирования функции редактирования товаров. Запустите приложение, перейдите по ссылке Admin/Index, и внесите какие-нибудь изменения. Нажмите кнопку Save. Вы вернетесь к представлению со списком, в котором будет отображаться сообщение TempData, как показано на рисунке 10-12.

Рисунок 10-12: Редактируем товар и видим сообщение TempData

259

Сообщение исчезнет, если вы перезагрузите страницу со списком товаров, потому что TempData удаляется после прочтения. Это очень удобно, так как мы не хотим, чтобы сохранялись старые сообщения.

Добавляем валидацию модели

В таких случаях нам всегда нужно добавлять правила валидации для нашей сущности модели. На данный момент администратор может отправить отрицательные цены или пустое описание, и SportsStore запросто сохранит их в базе данных. В листинге 10-14 показано применение атрибута DataAnnotations к классу Product, аналогичное применению атрибута к классу ShippingDetails в предыдущей главе.

Листинг 10-14: Применение атрибутов валидации к классу Product

using System.ComponentModel.DataAnnotations; using System.Web.Mvc;

namespace SportsStore.Domain.Entities

{

public class Product

{

[HiddenInput(DisplayValue = false)] public int ProductID { get; set; }

[Required(ErrorMessage = "Please enter a product name")] public string Name { get; set; }

[DataType(DataType.MultilineText)]

[Required(ErrorMessage = "Please enter a description")] public string Description { get; set; }

[Required]

[Range(0.01, double.MaxValue, ErrorMessage = "Please enter a positive price")] public decimal Price { get; set; }

[Required(ErrorMessage = "Please specify a category")] public string Category { get; set; }

}

}

Примечание

Теперь в классе Product больше атрибутов, чем свойств. Не волнуйтесь, если вам

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

23.

Когда мы использовали вспомогательный метод Html.EditorForModel для создания элементов формы для редактирования объекта Product, MVC Framework добавляла разметку и применяла классы CSS, необходимые для отображения ошибок валидации рядом с формой. На рисунке 10-13 показано, как при редактировании товара проявляется нарушение правил валидации, которые мы применили к классу Product.

260

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