8부: Ajax 업데이트와 쇼핑 카트
MVC Music Store는 웹 개발을 위해 ASP.NET MVC 및 Visual Studio를 사용하는 방법을 소개하고 단계별로 설명하는 자습서 애플리케이션입니다.
MVC Music Store는 온라인으로 음악 앨범을 판매하고 기본 사이트 관리, 사용자 로그인 및 쇼핑 카트 기능을 구현하는 간단한 샘플 저장소 구현입니다.
이 자습서 시리즈에서는 ASP.NET MVC Music Store 샘플 애플리케이션을 빌드하기 위해 수행된 모든 단계를 자세히 설명합니다. 8부에서는 아약스 업데이트 쇼핑 카트를 다룹니다.
사용자가 등록하지 않고 카트에 앨범을 배치할 수 있지만 체크 아웃을 완료하려면 게스트로 등록해야 합니다. 쇼핑 및 체크 아웃 프로세스는 카트에 항목을 익명으로 추가할 수 있는 ShoppingCart 컨트롤러와 체크 아웃 프로세스를 처리하는 체크 아웃 컨트롤러의 두 컨트롤러로 구분됩니다. 이 섹션에서는 쇼핑 카트로 시작한 다음, 다음 섹션에서 체크 아웃 프로세스를 빌드합니다.
카트, 주문 및 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; }
}
}
이 클래스는 RecordId 속성에 대한 [Key] 특성을 제외하고 지금까지 사용한 다른 클래스와 매우 유사합니다. 카트 항목에는 익명 쇼핑을 허용하는 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; }
}
}
또한 DbSet<Artist>를 포함하여 새 모델 클래스를 노출하는 DbSets를 포함하도록 MusicStoreEntities 클래스를 마지막으로 업데이트합니다. 업데이트된 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; }
}
}
쇼핑 카트 비즈니스 논리 관리
다음으로 Models 폴더에 ShoppingCart 클래스를 만듭니다. ShoppingCart 모델은 카트 테이블에 대한 데이터 액세스를 처리합니다. 또한 쇼핑 카트에서 항목을 추가하고 제거하기 위한 비즈니스 논리를 처리합니다.
사용자가 쇼핑 카트에 항목을 추가하기 위해 계정에 등록하도록 요구하지 않기 때문에 쇼핑 카트에 액세스할 때 사용자에게 임시 고유 식별자(GUID 또는 전역 고유 식별자 사용)를 할당합니다. ASP.NET Session 클래스를 사용하여 이 ID를 저장합니다.
참고: ASP.NET 세션은 사이트를 떠난 후 만료되는 사용자별 정보를 저장하는 편리한 장소입니다. 세션 상태의 오용은 대규모 사이트에 성능에 영향을 줄 수 있지만 간단한 사용은 데모 목적으로 잘 작동합니다.
ShoppingCart 클래스는 다음 메서드를 노출합니다.
AddToCart 는 Album을 매개 변수로 사용하여 사용자의 카트에 추가합니다. Cart 테이블은 각 앨범의 수량을 추적하므로 필요한 경우 새 행을 만들거나 사용자가 이미 앨범 복사본 하나를 주문한 경우 수량을 증가시키기 위한 논리가 포함되어 있습니다.
RemoveFromCart 는 앨범 ID를 가져와서 사용자의 카트에서 제거합니다. 사용자가 카트에 앨범 복사본이 하나만 있으면 행이 제거됩니다.
EmptyCart 는 사용자의 쇼핑 카트에서 모든 항목을 제거합니다.
GetCartItems는 표시 또는 처리를 위해 CartItems 목록을 검색합니다.
GetCount 는 사용자가 장바구니에 있는 총 앨범 수를 검색합니다.
GetTotal 은 카트에 있는 모든 항목의 총 비용을 계산합니다.
CreateOrder 는 체크 아웃 단계에서 쇼핑 카트를 주문으로 변환합니다.
GetCart 은 컨트롤러가 카트 개체를 가져올 수 있는 정적 메서드입니다. GetCartId 메서드를 사용하여 사용자 세션에서 CartId 읽기를 처리합니다. GetCartId 메서드에는 사용자의 세션에서 사용자의 CartId를 읽을 수 있도록 HttpContextBase가 필요합니다.
전체 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();
}
}
}
ViewModels
쇼핑 카트 컨트롤러는 모델 개체에 명확하게 매핑되지 않는 보기에 몇 가지 복잡한 정보를 전달해야 합니다. 우리의 견해에 맞게 모델을 수정하고 싶지는 않습니다. 모델 클래스는 사용자 인터페이스가 아닌 도메인을 나타내야 합니다. 한 가지 해결 방법은 Store Manager 드롭다운 정보와 마찬가지로 ViewBag 클래스를 사용하여 뷰에 정보를 전달하는 것이지만, ViewBag을 통해 많은 정보를 전달하면 관리하기가 어려워집니다.
이에 대한 해결 방법은 ViewModel 패턴을 사용하는 것입니다. 이 패턴을 사용하는 경우 특정 보기 시나리오에 최적화되고 보기 템플릿에 필요한 동적 값/콘텐츠에 대한 속성을 노출하는 강력한 형식의 클래스를 만듭니다. 그런 다음 컨트롤러 클래스를 채우고 이러한 보기 최적화 클래스를 사용할 보기 템플릿에 전달할 수 있습니다. 이렇게 하면 보기 템플릿 내에서 형식 안전성, 컴파일 시간 검사 및 편집기 IntelliSense를 사용할 수 있습니다.
쇼핑 카트 컨트롤러에서 사용할 두 가지 보기 모델을 만듭니다. ShoppingCartViewModel은 사용자의 쇼핑 카트 콘텐츠를 보관하고, 사용자가 카트에서 항목을 제거할 때 ShoppingCartRemoveViewModel을 사용하여 확인 정보를 표시합니다.
프로젝트의 루트에 새 ViewModels 폴더를 만들어 정리해 보겠습니다. 프로젝트를 마우스 오른쪽 단추로 클릭하고 추가/새 폴더를 선택합니다.
폴더 이름을 ViewModels로 지정합니다.
그런 다음 ViewModels 폴더에 ShoppingCartViewModel 클래스를 추가합니다. 여기에는 카트 항목 목록과 카트의 모든 항목에 대한 총 가격을 보유하는 10진수 값의 두 가지 속성이 있습니다.
using System.Collections.Generic;
using MvcMusicStore.Models;
namespace MvcMusicStore.ViewModels
{
public class ShoppingCartViewModel
{
public List<Cart> CartItems { get; set; }
public decimal CartTotal { get; set; }
}
}
이제 다음 네 가지 속성을 사용하여 ViewModels 폴더에 ShoppingCartRemoveViewModel을 추가합니다.
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; }
}
}
쇼핑 카트 컨트롤러
쇼핑 카트 컨트롤러에는 카트에 항목 추가, 카트에서 항목 제거, 카트의 항목 보기 등 세 가지 기본 목적이 있습니다. 방금 만든 세 가지 클래스인 ShoppingCartViewModel, ShoppingCartRemoveViewModel 및 ShoppingCart를 사용합니다. StoreController 및 StoreManagerController와 마찬가지로 MusicStoreEntities의 instance 저장할 필드를 추가합니다.
빈 컨트롤러 템플릿을 사용하여 프로젝트에 새 쇼핑 카트 컨트롤러를 추가합니다.
전체 ShoppingCart 컨트롤러는 다음과 같습니다. 인덱스 및 컨트롤러 추가 작업은 매우 친숙해 보일 것입니다. 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");
}
}
}
jQuery를 사용하여 Ajax 업데이트
다음에는 ShoppingCartViewModel에 강력한 형식의 쇼핑 카트 인덱스 페이지를 만들고 이전과 동일한 방법을 사용하여 목록 보기 템플릿을 사용합니다.
그러나 Html.ActionLink를 사용하여 카트에서 항목을 제거하는 대신 jQuery를 사용하여 HTML 클래스 RemoveLink가 있는 이 보기의 모든 링크에 대한 클릭 이벤트를 "연결"합니다. 폼을 게시하는 대신 이 클릭 이벤트 처리기는 RemoveFromCart 컨트롤러 작업에 대한 AJAX 콜백을 수행합니다. RemoveFromCart는 JSON 직렬화된 결과를 반환하며, jQuery 콜백은 jQuery를 사용하여 페이지에 대한 4가지 빠른 업데이트를 구문 분석하고 수행합니다.
-
- 목록에서 삭제된 앨범을 제거합니다.
-
- 헤더의 카트 수 업데이트
-
- 사용자에게 업데이트 메시지를 표시합니다.
-
- 카트 총 가격 업데이트
인덱스 보기 내의 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 업데이트를 볼 수 있습니다.
등록되지 않은 사용자가 카트에 항목을 추가할 수 있도록 작동하는 쇼핑 카트를 만들었습니다. 다음 섹션에서는 체크 아웃 프로세스를 등록하고 완료할 수 있습니다.