Отправка заказов

129 Исходный код проекта

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

Добавьте файл по имени ShippingDetails.cs в папку Entities проекта GameStore.Domain и отредактируйте его содержимое согласно примеру ниже. Этот класс будет применяться для представления информации о доставке пользователю.

using System.ComponentModel.DataAnnotations;

namespace GameStore.Domain.Entities
{
    public class ShippingDetails
    {
        [Required(ErrorMessage = "Укажите как вас зовут")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Вставьте первый адрес доставки")]
        public string Line1 { get; set; }
        public string Line2 { get; set; }
        public string Line3 { get; set; }

        [Required(ErrorMessage = "Укажите город")]
        public string City { get; set; }

        [Required(ErrorMessage = "Укажите страну")]
        public string Country { get; set; }

        public bool GiftWrap { get; set; }
    }
}

В примере используются атрибуты проверки достоверности из пространства имен System.ComponentModel.DataAnnotations. Созданный класс модели ShippingDetails не содержит никакой функциональности, поэтому в модульном тестировании не нуждается.

Добавление процесса оплаты

Цель заключается в том, чтобы обеспечить пользователям возможность ввода информации о доставке и отправки заказа. Для начала потребуется добавить к представлению итоговой информации по корзине кнопку "Оформить заказ". Изменения, которые необходимо внести в файл Views/Cart/Index.cshtml, приведены в примере ниже:

...
<div class="text-center">
    <a class="btn btn-primary" href="@Model.ReturnUrl">Продолжить покупки</a>
    @Html.ActionLink("Оформить заказ", "Checkout", null, new { @class = "btn btn-primary" })
</div>
...

Это изменение обеспечивает генерацию ссылки, стилизованной в виде кнопки, щелчок на которой приводит к вызову метода действия Checkout() контроллера Cart. На рисунке ниже показано, как выглядит эта кнопка:

Кнопка оформления заказа

Как и можно было ожидать, теперь мы должны определить в классе CartController метод Checkout(), как показано в примере ниже:

public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails)
{
    return View(new ShippingDetails());
}

Метод Checkout() возвращает стандартное представление, передавая ему новый объект ShippingDetails в качестве модели представления. Чтобы создать представление для этого метода действия, щелкните правой кнопкой мыши на методе Checkout() и выберите в контекстном меню пункт Add View (Добавить представление). Установите имя представления в Checkout и щелкните на кнопке ОК. Среда Visual Studio создаст файл Views/Cart/Checkout.cshtml, содержимое которого необходимо привести в соответствие с кодом ниже:

@model GameStore.Domain.Entities.ShippingDetails

@{
    ViewBag.Title = "GameStore: форма заказа";
}

<h2>Оформить заказ сейчас</h2>
<p>Пожалуйста введи ваши контактные данные, и мы сразу отправим товар!</p>


@using (Html.BeginForm())
{
    <h3>Данные</h3>
    <div class="form-group">
        <label>Ваше имя:</label>
        @Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
    </div>

    <h3>Адрес доставки</h3>
    <div class="form-group">
        <label>Первый адрес:</label>
        @Html.TextBoxFor(x => x.Line1, new { @class = "form-control" })
    </div>
    <div class="form-group">
        <label>Второй адрес:</label>
        @Html.TextBoxFor(x => x.Line2, new { @class = "form-control" })
    </div>
    <div class="form-group">
        <label>Третий адрес:</label>
        @Html.TextBoxFor(x => x.Line3, new { @class = "form-control" })
    </div>
    <div class="form-group">
        <label>Город:</label>
        @Html.TextBoxFor(x => x.City, new { @class = "form-control" })
    </div>
    <div class="form-group">
        <label>Страна:</label>
        @Html.TextBoxFor(x => x.Country, new { @class = "form-control" })
    </div>

    <h3>Опции</h3>
    <div class="checkbox">
        <label>
            @Html.EditorFor(x => x.GiftWrap)
            Использовать подарочную упаковку?
        </label>
    </div>

    <div class="text-center">
        <input class="btn btn-primary" type="submit" value="Обработать заказ" />
    </div>
}

