パート 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 フォルダーを作成して整理しましょう。 プロジェクトを右クリックし、[追加] - [新しいフォルダー] を選択します。

Screenshot of the project window showing the right-click menu with the Add and New Folder options highlighted in yellow.

フォルダーに「ViewModels」という名前を付けます。

Screenshot of the Solution Explorer showing the newly created and newly named folder, View Models, highlighted with a black box.

次に、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 コントローラーをプロジェクトに追加します。

Screenshot of the Add Controller window with Shopping Cart Controller in the Controller name field and highlighted in blue.

完成した 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 に厳密に型指定され、以前と同じメソッドを使用してリスト ビュー テンプレートを使用するショッピング カート インデックス ページを作成します。

Screenshot of the Add View window showing the View name field, the View engine, Model Class, and Scaffold dropdowns, and the Use a layout file picker.

ただし、カートからアイテムを削除するために Html.ActionLink を使用する代わりに、jQuery を使用して、HTML クラスの RemoveLink を持つこのビュー内のすべてのリンクのクリック イベントを "接続" します。 このクリック イベント ハンドラーは、フォームを投稿するのではなく、RemoveFromCart コントローラー アクションに対して AJAX コールバックを行うだけです。 RemoveFromCart は JSON でシリアル化された結果を返します。これにより、jQuery コールバックが解析を行い、jQuery を使用してページに対して 4 つのクイック更新を実行します。

    1. 削除したアルバムを一覧から削除する
    1. ヘッダーのカート数を更新する
    1. 更新メッセージをユーザーに表示する
    1. カートの合計金額を更新する

削除シナリオはインデックス ビュー内の 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>

これで、ストアをクリックして、ショッピング カートとの間でアルバムの追加と削除をテストできます。 アプリケーションを実行し、ストア インデックスを参照します。

Screenshot of the Music Store window showing the genre details defined from all album data entered into the database.

次に、ジャンルをクリックしてアルバムの一覧を表示します。

Screenshot of the Music Store window showing the list of albums associated with the Disco genre in the album database.

アルバム タイトルをクリックすると、[カートに追加] ボタンを含む、更新されたアルバムの詳細ビューが表示されるようになりました。

Screenshot of the Music Store window showing the updated Album Details view and the Add to cart button.

[カートに追加] ボタンをクリックすると、ショッピング カートの概要リストと共にショッピング カート インデックス ビューが表示されます。

Screenshot of the Music Store window showing the Shopping Cart view with a summary list of all items in the cart.

ショッピング カートを読み込んだ後、[カートから削除] リンクをクリックすると、ショッピング カートに対する Ajax の更新が表示されます。

Screenshot of the Music Store window showing the Shopping Cart view with the album removed from the summary list.

未登録ユーザーが自分のカートにアイテムを追加できるショッピング カートを構築しました。 次のセクションでは、ユーザーが、登録してチェックアウト プロセスを完了できるようにします。