パート 8: ショッピング カートと Ajax 更新
作成者: Jon Galloway
MVC Music Store は、ASP.NET MVC と Visual Studio を使用した Web 開発の手順を段階的に紹介し、説明するチュートリアル アプリケーションです。
MVC Music Store は、音楽アルバムをオンラインで販売する軽量なサンプル ストア実装で、基本的なサイト管理、ユーザー サインイン、ショッピング カート機能を実装しています。
このチュートリアル シリーズでは、ASP.NET MVC Music Store サンプル アプリケーションをビルドするために実行されるすべての手順について詳しく説明します。 パート 8 では、ショッピング カートと Ajax 更新について説明します。
ユーザーは登録せずにカートにアルバムを配置できますが、チェックアウトを完了するにはゲストとして登録する必要があります。 ショッピングとチェックアウトのプロセスは 2 つのコントローラーに分かれます。1 つはアイテムをカートに匿名で追加できる ShoppingCart コントローラーで、もう 1 つはチェックアウト プロセスを処理する Checkout コントローラーです。 このセクションではショッピング カートから始め、次のセクションでチェックアウト プロセスをビルドします。
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; }
}
}
このクラスは、RecordId プロパティの [Key] 属性を除き、これまでに使用した他のクラスとかなり似ています。 Cart アイテムには、匿名ショッピングを許可する 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 クラスに対し、新しい Model クラスを公開する DbSet を含めて (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; }
}
}
ショッピング カートのビジネス ロジックの管理
次に、Models フォルダーに ShoppingCart クラスを作成します。 ShoppingCart モデルは、Cart テーブルへのデータ アクセスを処理します。 さらに、ショッピング カートに対してアイテムの追加や削除を行うためのビジネス ロジックを処理します。
ショッピング カートにアイテムを追加するためだけにアカウントのサインアップをユーザーに求めることを避けるため、ユーザーがショッピング カートにアクセスするときに一時的な一意識別子 (GUID またはグローバル一意識別子を使用) を割り当てます。 この ID は、ASP.NET Session クラスを使用して保存します。
注: ASP.NET Session は、ユーザー固有の情報を保存するのに便利な場所ですが、サイトを離れると有効期限が切れます。 大規模なサイトでは、セッション状態の誤用がパフォーマンスに影響を与える可能性があります。ここでは、デモンストレーション目的の軽度な使用なので、問題なく動作します。
ShoppingCart クラスは、次のメソッドを公開します。
AddToCart は、パラメーターとして Album を取得し、ユーザーのカートに追加します。 Cart テーブルは各アルバムの数量を追跡するため、必要に応じて新しい行を作成するロジックを含みます。または、ユーザーが既にアルバムを 1 コピー注文している場合は、数量が増えます。
RemoveFromCart は Album ID を取得し、ユーザーのカートから削除します。 ユーザーのカートにあるアルバムのコピーが 1 つだけの場合は、行が削除されます。
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();
}
}
}
ViewModel
Shopping Cart コントローラーは、Model オブジェクトに適切にマップされない複雑な情報をビューに伝達する必要があります。 ビューに合わせてモデルを変更する必要はありません。Model クラスは、ユーザー インターフェイスではなく、ドメインを表す必要があります。 1 つの解決策は、Store Manager のドロップダウン情報の場合と同様に、ViewBag クラスを使用してビューに情報を渡すことですが、ViewBag を使用して多くの情報を渡すのは管理が困難です。
これに対する解決策は、ViewModel パターンを使用することです。 このパターンを使用する場合は、特定のビュー シナリオ用に最適化され、ビュー テンプレートで必要な動的な値またはコンテンツのプロパティを公開する、厳密に型指定されたクラスを作成します。 コントローラー クラスは、これらのビュー最適化クラスを設定し、使用するビュー テンプレートに渡すことができます。 これにより、ビュー テンプレート内のタイプ セーフ、コンパイル時チェック、エディターの IntelliSense が有効になります。
Shopping Cart コントローラーで使用する 2 つのビュー モデルを作成します。1 つは ShoppingCartViewModel で、ユーザーのショッピング カートの内容を保持します。もう 1 つは ShoppingCartRemoveViewModel で、ユーザーがカートから何かを削除したときに、確認情報を表示するために使用されます。
プロジェクトのルートに新しい ViewModels フォルダーを作成して整理しましょう。 プロジェクトを右クリックし、[追加] - [新しいフォルダー] を選択します。
フォルダーに「ViewModels」という名前を付けます。
次に、ViewModels フォルダーに ShoppingCartViewModel クラスを追加します。 これには 2 つのプロパティがあります。カート アイテムのリストと、カート内の全アイテムの合計金額を保持する 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; }
}
}
次に、次の 4 つのプロパティを使用して、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; }
}
}
Shopping Cart コントローラー
Shopping Cart コントローラーには、カートへのアイテムの追加、カートからのアイテムの削除、カート内のアイテムの表示という 3 つの主な目的があります。 作成した 3 つのクラス (ShoppingCartViewModel、ShoppingCartRemoveViewModel、ShoppingCart) を使用します。 StoreController と StoreManagerController の場合と同様に、MusicStoreEntities のインスタンスを保持するフィールドを追加します。
空のコントローラー テンプレートを使用して、新しい Shopping Cart コントローラーをプロジェクトに追加します。
完成した ShoppingCart コントローラーを次に示します。 Index と Add Controller アクションは見慣れているはずです。 Remove と CartSummary コントローラー アクションは、次のセクションで説明する 2 つの特殊なケースを処理します。
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 の更新が表示されます。
未登録ユーザーが自分のカートにアイテムを追加できるショッピング カートを構築しました。 次のセクションでは、ユーザーが、登録してチェックアウト プロセスを完了できるようにします。