Для каждого свойства в модели мы создали элемент <label> вместе с элементом <input>, сформатированным посредством библиотеки Bootstrap и предназначенным для пользовательского ввода. Чтобы увидеть результат созданного представления, показанный на рисунке ниже, запустите приложение и щелкните на кнопке "Заказать" в верхней части страницы и затем на кнопке "Оформить заказ". (Попасть на это представление можно также за счет перехода на URL вида /Cart/Checkout):

Форма для сбора деталей о доставке

Проблема, связанная с этим представлением, заключается в том, что оно содержит много повторяющейся разметки. В MVC Framework имеются вспомогательные методы HTML, которые могли бы сократить дублирование, но они затрудняют структурирование и стилизацию содержимого желаемым способом. Вместо этого мы воспользуемся удобным средством для получения метаданных об объекте модели представления и объединим их с помощью смеси выражений C# и Razor.

В примере ниже видно, что было сделано:

...
    <h3>Адрес доставки</h3>
    foreach (var property in ViewData.ModelMetadata.Properties) {
        if (property.PropertyName != "Name" && property.PropertyName != "GiftWrap") {
            <div class="form-group">
                <label>@(property.DisplayName ?? property.PropertyName)</label>
                @Html.TextBox(property.PropertyName, null, new { @class = "form-control" })
            </div>
        }
    }

    <h3>Опции</h3>

...

Статическое свойство ViewData.ModelMetadata возвращает объект ModelMetaData, который предоставляет информацию о типе модели для представления. Свойство Properties, применяемое в цикле foreach, возвращает коллекцию объектов ModelMetaData, каждый из которых представляет свойство, определенное в типе модели. Свойство PropertyName используется, чтобы гарантировать отсутствие генерации содержимого для свойств Name или GiftWrap (которые обрабатываются в другом месте представления) и генерировать набор элементов, дополненных классами Bootstrap, для всех остальных свойств.

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

Тем не менее, завершено еще не все. Если вы запустите приложение и взглянете на вывод, сгенерированный представлением, то увидите, что некоторые метки не вполне корректны, как показано на рисунке выше. Дело в том, что имена свойств не всегда подходят для качественных меток. Именно поэтому выполняется проверка доступности значения DisplayName при генерации элементов формы, примерно так:

@(property.DisplayName ?? property.PropertyName)

Чтобы задействовать свойство DisplayName, необходимо применить атрибут Display к классу модели, как показано в примере ниже:

using System.ComponentModel.DataAnnotations;

namespace GameStore.Domain.Entities
{
    public class ShippingDetails
    {
        [Required(ErrorMessage = "Укажите как вас зовут")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Вставьте первый адрес доставки")]
        [Display(Name="Первый адрес")]
        public string Line1 { get; set; }

        [Display(Name = "Второй адрес")]
        public string Line2 { get; set; }

        [Display(Name = "Третий адрес")]
        public string Line3 { get; set; }

        [Required(ErrorMessage = "Укажите город")]
        [Display(Name = "Город")]
        public string City { get; set; }

        [Required(ErrorMessage = "Укажите страну")]
        [Display(Name = "Страна")]
        public string Country { get; set; }

        public bool GiftWrap { get; set; }
    }
}

Установка значения Name для атрибута Display позволяет настроить значение, которое будет читаться свойством DisplayName внутри представления. Увидеть результат можно, запустив приложение и просмотрев страницу для сбора деталей о доставке:

Результат применения атрибута Display к типу модели

Этот пример демонстрирует два разных аспекта работы с инфраструктурой MVC Framework. Первый аспект заключается в том, для любой ситуации можно найти обходной путь, чтобы упростить разметку или код. Второй аспект состоит в том, что хотя роль, которую играют представления в шаблоне MVC, ограничена отображением данных и разметки, инструменты, предлагаемые Razor и C# для решения этой задачи, являются развитыми и гибкими, даже в рамках работы с метаданными типов.

Реализация обработчика заказов

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

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

Добавьте новый интерфейс по имени IOrderProcessor в папку Abstract проекта GameStore.Domain и отредактируйте его содержимое согласно примеру ниже:

using GameStore.Domain.Entities;

namespace GameStore.Domain.Abstract
{
    public interface IOrderProcessor
    {
        void ProcessOrder(Cart cart, ShippingDetails shippingDetails);
    }
}

Реализация интерфейса

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

Создайте новый файл класса по имени EmailOrderProcessor.cs в папке Concrete проекта GameStore.Domain и приведите его содержимое к виду, показанному в примере ниже. Для отправки электронной почты в этом классе применяется встроенная поддержка протокола SMTP, доступная в библиотеке .NET Framework:

using System.Net;
using System.Net.Mail;
using System.Text;
using GameStore.Domain.Abstract;
using GameStore.Domain.Entities;

namespace GameStore.Domain.Concrete
{
    public class EmailSettings
    {
        public string MailToAddress = "orders@example.com";
        public string MailFromAddress = "gamestore@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 = true;
        public string FileLocation = @"c:\game_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("Новый заказ обработан")
                    .AppendLine("---")
                    .AppendLine("Товары:");

