[ Полезный рекламный блок ]
Попробуйте свои силы в игре, где ваши навыки программирования на C# станут решающим фактором. Переходите по ссылке 🔰.
В этой статье, мы продолжим писать магазин на Asp.Net Core и Entity Framework Core. В четвертой части мы узнали каким образом адаптировать приложение GameStore для работы с более крупными объемами данных. Добавили к нему поддержку разбиения на страницы, упорядочения и поиска данных. Для ознакомления с четвертой частью, перейдите по ссылке.
В данной статье будут построены части приложения GameStore, реализующие интерфейс для покупателей, который позволит выбирать товары, просматривать корзину для покупок и оформлять заказ. Добавляемые функциональные средства в значительной степени связаны с инфраструктурой ASP.NET Core MVC и основаны на фундаменте Entity Framework Core, который был создан в предшествующих главах.
Магазин на Asp.Net Core MVC EF. Часть 5
Добавление импорта представления
Добавление начальных данных о товарах
Создание и применение миграции
Отображение товаров для покупателя
Добавление корзины для покупок
Создание контроллера и представления
Изложение материала в статье будет вестись ускоренными темпами, поскольку большая часть работы касается построения средств с использованием ASP.NET Core МVС поверх фундамента, сформированного с помощью инфраструктуры Entity Fгamework Core в предыдущих главах.
Добавление импорта представления
В предыдущей главе класс PagedList применялся в представлениях без модификации модели представления просто для демонстрации того, что средства масштабирования можно добавить, внеся минимальные изменения. В настоящей главе класс PagedList будет использоваться в представлениях напрямую, поэтому добавим содержащее его пространство имен в файл импортирования представлений Views / _ViewImports.cshtml:
1 2 3 4 |
@using GameStore @using GameStore.Models @using GameStore.Models.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
Модификация модели данных
Что бы подготовить модель данных к работе со средствами интерфейса для покупателей, добавим в класс Product свойство Description, которое позволит покупателям получить немного сведений о товаре:
1 2 3 4 5 6 7 8 9 10 11 |
public class Product { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal PurchasePrice { get; set; } public decimal RetailPrice { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } } |
Для облегчения запроса и сохранения данных по категории добавим в класс Саtegory навигационное свойство, которое инфраструктура Entity Framework Core сможет заполнять связанными объектами Product:
1 2 3 4 5 6 7 |
{ public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public IEnumerable<Product> Products { get; set; } } |
Добавление начальных данных о товарах
Нам необходима возможность переключения между крупными объемами тестовых данных и небольшими объемами реалистичных данных. С этой целью добавим в контроллер Seed код, который обеспечит наличие стандартных категорий и товаров GameStore, создадим метод CreateProductionData:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
public class SeedController : Controller { //... [HttpPost] public IActionResult CreateProductionData() { ClearData(); _context.Categories.AddRange ( new Category { Name = "Шутеры", Description = "Наделай шуму", Products = new Product[] { new Product { Name = "WarZone", Description = "Мультиплатформенная компьютерная игра в жанре многопользовательского шутера от первого лица.", PurchasePrice = 50, RetailPrice = 70}, new Product { Name = "DOOM Eternal", Description = "DOOM Eternal смогла объединить механики традиционного шутера с элементами слэшера.", PurchasePrice = 76, RetailPrice = 90}, new Product { Name = "Battlefield V", Description = "В 2019 году Infinity Ward выпустила, пожалуй, самую топовую Call of Duty за последние несколько лет. ", PurchasePrice = 100, RetailPrice = 120}, new Product { Name = "Wolfenstein 2", Description = "Bulletstorm стала лебединой песней студии People Can Fly. Это накачанный адреналином шутер с отмороженным наёмником в главной роли.", PurchasePrice = 35, RetailPrice = 50}, } }, new Category { Name = "Стратегии", Description = "Думай чем можешь", Products = new Product[] { new Product { Name = "Civilization 5", Description = "Пожалуй, лучшая современная часть «Цивилизации». В меру глубокая и понятная даже для новичка", PurchasePrice = 50, RetailPrice = 70}, new Product { Name = "Age of Empires 2", Description = "Вторую часть AOE можно считать современной классикой стратегий в реальном времени. ", PurchasePrice = 76, RetailPrice = 90}, new Product { Name = "Warcraft 3 ", Description = "История принца Артаса, который буквально продал душу ради своего народа, знакома миллионам игроков.", PurchasePrice = 100, RetailPrice = 120}, new Product { Name = "SimCity 4 ", Description = "Четвёртую часть градостроительного симулятора от создателя The Sims Уилла Райта по праву можно считать одной из лучших игр в жанре.", PurchasePrice = 35, RetailPrice = 50}, } }, new Category { Name = "Фатинги", Description = "Ударил - беги", Products = new Product[] { new Product { Name = "Mortal Kombat 11", Description = "Самое полное издание Mortal Kombat 11. Погружайтесь в две кинематографические сюжетные кампании.", PurchasePrice = 50, RetailPrice = 70}, new Product { Name = "Street Fighter V: Champion Edition", Description = "Последняя часть легендарной серии файтингов, с которой и зародился жанр, получила самое крупное обновление.", PurchasePrice = 76, RetailPrice = 90}, new Product { Name = "SOULCALIBUR VI", Description = "Душа внутри пылает – и пылает она еще ярче и пуще прежнего. Серия возвращается к своим корням с легендарными поединками с оружием.", PurchasePrice = 100, RetailPrice = 120}, new Product { Name = "TEKKEN 7", Description = "И вот, спустя 20 лет, война между членами клана Мисима, наконец, достигла своего апогея.", PurchasePrice = 35, RetailPrice = 50}, } } ); _context.SaveChanges(); return RedirectToAction(nameof(Index)); } } |
Новый метод действия создает последовательность объектов Category и устанавливает навигационное свойство Products в коллекцию объектов Product. Все объекты передаются методу AddRange() и сохраняются в БД методом SaveChanges().
Чтобы нацелиться на новый метод действия, добавим в представление Index, применяемое контроллером Seed, следующий элемент:
1 2 3 |
<div class="text-center p-3"> <button type="submit" asp-action="CreateProductionData" class="btn btn-primary">Заполнить базу продуктами</button> </div> |
Элемент button отправляет НТГР-запрос POST, результатом которого будет очистка БД и ее заполнение стандартными категориями и товарами GameStore.
Создание и применение миграции
Чтобы обновить БД, потребуется создать и применить к ней миграцию.
Для создания миграции в окне Package Manager Console введите следующую команду:
1 |
Add-Migration AddCustomer |
Для выполнения инструкций миграции, в окне Package Manager Console выполните команду:
1 |
Update-Database |
Первая команда создает новую миграцию по имени AddCustomer, которая будет содержать команды, требующиеся для подготовки БД к хранению новых объектов. Вторая команда выполняет такие команды для обновления БД.
Запустите приложение, перейдите на страницу “Начальные данные” очистите таблицы и заполните продуктами. Проверьте работу приложения.
Отображение товаров для покупателя
В последующих разделах будет добавлена поддержка для отображения списка товаров пользователю, позволяя ему фильтровать товары по категориям и просматривать товары доступные для покупки. Поддержка будет строиться на основе средств созданных ранее.
Подготовка модели данных
Чтобы приступить к части приложения, связанной с интерфейсом для покупателей, добавьте возможность запрашивания объектов Product по их категории, начав с интерфейса хранилища IProduct:
1 2 3 4 5 6 7 8 9 10 |
public interface IProduct { PagedList<Product> GetProducts(QueryOptions options, int category = 0); IEnumerable<Product> GetAllProducts(); Product GetProduct(int id); void AddProduct(Product product); void UpdateProduct(Product product); void UpdateAll(Product[] products); void DeleteProduct(Product product); } |
Внесем соответствующее изменение в класс реализации, применяя LINQ-мeтoд Where() для запрашивания на основе свойства внешнего ключа, которое ассоциирует объект Product со связанным объектом Category, изменим реализацию метода в классе ProductRepository:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class ProductRepository : IProduct { //... public PagedList<Product> GetProducts(QueryOptions options, int category = 0) { IQueryable<Product> products = _context.Products.Include(e => e.Category); if (category != 0) { products = products.Where(e => e.CategoryId == category); } return new PagedList<Product>(products, options); } } |
Интерфейс IQueryable<T> позволяет формировать запрос на основе параметров метода, создавая объект, который будет запрашивать БД, только когда он перечисляется. Это преимущество работы с объектами IQueryable<T>, хотя есть и недостаток — легкость, с которой можно непредумышленно инициировать дублированные запросы.
Создание URL возврата
Нам необходимо знать, на какой URL переходить после того, как пользователь выбрал товар. Для облегчения процесса создадим папку Infrastructure и добавим в нее файл класса по имени UrlExtensions.cs со следующим кодом:
1 2 3 4 5 6 7 |
public static class UrlExtensions { public static string PathAndQuery(this HttpRequest request) { return request.QueryString.HasValue ? $"{request.Path}{request.QueryString}" : request.Path.ToString(); } } |
В классе UrlExtensions определяется расширяющий метод PathAndQuery(), который будет использоваться в элементе form.
Перейдем в файл Views / ViewImports.cshtml, добавим туда следующее пространство имен:
1 2 3 4 5 |
@using GameStore @using GameStore.Models @using GameStore.Models.Pages @using GameStore.Infrastructure @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
Создание контроллера Store, представлений и компоновки
Чтобы предоставить контроллер, который будет представлять данные покупателю, добавим в папку Controllers файл по имени StoreController.cs и поместим в него следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class StoreController : Controller { private readonly IProduct _products; private readonly ICategory _categories; public StoreController(IProduct products, ICategory categories) { _products = products; _categories = categories; } public IActionResult Index([FromQuery(Name = "options")] QueryOptions productOptions, QueryOptions catOptions, int category) { ViewBag.Categories = _categories.GetCategories(catOptions); ViewBag.SelectedCategory = category; return View(_products.GetProducts(productOptions, category)); } } |
Для управления отображением данных Product и Category используются два объекта QueryOptions. Они применяются для получения объекта PagedList<Product>, который передается представлению как его модель, и объекта PagedList<Category>. добавляемого к ViewBag.
Чтобы обеспечить средствам интерфейса для покупателей компоновку, создадим папку Views / Store и поместим в нее файл по имени _Layout.cshtml со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - GameStore</title> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/GameStore.styles.css" asp-append-version="true" /> </head> <body> <div class="container-fluid"> <div class="row bg-dark p-4 text-white"> <div class="col-auto"><h4>Магазин игр</h4></div> <div class="col"></div> <div class="col-auto text-right"> (Корзина) </div> </div> </div> @RenderBody() <footer class="border-top footer text-muted"> <div class="container"> © 2022 - GameStore - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </div> </footer> <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> @await RenderSectionAsync("Scripts", required: false) </body> </html> |
Компоновка представляет стандартный заголовок “Магазин игр, с заполнителем вместо сводки по корзине для покупок, которая будет добавлена в приложение позже. Для отображения списка товаров добавим в папку Views / Store файл по имени Index.cshtml со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
class="container-fluid"> <div class="row no-gutters"> <div class="col-auto"> @Html.Partial("Categories", ViewBag.Categories as PagedList<Category>) </div> <div class="col"> <div class="container-fluid"> <div class="row pt-1 pb-1"> <div class="col text-center"> @Html.Partial("Pages", Model) </div> </div> <div class="row pt-1 pb-1"> <div class="col"></div> <div class="col-6 text-center form-group"> <input form="pageform" type="hidden" name="options.searchpropertyname" value="Name" /> <input form="pageform" name="options.searchterm" placeholder="Поиск..." class="form-control" /> </div> <div class="col"> <button form="pageform" class="btn btn-secondary" type="submit">Поиск</button> </div> <div class="col"></div> </div> @foreach (Product p in Model) { <div class="row"> <div class="col"> <div class="car m-1 p-1 bg-light"> <div class="bg-faded p-1"> <h4> @p.Name <span style="float:right"> <small>@p.RetailPrice</small> </span> </h4> </div> <form id="@p.Id" asp-action="AddToCart" asp-controller="Cart" method="post"> <input type="hidden" name="Id" value="@p.Id" /> <input type="hidden" name="Name" value="@p.Name" /> <input type="hidden" name="RetailPrice" value="@p.RetailPrice" /> <input type="hidden" name="returnUrl" value="@ViewContext.HttpContext.Request.PathAndQuery()" /> <span class="card-text p-1"> @(p.Description ?? "Нет описания") <button type="submit" class="btn btn-success btn-sm pull-right" style="float:right">Добавить в корзину</button> </span> </form> </div> </div> </div> } </div> </div> </div> </div> |
В представлении Index собрано вместе несколько средств, для отображения товаров, включая разбиение на страницы и поддержку поиска. Чтобы отобразить пользователю список категорий, добавим в папку Views / Store файл по имени Categories.cshtml со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
@model PagedList<Category> <div class="container-fluid mt-4"> <div class="row no-gutters"> <div class="col mt-1"> <button form="pageform" name="category" value="0" type="submit" class="btn btn-block @(ViewBag.SelectedCategory == 0 ? "btn-primary":"btn-outline-primary")"> Все </button> </div> </div> <div class="row no-gutters mt-4"></div> <div class="row no-gutters"> <div class="col mt-1"> <button form="pageform" name="catoptions.currentPage" value="@(Model.CurrentPage - 1)" class="btn btn-block btn-outline-secondary @(Model.HasPreviousPage ? "disabled":"")" type="submit"> Назад </button> </div> </div> @foreach (Category c in Model) { <div class="row no-gutters"> <div class="col mt-1"> <button form="pageform" name="category" value="@c.Id" type="submit" class="btn blockquote @(ViewBag.SelectedCategory == c.Id ? "btn-primary":"btn-outline-primary")"> @c.Name </button> </div> </div> } <div class="row no-gutters"> <div class="col mt-1"> <button form="pageform" name="catoptions.currentPage" value="@(Model.CurrentPage + 1)" class="btn blockquote btn-outline-secondary @(!Model.HasNextPage? "disable" : "")" type="submit"> Вперед </button> </div> </div> </div> |
Представление Categories отображает список доступных категорий и предлагает кнопки Previous (Предыдущая) и Next (Следующая) для листания списка. Элементы button, выбирающие категории, применяют НТМL-форму по имени pagesform для нацеливания на контроллер с помощью значения первичного ключа выбранной категории.
Для тестирования результата, запустим приложение и перейдем по адресу:
Проверьте, как работает поиск, навигация, выбор категории.
Можете так же проверить, как контроллер Store справляется с крупными объемами данных, перейдя по ссылке https://localhost:7191/seed и сгенерировав крупный объём данных.
Так как модель продукта была изменена, мы добавили описание к товару, необходимо изменить функцию наполнения товаров. Для этого, перейдем в контроллер Seed, изменим определение метода CreateSeedData():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public class SeedController : Controller { //... [HttpPost] public IActionResult CreateSeedData(int count) { ClearData(); if (count > 0) { _context.Database.SetCommandTimeout(TimeSpan.FromMinutes(10)); _context.Database.ExecuteSqlRaw("DROP PROCEDURE IF EXISTS CreateSeedData"); _context.Database.ExecuteSqlRaw($@" CREATE PROCEDURE CreateSeedData @RowCount decimal AS BEGIN SET NOCOUNT ON DECLARE @i INT = 0; DECLARE @catId INT; DECLARE @CatCount INT = @RowCount / 10; DECLARE @pprice DECIMAL(5,2); DECLARE @rprice DECIMAL(5,2); BEGIN TRANSACTION WHILE @i < @CatCount BEGIN INSERT INTO Categories (Name,Description) VALUES (CONCAT('Category-',@i), 'Test Data Category'); SET @catId = SCOPE_IDENTITY(); DECLARE @j INT = 1; WHILE @j <= 10 BEGIN SET @pprice = RAND() * (500-5+1); SET @rprice = (RAND() * @pprice) + @pprice; INSERT INTO Products (Name,CategoryId,PurchasePrice,RetailPrice,Description) VALUES (CONCAT('Product',@i,'-',@j),@catId,@pprice,@rprice,CONCAT('Description',@i,'-',@j)) SET @j = @j + 1 END SET @i = @i + 1 END COMMIT END"); _context.Database.BeginTransaction(); _context.Database.ExecuteSqlRaw($"EXEC CreateSeedData @RowCount = {count}"); _context.Database.CommitTransaction(); } return RedirectToAction(nameof(Index)); } } |
Добавление корзины для покупок
Конфигурирование сессии
Следующий шаг связан с добавлением поддержки для выбора товаров и их сохранения в корзине, которую затем можно применять для оформления заказа. В последующих темах приложение будет сконфигурировано для сохранения данных сеанса и использования этого в качестве временного хранилища выбранных товаров.
Чтобы включить сеансы в БД, добавим в класс Program, следующий код:
1 2 3 4 5 6 7 8 9 10 |
builder.Services.AddSession(options => { options.Cookie.Name = "GameStore.Session"; options.IdleTimeout = System.TimeSpan.FromHours(48); options.Cookie.HttpOnly = false; }); var app = builder.Build(); app.UseSession(); |
Средство сеансов будет сохранять только значения string. Для облегчения работы с этим средством добавьте в папку Infrastructure файл класса по имени SessionExtensions.cs со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 |
public static class SessionExtensions { public static void SetJson(this ISession session, string key, object value) { session.SetString(key, JsonConvert.SerializeObject(value)); } public static T GetJson<T>(this ISession session, string key) { var sessionData = session.GetString(key); return sessionData == null ? default(T) : JsonConvert.DeserializeObject<T>(sessionData); } } |
В классе SessionExtensions определены расширяющие методы, которые сериализируют объекты в формат JSON и восстанавливают их снова, позволяя легко сохранять простые объекты как данные сеанса.
Создание класса модели Cart
Для представления выбранных покупателем товаров добавьте в папку Models файл по имени Cart.cs и определите в нем класс со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class Cart { private List<OrderLine> selections = new List<OrderLine>(); public IEnumerable<OrderLine> Selections { get => selections; } public Cart AddItem(Product p, int quantity) { OrderLine orderLine = selections.Where(e => e.ProductId == p.Id).FirstOrDefault(); if (orderLine != null) { orderLine.Quantity += quantity; } else { selections.Add(new OrderLine { ProductId = p.Id, Product = p, Quantity = quantity }); } return this; } public Cart RemoveItem(int productId) { selections.RemoveAll(e => e.ProductId == productId); return this; } public void Clear() => selections.Clear(); } |
Класс Cart управляет коллекцией объектов OrderLine, которая представляет выбранные товары и может быть легко сохранена в БД при создании заказа.
Создание контроллера и представления
Чтобы предоставить логику, поддерживающую работу с объектами Cart, добавьте в Controllers файл по имени CartController.cs и поместите в него следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
[ViewComponent(Name = "Cart")] public class CartController : Controller { private readonly IProduct _products; private readonly IOrder _order; public CartController(IProduct products, IOrder order) { _products = products; _order = order; } private Cart GetCart() => HttpContext.Session.GetJson<Cart>("Cart") ?? new Cart(); private void SaveCart(Cart cart) => HttpContext.Session.SetJson("Cart", cart); public IActionResult Index(string returnUrl) { ViewBag.returnUrl = returnUrl; return View(GetCart()); } [HttpPost] public IActionResult AddToCart(Product product, string returnUrl) { SaveCart(GetCart().AddItem(product, 1)); return RedirectToAction(nameof(Index), new { returnUrl }); } [HttpPost] public IActionResult RemoveFromCart(int productId, string returnUrl) { SaveCart(GetCart().RemoveItem(productId)); return RedirectToAction(nameof(Index), new { returnUrl }); } public IActionResult Completed() { return View(); } public IActionResult CreateOrder() { return View(); } [HttpPost] public IActionResult CreateOrder(Order order) { order.Lines = GetCart().Selections.Select(e => new OrderLine { ProductId = e.ProductId, Quantity = e.Quantity }).ToArray(); _order.AddOrder(order); SaveCart(new Cart()); return RedirectToAction(nameof(Completed)); } public IViewComponentResult Invoke(ISession session) { return new ViewViewComponentResult() { ViewData = new ViewDataDictionary<Cart>(ViewData, session.GetJson<Cart>("Cart")) }; } } |
Контроллер определяет действия, которые добавляют и удаляют элементы из корзины, отображают содержимое корзины и предоставляют покупателю возможность оформить заказ. Ряд методов принимает параметр returnUrl, позволяющий пользователю возвратиться к списку товаров, не утрачивая параметры строки запроса, которые конфигурируют разбиение на страницы и фильтрацию по категории. Класс CartController также является компонентом представления, который будет применяться для отображения сводки по корзине в компоновке интерфейса покупателей.
Создание представлений
Чтобы снабдить новый контроллер представлением для управления корзиной, создайте папку Views / Cart и добавьте в нее файл по имени Index.cshtml со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
@model Cart @{ Layout = "~/Views/Store/_Layout.cshtml"; } <h2 class="m-3">Ваша корзина</h2> <div class="container-fluid"> <div class="row"> <div class="col font-weight-bold">Количество</div> <div class="col font-weight-bold">Товар</div> <div class="col font-weight-bold text-right">Цена</div> <div class="col font-weight-bold text-right">Общая сумма</div> <div class="col"></div> </div> @if (Model.Selections.Count() == 0) { <div class="row mt-2"> <div class="col-12"> <h4>Корзина пуста</h4> </div> </div> } else { @foreach (OrderLine line in Model.Selections) { <div class="row mt-1"> <div class="col">@line.Quantity</div> <div class="col">@line.Product.Name</div> <div class="col text-right">@line.Product.RetailPrice.ToString("f2")</div> <div class="col text-right">$@((line.Product.RetailPrice * line.Quantity).ToString("f2"))</div> <div class="col"> <form asp-action="RemoveFromCart"> <button type="submit" name="productId" value="@line.ProductId" class="btn btn-sm btn-outline-danger"> Удалить </button> </form> </div> </div> } } <div class="row mt-2"> <div class="col"></div> <div class="col"></div> <div class="col text-right font-weight-bold">Всего:</div> <div class="col text-right font-weight-bold"> $@(Model.Selections.Sum(e=>e.Product.RetailPrice * e.Quantity).ToString("f2")) </div> <div class="col"></div> </div> </div> <div class="text-align-content-center m-2"> @if (ViewBag.returnUrl != null) { <a href="@ViewBag.returnUrl" class="btn btn-outline-primary">Продолжить покупки</a> } <a asp-action="CreateOrder" class="btn btn-primary">Оформить заказ</a> </div> |
Представление отображает сводку по товарам, выбранным покупателем, и предлагает кнопки, которые позволяют возвратиться к списку товаров или продолжить оформление заказа. Чтобы собрать информацию для создания заказа, добавим в папку Views / Cart файл по имени CreateOrder.cshtml со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@model Order @{ Layout = "~/Views/Store/_Layout.cshtml"; } <h2 class="m-3">Общая информация</h2> <form asp-action="CreateOrder" method="post" class="m-4"> <div class="form-group"> <label>Ваше имя:</label> <input asp-for="CustomerName" class="form-control" /> </div> <div class="form-group"> <label>Ваше адрес:</label> <input asp-for="Address" class="form-control" /> </div> <div class="form-group"> <label>Ваша область:</label> <input asp-for="State" class="form-control" /> </div> <div class="form-group"> <label>Ваш почтовый индекс:</label> <input asp-for="ZipCode" class="form-control" /> </div> <div class="text-center m-2"> <button type="submit" class="btn btn-primary">Оформить заказ</button> <a asp-action="Index" class="btn btn-secondary">Отмена</a> </div> </form> |
Для отображения пользователю сообщения о том, что заказ создан, добавим в папку Views / Cart файл по имени Completed.cshtml со следующим содержимым:
1 2 3 4 5 6 7 8 9 |
@{ Layout = "~/Views/Store/_Layout.cshtml"; } <div class="text-center m-4"> <h2>Спасибо!</h2> <p>Спасибо за заказ!</p> <p>Мы свяжимся с Вами в ближайшее время.</p> <a asp-action="Index" asp-controller="Store" class="btn btn-primary">Ок</a> </div> |
Чтобы создать представление для виджета (графического элемента) со сводкой по корзине, создадим папку Views / Shared / Components / Cart и добавим в нее файл по имени Default.cshtml со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@model Cart @if (Model?.Selections?.Count() > 0) { <div> @Model.Selections.Count() товаров, $@(Model.Selections.Sum(e=>e.Quantity * e.Product.RetailPrice).ToString("f2")) </div> if (ViewContext.RouteData.Values["controller"] as string != "Cart") { <a asp-action="Index" asp-controller="Cart" class="btn btn-sm btn-light">В корзину</a> } } |
Представление Default отображает количество элементов в корзине и их общую стоимость. Также имеется кнопка, которая переместит на контроллер Cart, если представление было визуализировано другим контроллером. Чтобы отобразить виджет корзины, с помощью метода Component.InvokeAsync() добавим компонент представления в компоновку, используемую для функциональных средств магазина Views / Store / _Layout.cshtml:
1 2 3 |
<div class="col-auto text-right"> @await Component.InvokeAsync("Cart", Context.Session) </div> |
Запустим приложение и выполним тестирование. Для этого выберите один или несколько товаров и добавьте их в корзину.
После этого перейдите в корзину, просмотрите добавленные товары, выполните удаление любого из них.
Нажмите оформить заказ, заполните данные на форме:
Нажмите кнопку “Оформить заказ”.
Итог
В главе работа над приложением GameStore продолжилась добавлением средств интерфейса для покупателей. Был создан список товаров, который пользователь может пролистывать, искать в нем товары или фильтровать их по категориям. Выбранные товары добавляются в корзину, которая затем может применяться для оформления заказа, сохраняемого в БД. Большинство функциональных средств, добавленных в главе, используют инфраструктуру МVС для создания надстройки поверх фундамента Entity Framework Саге, образованного в предшествующих главах. Такой шаблон вы будете видеть и в собственных проектах — большой объем первоначальной конфигурации модели данных и кода, а затем набор средств пользовательского интерфейса, которые быстро встают на свои места. В следующей главе проект приложения GameStore завершается созданием веб-службы REST.
На этом статья «Магазин на Asp.Net Core MVC EF», подошла к концу, надеюсь вам было интересно. Вы можете скачать исходный код в моем репозитории — Github.
Поделитесь вашим опытом в комментариях, какой был ваш первый проект, магазин на Asp.Net Core MVC EF или что-то другое?
Так же вам может быть интересна предыдущая статья:
Вы хотите научится писать код на языке программирования C#?
Создавать различные информационные системы, состоящие из сайтов, мобильных клиентов, десктопных приложений, телеграмм-ботов и т.д.
Переходите к нам на страницу Dijix и ознакомьтесь с условиями обучения, мы специализируемся только на индивидуальных занятиях, как для начинающих, так и для более продвинутых программистов. Вы можете взять как одно занятие для проработки интересующего Вас вопроса, так и несколько, для более плотной работы. Благодаря личному кабинету, каждый студент повысит качество своего обучения, в вашем распоряжении:
- Доступ к пройденному материалу
- Тематические статьи
- Библиотека книг
- Онлайн тестирование
- Общение в закрытых группах
Живи в своем мире, программируй в нашем.