Часть 8. Корзина для покупок с обновлениями Ajax
Музыкальное хранилище MVC — это учебное приложение, которое представляет и объясняет пошаговые инструкции по использованию ASP.NET MVC и Visual Studio для веб-разработки.
Музыкальный магазин MVC — это упрощенный пример реализации магазина, который продает музыкальные альбомы в Интернете и реализует базовые функции администрирования сайтов, входа пользователей и корзины для покупок.
В этой серии учебников подробно описаны все шаги, предпринятые для создания примера приложения ASP.NET MVC Music Store. Часть 8 охватывает корзину с ajax Обновления.
Мы разрешим пользователям размещать альбомы в своей корзине без регистрации, но для завершения оформления заказа им потребуется зарегистрироваться в качестве гостей. Процесс покупки и оформления заказа будет разделен на два контроллера: Контроллер ShoppingCart, который позволяет анонимно добавлять товары в корзину, и контроллер оформления заказа, который обрабатывает процесс оформления заказа. Мы начнем с корзины в этом разделе, а затем создадим процесс оформления заказа в следующем разделе.
Добавление классов моделей Cart, Order и OrderDetail
В наших процессах Корзина покупок и Оформления заказа будут использоваться некоторые новые классы. Щелкните правой кнопкой мыши папку Models и добавьте класс Cart (Cart.cs) со следующим кодом.
using System.ComponentModel.DataAnnotations;
namespace MvcMusicStore.Models
{
public class Cart
{
[Key]
public int RecordId { get; set; }
public string CartId { get; set; }
public int AlbumId { get; set; }
public int Count { get; set; }
public System.DateTime DateCreated { get; set; }
public virtual Album Album { get; set; }
}
}
Этот класс очень похож на другие, которые мы использовали до сих пор, за исключением атрибута [Key] для свойства RecordId. Наши элементы Корзины будут иметь строковый идентификатор с именем CartID, чтобы разрешить анонимные покупки, но таблица содержит целочисленный первичный ключ с именем RecordId. По соглашению Entity Framework Code-First ожидает, что первичным ключом таблицы с именем Cart будет CartId или ID, но при необходимости мы можем легко переопределить его с помощью заметок или кода. Это пример того, как мы можем использовать простые соглашения в Entity Framework Code-First, когда они нам подходят, но мы не ограничены ими, когда они этого не делают.
Затем добавьте класс Order (Order.cs) со следующим кодом.
using System.Collections.Generic;
namespace MvcMusicStore.Models
{
public partial class Order
{
public int OrderId { get; set; }
public string Username { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public decimal Total { get; set; }
public System.DateTime OrderDate { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
}
}
Этот класс отслеживает сводку и сведения о доставке для заказа. Он еще не компилируется, так как имеет свойство навигации OrderDetails, которое зависит от класса, который мы еще не создали. Давайте исправим это, добавив класс с именем OrderDetail.cs, добавив следующий код.
namespace MvcMusicStore.Models
{
public class OrderDetail
{
public int OrderDetailId { get; set; }
public int OrderId { get; set; }
public int AlbumId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public virtual Album Album { get; set; }
public virtual Order Order { get; set; }
}
}
Мы внося последнее обновление в класс MusicStoreEntities, чтобы включить наборы DbSet, которые предоставляют эти новые классы Model, а также DbSet<Artist>. Обновленный класс MusicStoreEntities отображается, как показано ниже.
using System.Data.Entity;
namespace MvcMusicStore.Models
{
public class MusicStoreEntities : DbContext
{
public DbSet<Album> Albums { get; set; }
public DbSet<Genre> Genres { get; set; }
public DbSet<Artist> Artists {
get; set; }
public DbSet<Cart>
Carts { get; set; }
public DbSet<Order> Orders
{ get; set; }
public DbSet<OrderDetail>
OrderDetails { get; set; }
}
}
Управление бизнес-логикой корзины покупок
Далее мы создадим класс ShoppingCart в папке Models. Модель ShoppingCart обрабатывает доступ к данным таблицы Cart. Кроме того, он будет обрабатывать бизнес-логику для добавления и удаления элементов из корзины.
Так как мы не хотим требовать от пользователей регистрации учетной записи только для добавления товаров в корзину, мы назначим пользователям временный уникальный идентификатор (с помощью GUID или глобально уникального идентификатора) при доступе к корзине. Мы будем хранить этот идентификатор с помощью класса ASP.NET Session.
Примечание. Сеанс ASP.NET — это удобное место для хранения сведений о пользователях, срок действия которых истекает после того, как они покинут сайт. Хотя неправильное использование состояния сеанса может повлиять на производительность на крупных сайтах, наше легкое использование хорошо подходит для демонстрационных целей.
Класс ShoppingCart предоставляет следующие методы:
AddToCart принимает альбом в качестве параметра и добавляет его в корзину пользователя. Так как таблица Cart отслеживает количество для каждого альбома, она включает логику создания новой строки при необходимости или просто увеличения количества, если пользователь уже заказал одну копию альбома.
RemoveFromCart получает идентификатор альбома и удаляет его из корзины пользователя. Если у пользователя была только одна копия альбома в корзине, строка удаляется.
EmptyCart удаляет все элементы из корзины пользователя.
GetCartItems получает список CartItems для отображения или обработки.
GetCount получает общее количество альбомов, имеющихся у пользователя в корзине для покупок.
GetTotal вычисляет общую стоимость всех товаров в корзине.
CreateOrder преобразует корзину в заказ на этапе оформления заказа.
GetCart — это статический метод, который позволяет нашим контроллерам получить объект cart. Он использует метод GetCartId для обработки чтения CartId из сеанса пользователя. Методу GetCartId требуется HttpContextBase, чтобы он смог считывать CartId пользователя из сеанса пользователя.
Вот полный класс ShoppingCart:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcMusicStore.Models
{
public partial class ShoppingCart
{
MusicStoreEntities storeDB = new MusicStoreEntities();
string ShoppingCartId { get; set; }
public const string CartSessionKey = "CartId";
public static ShoppingCart GetCart(HttpContextBase context)
{
var cart = new ShoppingCart();
cart.ShoppingCartId = cart.GetCartId(context);
return cart;
}
// Helper method to simplify shopping cart calls
public static ShoppingCart GetCart(Controller controller)
{
return GetCart(controller.HttpContext);
}
public void AddToCart(Album album)
{
// Get the matching cart and album instances
var cartItem = storeDB.Carts.SingleOrDefault(
c => c.CartId == ShoppingCartId
&& c.AlbumId == album.AlbumId);
if (cartItem == null)
{
// Create a new cart item if no cart item exists
cartItem = new Cart
{
AlbumId = album.AlbumId,
CartId = ShoppingCartId,
Count = 1,
DateCreated = DateTime.Now
};
storeDB.Carts.Add(cartItem);
}
else
{
// If the item does exist in the cart,
// then add one to the quantity
cartItem.Count++;
}
// Save changes
storeDB.SaveChanges();
}
public int RemoveFromCart(int id)
{
// Get the cart
var cartItem = storeDB.Carts.Single(
cart => cart.CartId == ShoppingCartId
&& cart.RecordId == id);
int itemCount = 0;
if (cartItem != null)
{
if (cartItem.Count > 1)
{
cartItem.Count--;
itemCount = cartItem.Count;
}
else
{
storeDB.Carts.Remove(cartItem);
}
// Save changes
storeDB.SaveChanges();
}
return itemCount;
}
public void EmptyCart()
{
var cartItems = storeDB.Carts.Where(
cart => cart.CartId == ShoppingCartId);
foreach (var cartItem in cartItems)
{
storeDB.Carts.Remove(cartItem);
}
// Save changes
storeDB.SaveChanges();
}
public List<Cart> GetCartItems()
{
return storeDB.Carts.Where(
cart => cart.CartId == ShoppingCartId).ToList();
}
public int GetCount()
{
// Get the count of each item in the cart and sum them up
int? count = (from cartItems in storeDB.Carts
where cartItems.CartId == ShoppingCartId
select (int?)cartItems.Count).Sum();
// Return 0 if all entries are null
return count ?? 0;
}
public decimal GetTotal()
{
// Multiply album price by count of that album to get
// the current price for each of those albums in the cart
// sum all album price totals to get the cart total
decimal? total = (from cartItems in storeDB.Carts
where cartItems.CartId == ShoppingCartId
select (int?)cartItems.Count *
cartItems.Album.Price).Sum();
return total ?? decimal.Zero;
}
public int CreateOrder(Order order)
{
decimal orderTotal = 0;
var cartItems = GetCartItems();
// Iterate over the items in the cart,
// adding the order details for each
foreach (var item in cartItems)
{
var orderDetail = new OrderDetail
{
AlbumId = item.AlbumId,
OrderId = order.OrderId,
UnitPrice = item.Album.Price,
Quantity = item.Count
};
// Set the order total of the shopping cart
orderTotal += (item.Count * item.Album.Price);
storeDB.OrderDetails.Add(orderDetail);
}
// Set the order's total to the orderTotal count
order.Total = orderTotal;
// Save the order
storeDB.SaveChanges();
// Empty the shopping cart
EmptyCart();
// Return the OrderId as the confirmation number
return order.OrderId;
}
// We're using HttpContextBase to allow access to cookies.
public string GetCartId(HttpContextBase context)
{
if (context.Session[CartSessionKey] == null)
{
if (!string.IsNullOrWhiteSpace(context.User.Identity.Name))
{
context.Session[CartSessionKey] =
context.User.Identity.Name;
}
else
{
// Generate a new random GUID using System.Guid class
Guid tempCartId = Guid.NewGuid();
// Send tempCartId back to client as a cookie
context.Session[CartSessionKey] = tempCartId.ToString();
}
}
return context.Session[CartSessionKey].ToString();
}
// When a user has logged in, migrate their shopping cart to
// be associated with their username
public void MigrateCart(string userName)
{
var shoppingCart = storeDB.Carts.Where(
c => c.CartId == ShoppingCartId);
foreach (Cart item in shoppingCart)
{
item.CartId = userName;
}
storeDB.SaveChanges();
}
}
}
Модели представлений
Нашему контроллеру корзины покупок потребуется передать некоторые сложные сведения в свои представления, которые не сопоставляются с нашими объектами модели. Мы не хотим изменять модели в соответствии с нашими представлениями; Классы моделей должны представлять наш домен, а не пользовательский интерфейс. Одним из решений было бы передать информацию в представления с помощью класса ViewBag, как это было с раскрывающимся списком диспетчера магазинов, но передача большого объема информации через ViewBag становится трудной для управления.
Решением этой проблемы является использование шаблона ViewModel . При использовании этого шаблона мы создаем строго типизированные классы, оптимизированные для конкретных сценариев представления и предоставляющие свойства для динамических значений и содержимого, необходимых для шаблонов представлений. Затем классы контроллера могут заполнить и передать эти классы, оптимизированные для просмотра, в шаблон представления для использования. Это позволяет обеспечить безопасность типов, проверку во время компиляции и редактор IntelliSense в шаблонах представлений.
Мы создадим две модели представления для использования в нашем контроллере корзины: ShoppingCartViewModel будет содержать содержимое корзины пользователя, а ShoppingCartRemoveViewModel будет использоваться для отображения сведений подтверждения, когда пользователь удаляет что-то из корзины.
Давайте создадим новую папку ViewModels в корневом каталоге проекта, чтобы все было упорядочено. Щелкните проект правой кнопкой мыши и выберите Добавить / Создать папку.
Назовите папку ViewModels.
Затем добавьте класс ShoppingCartViewModel в папку ViewModels. Он имеет два свойства: список элементов корзины и десятичное значение для хранения общей цены для всех товаров в корзине.
using System.Collections.Generic;
using MvcMusicStore.Models;
namespace MvcMusicStore.ViewModels
{
public class ShoppingCartViewModel
{
public List<Cart> CartItems { get; set; }
public decimal CartTotal { get; set; }
}
}
Теперь добавьте ShoppingCartRemoveViewModel в папку ViewModels со следующими четырьмя свойствами.
namespace MvcMusicStore.ViewModels
{
public class ShoppingCartRemoveViewModel
{
public string Message { get; set; }
public decimal CartTotal { get; set; }
public int CartCount { get; set; }
public int ItemCount { get; set; }
public int DeleteId { get; set; }
}
}
Контроллер корзины для покупок
Контроллер корзины покупок имеет три main цели: добавление товаров в корзину, удаление товаров из корзины и просмотр элементов в корзине. Он будет использовать три только что созданных класса: ShoppingCartViewModel, ShoppingCartRemoveViewModel и ShoppingCart. Как и в StoreController и StoreManagerController, мы добавим поле для хранения экземпляра MusicStoreEntities.
Добавьте новый контроллер корзины покупок в проект с помощью шаблона Пустой контроллер.
Вот полный ShoppingCart Контроллер. Действия Index и Add Controller должны выглядеть очень хорошо. Действия контроллера Remove и CartSummary обрабатывают два особых случая, которые мы обсудим в следующем разделе.
using System.Linq;
using System.Web.Mvc;
using MvcMusicStore.Models;
using MvcMusicStore.ViewModels;
namespace MvcMusicStore.Controllers
{
public class ShoppingCartController : Controller
{
MusicStoreEntities storeDB = new MusicStoreEntities();
//
// GET: /ShoppingCart/
public ActionResult Index()
{
var cart = ShoppingCart.GetCart(this.HttpContext);
// Set up our ViewModel
var viewModel = new ShoppingCartViewModel
{
CartItems = cart.GetCartItems(),
CartTotal = cart.GetTotal()
};
// Return the view
return View(viewModel);
}
//
// GET: /Store/AddToCart/5
public ActionResult AddToCart(int id)
{
// Retrieve the album from the database
var addedAlbum = storeDB.Albums
.Single(album => album.AlbumId == id);
// Add it to the shopping cart
var cart = ShoppingCart.GetCart(this.HttpContext);
cart.AddToCart(addedAlbum);
// Go back to the main store page for more shopping
return RedirectToAction("Index");
}
//
// AJAX: /ShoppingCart/RemoveFromCart/5
[HttpPost]
public ActionResult RemoveFromCart(int id)
{
// Remove the item from the cart
var cart = ShoppingCart.GetCart(this.HttpContext);
// Get the name of the album to display confirmation
string albumName = storeDB.Carts
.Single(item => item.RecordId == id).Album.Title;
// Remove from cart
int itemCount = cart.RemoveFromCart(id);
// Display the confirmation message
var results = new ShoppingCartRemoveViewModel
{
Message = Server.HtmlEncode(albumName) +
" has been removed from your shopping cart.",
CartTotal = cart.GetTotal(),
CartCount = cart.GetCount(),
ItemCount = itemCount,
DeleteId = id
};
return Json(results);
}
//
// GET: /ShoppingCart/CartSummary
[ChildActionOnly]
public ActionResult CartSummary()
{
var cart = ShoppingCart.GetCart(this.HttpContext);
ViewData["CartCount"] = cart.GetCount();
return PartialView("CartSummary");
}
}
}
Обновления Ajax с jQuery
Далее мы создадим страницу индекса корзины покупок, которая строго типизирована в ShoppingCartViewModel и использует шаблон Представления списка, используя тот же метод, что и раньше.
Однако вместо использования Html.ActionLink для удаления элементов из корзины мы будем использовать jQuery, чтобы "подключить" событие щелчка для всех ссылок в этом представлении, имеющих HTML-класс RemoveLink. Вместо публикации формы этот обработчик событий щелчка просто выполнит обратный вызов AJAX к нашему действию контроллера RemoveFromCart. Функция RemoveFromCart возвращает сериализованный результат JSON, который затем выполняется нашим обратным вызовом jQuery и выполняет четыре быстрых обновления страницы с помощью jQuery:
-
- Удаляет удаленный альбом из списка
-
- Обновления число корзин в заголовке
-
- Отображает сообщение об обновлении для пользователя
-
- Обновления общей стоимости корзины
Так как сценарий удаления обрабатывается обратным вызовом Ajax в представлении индекса, дополнительное представление для действия RemoveFromCart не требуется. Ниже приведен полный код для представления /ShoppingCart/Index:
@model MvcMusicStore.ViewModels.ShoppingCartViewModel
@{
ViewBag.Title = "Shopping Cart";
}
<script src="/Scripts/jquery-1.4.4.min.js"
type="text/javascript"></script>
<script type="text/javascript">
$(function () {
// Document.ready -> link up remove event handler
$(".RemoveLink").click(function () {
// Get the id from the link
var recordToDelete = $(this).attr("data-id");
if (recordToDelete != '') {
// Perform the ajax post
$.post("/ShoppingCart/RemoveFromCart", {"id": recordToDelete },
function (data) {
// Successful requests get here
// Update the page elements
if (data.ItemCount == 0) {
$('#row-' + data.DeleteId).fadeOut('slow');
} else {
$('#item-count-' + data.DeleteId).text(data.ItemCount);
}
$('#cart-total').text(data.CartTotal);
$('#update-message').text(data.Message);
$('#cart-status').text('Cart (' + data.CartCount + ')');
});
}
});
});
</script>
<h3>
<em>Review</em> your cart:
</h3>
<p class="button">
@Html.ActionLink("Checkout
>>", "AddressAndPayment", "Checkout")
</p>
<div id="update-message">
</div>
<table>
<tr>
<th>
Album Name
</th>
<th>
Price (each)
</th>
<th>
Quantity
</th>
<th></th>
</tr>
@foreach (var item in
Model.CartItems)
{
<tr id="row-@item.RecordId">
<td>
@Html.ActionLink(item.Album.Title,
"Details", "Store", new { id = item.AlbumId }, null)
</td>
<td>
@item.Album.Price
</td>
<td id="item-count-@item.RecordId">
@item.Count
</td>
<td>
<a href="#" class="RemoveLink"
data-id="@item.RecordId">Remove
from cart</a>
</td>
</tr>
}
<tr>
<td>
Total
</td>
<td>
</td>
<td>
</td>
<td id="cart-total">
@Model.CartTotal
</td>
</tr>
</table>
Чтобы проверить это, мы должны иметь возможность добавлять элементы в нашу корзину. Мы обновим представление "Сведения о Магазине ", добавив кнопку "Добавить в корзину". Пока мы в ней, мы можем включить некоторые дополнительные сведения об альбоме, которые мы добавили с момента последнего обновления этого представления: Жанр, Исполнитель, Цена и Обложка альбома. Появится обновленный код представления Сведений о магазине, как показано ниже.
@model MvcMusicStore.Models.Album
@{
ViewBag.Title = "Album - " + Model.Title;
}
<h2>@Model.Title</h2>
<p>
<img alt="@Model.Title"
src="@Model.AlbumArtUrl" />
</p>
<div id="album-details">
<p>
<em>Genre:</em>
@Model.Genre.Name
</p>
<p>
<em>Artist:</em>
@Model.Artist.Name
</p>
<p>
<em>Price:</em>
@String.Format("{0:F}",
Model.Price)
</p>
<p class="button">
@Html.ActionLink("Add to
cart", "AddToCart",
"ShoppingCart", new { id = Model.AlbumId }, "")
</p>
</div>
Теперь мы можем щелкнуть магазин и проверить добавление и удаление альбомов в корзину и из нашей корзины. Запустите приложение и перейдите к индексу Магазина.
Затем щелкните жанр, чтобы просмотреть список альбомов.
Щелкнув название альбома, вы увидите обновленное представление Сведений об альбоме, включая кнопку "Добавить в корзину".
При нажатии кнопки "Добавить в корзину" отображается представление индекса корзины покупок со списком сводки корзины.
После загрузки корзины можно щелкнуть ссылку Удалить из корзины, чтобы увидеть обновление Ajax для вашей корзины.
Мы создали рабочую корзину для покупок, которая позволяет незарегистрированным пользователям добавлять элементы в свою корзину. В следующем разделе мы разрешим им зарегистрировать и завершить процесс оформления заказа.