                foreach (var line in cart.Lines)
                {
                    var subtotal = line.Game.Price * line.Quantity;
                    body.AppendFormat("{0} x {1} (итого: {2:c}", 
                        line.Quantity, line.Game.Name, subtotal);
                }

                body.AppendFormat("Общая стоимость: {0:c}", cart.ComputeTotalValue())
                    .AppendLine("---")
                    .AppendLine("Доставка:")
                    .AppendLine(shippingInfo.Name)
                    .AppendLine(shippingInfo.Line1)
                    .AppendLine(shippingInfo.Line2 ?? "")
                    .AppendLine(shippingInfo.Line3 ?? "")
                    .AppendLine(shippingInfo.City)
                    .AppendLine(shippingInfo.Country)
                    .AppendLine("---")
                    .AppendFormat("Подарочная упаковка: {0}",
                        shippingInfo.GiftWrap ? "Да" : "Нет");

                MailMessage mailMessage = new MailMessage(
                                       emailSettings.MailFromAddress,	// От кого
                                       emailSettings.MailToAddress,		// Кому
                                       "Новый заказ отправлен!",		// Тема
                                       body.ToString()); 				// Тело письма

                if (emailSettings.WriteAsFile)
                {
                    mailMessage.BodyEncoding = Encoding.UTF8;
                }

                smtpClient.Send(mailMessage);
            }
        }
    }
}

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

Не переживайте, если сервер SMTP вам не доступен. Если установить свойство EmailSettings.WriteAsFile в true, то сообщения электронной почты будут записываться в файлы внутри каталога, указанного в свойстве FileLocation. Этот каталог должен существовать и разрешать в него запись. Записываемые файлы будут иметь расширение .eml и могут быть прочитаны с помощью любого текстового редактора. В этом примере в качестве местоположения установлен каталог "c:\game_store_emails".

Регистрация реализации

Имея реализацию интерфейса IOrderProcessor и средства для ее конфигурирования, можно воспользоваться Ninject для создания экземпляров этой реализации. Отредактируйте файл NinjectDependencyResolver.cs в проекте GameStore.WebUI, внеся в метод AddBindings() изменения, которые показаны в примере ниже:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Web.Mvc;
using Moq;
using Ninject;
using GameStore.Domain.Abstract;
using GameStore.Domain.Entities;
using GameStore.Domain.Concrete;

namespace GameStore.WebUI.Infrastructure
{
    public class NinjectDependencyResolver : IDependencyResolver
    {
        // ...

        private void AddBindings()
        {
            kernel.Bind<IGameRepository>().To<EFGameRepository>();

            EmailSettings emailSettings = new EmailSettings
            {
                WriteAsFile = bool.Parse(ConfigurationManager
                    .AppSettings["Email.WriteAsFile"] ?? "false")
            };

            kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>()
                .WithConstructorArgument("settings", emailSettings);
        }
    }
}

Мы создали объект EmailSettings, который используется в Ninject-методе WithConstructorArgument(), так что его можно внедрять в конструктор EmailOrderProcessor, когда создаются новые экземпляры для обслуживания запросов интерфейса IOrderProcessor.

В этом примере ниже было указано значение только для одного свойства EmailSettings по имени WriteAsFile. Значение этого свойства читается с применением свойства ConfigurationManager.AppSettings, которое предоставляет доступ к настройкам приложения, размещенным в файле Web.config (из корневой папки проекта) и показанным в примере ниже:

