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

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

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

Мы визуализировали элементы input для каждого из полей формы с помощью вспомогательного метода Html.EditorFor. Это пример шаблонного вспомогательного метода. Мы позволяем MVC Framework решать, какой элемент input требуется для свойства модели представления, а не указываем его явно (например, с помощью Html.TextBoxFor).

Мы подробно опишем шаблонные вспомогательные методы в главе 20, но вы уже можете видеть из рисунка, MVC достаточно умная платформа, чтобы визуализировать чекбокс для свойств bool (например, для опции Gift wrap) и текстовые поля для строковых свойств.

Совет

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

сгенерирует метки и поля ввода для всех свойств в классе модели представления ShippingDetails. Тем не менее, мы хотели разделить элементы так, чтобы поля

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

Создаем IOrderProcessor

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

Определяем интерфейс

Добавьте новый интерфейс под названием IOrderProcessor в папку Abstract проекта SportsStore.Domain и отредактируйте его содержимое так, чтобы оно соответствовало листингу 9- 13.

Листинг 9-13: Интерфейс IOrderProcessor

using SportsStore.Domain.Entities;

namespace SportsStore.Domain.Abstract

{

public interface IOrderProcessor

{

void ProcessOrder(Cart cart, ShippingDetails shippingDetails);

}

}

Создаем реализацию интерфейса

Наша реализация IOrderProcessor будет обрабатывать заказы, отправляя их по электронной почте администратору сайта. Конечно, мы упрощаем процесс продажи. Большинство сайтов электронной коммерции не ограничиваются подтверждением заказов по e-mail, к тому же мы не обеспечили поддержку для обработки кредитных карт и других форм оплаты. Но мы не хотим отвлекаться от MVC, так что будем работать с электронной почтой.

Создайте новый класс под названием EmailOrderProcessor в папке Concrete проекта SportsStore.Domain и отредактируйте содержимое так, чтобы он соответствовал листингу 9-14. Для отправки e-mail этот класс использует встроенную поддержку SMTP, которая включена в библиотеку .NET Framework.

231

Листинг 9-14: Класс EmailOrderProcessor

using System.Net.Mail; using System.Text;

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

namespace SportsStore.Domain.Concrete

{

public class EmailSettings

{

public string MailToAddress = "orders@example.com"; public string MailFromAddress = "sportsstore@example.com"; public bool UseSsl = true;

public string Username = "MySmtpUsername"; public string Password = "MySmtpPassword"; public string ServerName = "smtp.example.com"; public int ServerPort = 587;

public bool WriteAsFile = false;

public string FileLocation = @"c:\sports_store_emails";

}

public class EmailOrderProcessor : IOrderProcessor

{

private EmailSettings emailSettings;

public EmailOrderProcessor(EmailSettings settings)

{

emailSettings = settings;

}

public void ProcessOrder(Cart cart, ShippingDetails shippingInfo)

{

using (var smtpClient = new SmtpClient())

{

smtpClient.EnableSsl = emailSettings.UseSsl; smtpClient.Host = emailSettings.ServerName; smtpClient.Port = emailSettings.ServerPort; smtpClient.UseDefaultCredentials = false;

smtpClient.Credentials = new NetworkCredential(emailSettings.Username, emailSettings.Password);

if (emailSettings.WriteAsFile)

{

smtpClient.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; smtpClient.PickupDirectoryLocation = emailSettings.FileLocation; smtpClient.EnableSsl = false;

}

StringBuilder body = new StringBuilder()

.AppendLine("A new order has been submitted")

.AppendLine("---")

.AppendLine("Items:");

foreach (var line in cart.Lines)

{

var subtotal = line.Product.Price * line.Quantity; body.AppendFormat("{0} x {1} (subtotal: {2:c}", line.Quantity,

line.Product.Name, subtotal);

}

body.AppendFormat("Total order value: {0:c}", cart.ComputeTotalValue())

.AppendLine("---")

.AppendLine("Ship to:")

.AppendLine(shippingInfo.Name)

.AppendLine(shippingInfo.Line1)

.AppendLine(shippingInfo.Line2 ?? "")

232

.AppendLine(shippingInfo.Line3 ?? "")

.AppendLine(shippingInfo.City)

.AppendLine(shippingInfo.State ?? "")

.AppendLine(shippingInfo.Country)

.AppendLine(shippingInfo.Zip)

.AppendLine("---")

.AppendFormat("Gift wrap: {0}", shippingInfo.GiftWrap ? "Yes" : "No");

MailMessage mailMessage = new MailMessage( emailSettings.MailFromAddress, // From emailSettings.MailToAddress, // To

"New order submitted!", // Subject body.ToString()); // Body

if (emailSettings.WriteAsFile)

{

mailMessage.BodyEncoding = Encoding.ASCII;

}

smtpClient.Send(mailMessage);

}

}

}

}

Для простоты мы также определили в листинге 9-14 класс EmailSettings. Экземпляр этого класса требуется конструктором EmailOrderProcessor и содержит все настройки, которые необходимы для конфигурации классов .NET, работающих с электронной почтой.

Совет

Не беспокойтесь, если у вас нет сервера SMTP. Если вы установите свойству EmailSettings.WriteAsFile значение true, e-mail сообщения будут записываться как файлы в директорию, указанную в свойстве FileLocation. Эта директория

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

Регистрируем реализацию

Теперь у нас есть реализация интерфейса IOrderProcessor и средства для ее настройки, мы также можем использовать Ninject для создания ее экземпляров. Отредактируйте класс

NinjectControllerFactory проекта SportsStore.WebUI и внесите в метод AddBindings изменения,

показанные в листинге 9-15.

Листинг 9-15: Добавление привязок Ninject для IOrderProcessor

using System;

using System.Web.Mvc; using System.Web.Routing; using Ninject;

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

using Moq;

using SportsStore.Domain.Concrete; using System.Configuration;

namespace SportsStore.WebUI.Infrastructure

{

233

public class NinjectControllerFactory : DefaultControllerFactory

{

private IKernel ninjectKernel;

public NinjectControllerFactory()

{

ninjectKernel = new StandardKernel(); AddBindings();

}

protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)

{

return controllerType == null ? null

: (IController)ninjectKernel.Get(controllerType);

}

private void AddBindings()

{

ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>();

EmailSettings emailSettings = new EmailSettings

{

WriteAsFile = bool.Parse(ConfigurationManager

.AppSettings["Email.WriteAsFile"] ?? "false")

};

ninjectKernel.Bind<IOrderProcessor>()

.To<EmailOrderProcessor>()

.WithConstructorArgument("settings", emailSettings);

}

}

}