...
<appSettings>
    <add key="webpages:Enabled" value="false" />
	<add key="webpages:Version" value="3.0.0.0" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    <add key="Email.WriteAsFile" value="true"/>
</appSettings>
...

Завершение построения контроллера Cart

Для завершения класса CartController мы должны изменить конструктор так, чтобы он требовал реализацию интерфейса IOrderProcessor, и добавить новый метод действия, который будет обрабатывать HTTP-запрос POST формы, когда пользователь щелкает на кнопке "Обработать заказ". Эти изменения показаны в примере ниже:

using System.Linq;
using System.Web.Mvc;
using GameStore.Domain.Entities;
using GameStore.Domain.Abstract;
using GameStore.WebUI.Models;

namespace GameStore.WebUI.Controllers
{
    public class CartController : Controller
    {
        private IGameRepository repository;
        private IOrderProcessor orderProcessor;

        public CartController(IGameRepository repo, IOrderProcessor processor)
        {
            repository = repo;
            orderProcessor = processor;
        }
		
		public ViewResult Checkout() {
            return View(new ShippingDetails());
        }

        [HttpPost]
        public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails)
        {
            if (cart.Lines.Count() == 0)
            {
                ModelState.AddModelError("", "Извините, ваша корзина пуста!");
            }

            if (ModelState.IsValid)
            {
                orderProcessor.ProcessOrder(cart, shippingDetails);
                cart.Clear();
                return View("Completed");
            }
            else
            {
                return View(shippingDetails);
            }
        }

        // ...
	}
}

В коде видно, что добавленный метод действия Checkout() декорирован атрибутом HttpPost, а это значит что он будет вызываться для запроса POST - в данном случае, когда пользователь отправляет форму. Мы снова полагаемся на систему привязки моделей для параметра ShippingDetails (создаваемого автоматически с применением данных формы HTTP) и параметра Cart (который создается с использованием специального связывателя).

Это изменение в конструкторе вынуждает обновить модульные тесты, созданные для класса CartController. Передача null в новом параметре конструктора позволит модульным тестам компилироваться.

Инфраструктура MVC Framework проверяет ограничения проверки достоверности, которые были применены к ShippingDetails с использованием атрибутов аннотаций данных, и через свойство ModelState сообщает нашему методу действия о любых нарушениях. Для проверки наличия проблем предназначено свойство ModelState.IsValid. Обратите внимание на вызов метода ModelState.AddModelError() для регистрации сообщения об ошибке, если в корзине нет элементов. Вскоре мы объясним, как отображать такие сообщения об ошибках.

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

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

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

// ...

[TestMethod]
public void Cannot_Checkout_Empty_Cart()
{
    // Организация - создание имитированного обработчика заказов
    Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();

    // Организация - создание пустой корзины
    Cart cart = new Cart();

    // Организация - создание деталей о доставке
    ShippingDetails shippingDetails = new ShippingDetails();

    // Организация - создание контроллера
    CartController controller = new CartController(null, mock.Object);

    // Действие
    ViewResult result = controller.Checkout(cart, shippingDetails);

    // Утверждение — проверка, что заказ не был передан обработчику 
    mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()),
        Times.Never());

    // Утверждение — проверка, что метод вернул стандартное представление 
    Assert.AreEqual("", result.ViewName);

    // Утверждение - проверка, что-представлению передана неверная модель
    Assert.AreEqual(false, result.ViewData.ModelState.IsValid);
}

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

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

// ...

[TestMethod]
public void Cannot_Checkout_Invalid_ShippingDetails()
{
    // Организация - создание имитированного обработчика заказов
    Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();

    // Организация — создание корзины с элементом
    Cart cart = new Cart();
    cart.AddItem(new Game(), 1);

    // Организация — создание контроллера
    CartController controller = new CartController(null, mock.Object);

    // Организация — добавление ошибки в модель
    controller.ModelState.AddModelError("error", "error");

    // Действие - попытка перехода к оплате
    ViewResult result = controller.Checkout(cart, new ShippingDetails());

    // Утверждение - проверка, что заказ не передается обработчику
    mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()),
        Times.Never());

    // Утверждение - проверка, что метод вернул стандартное представление
    Assert.AreEqual("", result.ViewName);

    // Утверждение - проверка, что-представлению передана неверная модель
    Assert.AreEqual(false, result.ViewData.ModelState.IsValid);
}

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

// ...

[TestMethod]
public void Can_Checkout_And_Submit_Order()
{
    // Организация - создание имитированного обработчика заказов
    Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>();

    // Организация — создание корзины с элементом
    Cart cart = new Cart();
    cart.AddItem(new Game(), 1);

    // Организация — создание контроллера
    CartController controller = new CartController(null, mock.Object);

    // Действие - попытка перехода к оплате
    ViewResult result = controller.Checkout(cart, new ShippingDetails());

    // Утверждение - проверка, что заказ передан обработчику
    mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()),
        Times.Once());

    // Утверждение - проверка, что метод возвращает представление 
    Assert.AreEqual("Completed", result.ViewName);

    // Утверждение - проверка, что представлению передается допустимая модель
    Assert.AreEqual(true, result.ViewData.ModelState.IsValid);
}

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

Отображение сообщений об ошибках проверки достоверности

Для проверки пользовательских данных инфраструктура MVC Framework будет использовать атрибуты проверки достоверности, примененные к классу ShippingDetails. Тем не менее, чтобы отобразить сообщения о проблемах пользователю, понадобится внести пару изменений. Прежде всего, необходимо предоставить пользователю сводку по всем проблемам. Это особенно важно, когда проблемы не относятся к определенным областям, таким как попытка пользователя оформить заказ, не имея товаров в корзине.

Чтобы отобразить полезную сводку по ошибкам проверки достоверности, можно воспользоваться вспомогательным методом Html.ValidationSummary(). В примере ниже показано добавление к представлению Checkout.cshtml:

@using (Html.BeginForm())
{
    @Html.ValidationSummary();
    <h3>Данные</h3>
	...
}

Следующий шаг заключается в создании нескольких стилей CSS, которые нацелены на классы, применяемые сводкой по проверке достоверности, и которые MVC Framework добавит к элементам, содержащим недопустимые данные. Для этого в папку Content проекта GameStore.WebUI добавляется новый файл типа Style Sheet (Таблица стилей) по имени ErrorStyles.css, в котором определены стили, показанные в примере ниже:

.field-validation-error {
    color: #f00;
}

.field-validation-valid {
    display: none;
}

.input-validation-error {
    border: 1px solid #f00;
    background-color: #fee;
}

.validation-summary-errors {
    font-weight: bold;
    color: #f00;
}

.validation-summary-valid {
    display: none;
}

Чтобы применить определенные стили, файл _Layout.cshtml изменяется с целью добавления элемента <link> для файла ErrorStyles.css:

...
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="~/Content/bootstrap.css" rel="stylesheet" />
    <link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
    <link href="~/Content/ErrorStyles.css" rel="stylesheet" />
    <title>@ViewBag.Title</title>
</head>
...

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

Отображение сообщений об ошибках проверки достоверности

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

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

Отображение итоговой страницы

Для завершения процесса оплаты мы отобразим пользователям страницу, которая подтверждает передачу заказа на обработку и отображает сообщение с благодарностью за покупки. Создайте в папке Views/Cart новое представление по имени Completed.cshtml и приведите его содержимое в соответствие с кодом:

@{
    ViewBag.Title = "Заказ обработан";
}

<h2>Спасибо!</h2>
<p>Спасибо за выбор нашего магазина. Заказ в кратчайшие сроки будет отправлен в службу доставки.</p>

Для интеграции этого представления в приложение никаких изменений в код вносить не придется, поскольку требуемые операторы уже были добавлены при определении метода действия Checkout() ранее. Теперь пользователи могут проходить по всему процессу, начиная с выбора товаров и заканчивая переходом к оплате. При условии, что предоставлена корректная информация о доставке (и в корзине присутствуют товары), в результате щелчка на кнопке "Обработать заказ" пользователи увидят итоговую страницу, показанную на рисунке:

Итоговая страницы обработки заказа с благодарностью
Пройди тесты
Лучший чат для C# программистов