Мы создали объект EmailSettings, который будем использовать с методом Ninject

WithConstructorArgument. Он будет внедряться в конструктор EmailOrderProcessor, когда создаются новые экземпляры для обслуживания запросов интерфейса IOrderProcessor. В листинге 9-15 мы установили значение только для одного из свойств EmailSettings - WriteAsFile. Мы читаем значение этого свойства с помощью свойства ConfigurationManager.AppSettings, которое дает нам доступ к настройкам приложения в файле Web.config (в корневой папке проекта), которые показаны в листинге 9-16.

Листинг 9-16: Настройки приложения в файле Web.config

<appSettings>

<add key="webpages:Version" value="2.0.0.0" /> <add key="webpages:Enabled" value="false" /> <add key="PreserveLoginUrl" value="true" />

<add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" />

<add key="Email.WriteAsFile" value="true"/>

</appSettings>

Завершаем CartController

Для завершения класса CartController мы должны изменить конструктор так, что он запрашивал реализацию интерфейса IOrderProcessor, и добавить новый метод действия, который будет обрабатывать форму POST после того, когда пользователь нажмет кнопку Complete order. Листинг 9-17 показывает оба изменения.

234

Листинг 9-17: Завершаем класс CartController

using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.WebUI.Models; using System;

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

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

namespace SportsStore.WebUI.Controllers

{

public class CartController : Controller

{

private IProductRepository repository; private IOrderProcessor orderProcessor;

public CartController(IProductRepository repo, IOrderProcessor proc)

{

repository = repo; orderProcessor = proc;

}

public ViewResult Index(Cart cart, string returnUrl)

{

return View(new CartIndexViewModel

{

Cart = cart, ReturnUrl = returnUrl

});

}

public ViewResult Summary(Cart cart)

{

return View(cart);

}

[HttpPost]

public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails)

{

if (cart.Lines.Count() == 0)

{

ModelState.AddModelError("", "Sorry, your cart is empty!");

}

if (ModelState.IsValid)

{

orderProcessor.ProcessOrder(cart, shippingDetails); cart.Clear();

return View("Completed");

}

else

{

return View(shippingDetails);

}

}

public ViewResult Checkout()

{

return View(new ShippingDetails());

}

// ...other action methods omitted for brevity...

}

}

235

Как видите, к методу действия Checkout теперь добавляется атрибут HttpPost, что означает, что он будет вызван для обработки запроса POST (в данном случае, когда пользователь отправляет форму). Опять же, мы полагаемся на механизм связывания данных как для параметра ShippingDetails (который создается автоматически на основе данных формы HTTP), так и для параметра Cart (который создается с помощью нашего пользовательского механизма связывания).

Примечание

Из-за изменения конструктора мы должны обновить модульные тесты, которые мы создали для класса CartController. Тесты будут скомпилированы, если вы передадите null в новый параметра конструктора.

MVC Framework проверяет ограничения валидации, которые мы применили к ShippingDetails помощью атрибутов DataAnnotation в листинге 9-17, и передает любые нарушения в наш метод действия через свойство ModelState. Мы можем увидеть, есть ли какие-нибудь проблемы, проверив свойство ModelState.IsValid. Обратите внимание, что мы вызываем метод ModelState.AddModelError для регистрирации сообщения об ошибке, если нет товаров в корзине. Мы вкратце объясним, как отображать такие ошибки, и подробно разберем связывание данных и валидацию в главах 22 и 23.

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

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

[TestMethod]

public void Cannot_Checkout_Empty_Cart()

{

//Arrange - create a mock order processor Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();

//Arrange - create an empty cart

Cart cart = new Cart();

// Arrange - create shipping details

ShippingDetails shippingDetails = new ShippingDetails();

//Arrange - create an instance of the controller CartController target = new CartController(null, mock.Object);

//Act

ViewResult result = target.Checkout(cart, shippingDetails);

//Assert - check that the order hasn't been passed on to the processor mock.Verify(m =>

m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never());

//Assert - check that the method is returning the default view Assert.AreEqual("", result.ViewName);

//Assert - check that we are passing an invalid model to the view Assert.AreEqual(false, result.ViewData.ModelState.IsValid);

}

236

Этот тест гарантирует, что пользователь не сможет подтвердить покупку с пустой корзиной. Чтобы это проверить, мы утверждаем, что ProcessOrder имитированной реализации IOrderProcessor никогда не вызывается, что метод возвращает представление по умолчанию (которое снова отобразит данные, введенные пользователем, и даст возможность их исправить), и что состояние модели, которое передается в представление, отмечено как недопустимое. Может показаться, что наши утверждения дублируют друг друга, но нам нужны все три, чтобы гарантировать корректное поведение.

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

[TestMethod]

public void Cannot_Checkout_Invalid_ShippingDetails()

{

//Arrange - create a mock order processor Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();

//Arrange - create a cart with an item

Cart cart = new Cart(); cart.AddItem(new Product(), 1);

//Arrange - create an instance of the controller CartController target = new CartController(null, mock.Object);

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

//Act - try to checkout

ViewResult result = target.Checkout(cart, new ShippingDetails());

//Assert - check that the order hasn't been passed on to the processor mock.Verify(m =>

m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never());

//Assert - check that the method is returning the default view Assert.AreEqual("", result.ViewName);

//Assert - check that we are passing an invalid model to the view Assert.AreEqual(false, result.ViewData.ModelState.IsValid);

}

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

[TestMethod]

public void Can_Checkout_And_Submit_Order()

{

//Arrange - create a mock order processor Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();

//Arrange - create a cart with an item

Cart cart = new Cart(); cart.AddItem(new Product(), 1);

//Arrange - create an instance of the controller CartController target = new CartController(null, mock.Object);

//Act - try to checkout

ViewResult result = target.Checkout(cart, new ShippingDetails());

// Assert - check that the order has been passed on to the processor

237

mock.Verify(m =>

m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Once());

//Assert - check that the method is returning the Completed view Assert.AreEqual("Completed", result.ViewName);

//Assert - check that we are passing a valid model to the view Assert.AreEqual(true, result.ViewData.ModelState.IsValid);

}

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

Отображаем ошибки валидации

Если пользователь введет недействительные реквизиты доставки, отдельные поля формы с проблемами будут подсвечены, но никаких сообщений отображаться не будет. Хуже того, если пользователь попытается подтвердить покупку с пустой корзиной, мы не дадим ему завершить заказ, но и не покажем никакого сообщения об ошибке. Чтобы решить эту проблему, мы должны добавить в представление ValidationSummary, как в главе 2. Листинг 9-18 показывает дополнение в представление Checkout.cshtml.

Листинг 9-18: Добавляем ValidationSummary

<h2>Check out now</h2>

Please enter your details, and we'll ship your goods right away! @using (Html.BeginForm()) {

@Html.ValidationSummary() <h3>Ship to</h3>

<div>Name: @Html.EditorFor(x => x.Name)</div>

Теперь, когда пользователь введет недействительные реквизиты доставки или попытается подтвердить покупку с пустой корзиной, он увидит сообщение об ошибке, как показано на рисунке

9-7.

Рисунок 9-7: Отображение сообщений валидации

238

Отображаем страницу подтверждения

Для завершения процесса подтверждения покупки мы покажем пользователю страницу с подтверждением, что заказ был обработан, и благодарностью за покупку. Кликните правой кнопкой мыши по любому методу действия в классе CartController и выберите Add View из контекстного меню. Назовите представление Completed, как показано на рисунке 9-8.

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

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

Листинг 9-19: Представление Completed.cshtml

@{

ViewBag.Title = "SportsStore: Order Submitted";

}

<h2>Thanks!</h2>

Thanks for placing your order. We'll ship your goods as soon as possible.

239

Теперь пользователь может пройти весь процесс, начиная с выбора товара и заканчивая подтверждением покупки. Если он предоставит действительные реквизиты доставки (и при наличии товаров в корзине), то, нажимая на кнопку Complete order, он попадет на страницу подтверждения, как показано на рисунке 9-9.

Рисунок 9-9: Страница с благодарностью

Резюме

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

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

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

240

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