Bagikan melalui


Bagian 8: Kelir Belanja dengan Ajax Updates

oleh Jon Galloway

MVC Music Store adalah aplikasi tutorial yang memperkenalkan dan menjelaskan langkah demi langkah cara menggunakan ASP.NET MVC dan Visual Studio untuk pengembangan web.

MVC Music Store adalah implementasi penyimpanan sampel ringan yang menjual album musik secara online, dan mengimplementasikan administrasi situs dasar, rincian masuk pengguna, dan fungsionalitas ke cart belanja.

Seri tutorial ini merinci semua langkah yang diambil untuk membangun aplikasi sampel MVC Music Store ASP.NET. Bagian 8 mencakup Keranjang Belanja dengan Ajax Updates.

Kami akan mengizinkan pengguna untuk menempatkan album di ke cart mereka tanpa mendaftar, tetapi mereka harus mendaftar sebagai tamu untuk menyelesaikan checkout. Proses belanja dan checkout akan dipisahkan menjadi dua pengontrol: Pengontrol ShoppingCart yang memungkinkan penambahan item secara anonim ke keranjang, dan Pengontrol Checkout yang menangani proses checkout. Kita akan mulai dengan Shopping Cart di bagian ini, lalu membangun proses Checkout di bagian berikut.

Menambahkan kelas model Cart, Order, dan OrderDetail

Proses Shopping Cart and Checkout kami akan menggunakan beberapa kelas baru. Klik kanan folder Model dan tambahkan kelas Cart (Cart.cs) dengan kode berikut.

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; }
    }
}

Kelas ini sangat mirip dengan yang lain yang telah kami gunakan sejauh ini, dengan pengecualian atribut [Kunci] untuk properti RecordId. Item Kelir kami akan memiliki pengidentifikasi string bernama CartID untuk memungkinkan belanja anonim, tetapi tabel menyertakan kunci primer bilangan bulat bernama RecordId. Berdasarkan konvensi, Entity Framework Code-First mengharapkan bahwa kunci primer untuk tabel bernama Cart akan berupa CartId atau ID, tetapi kita dapat dengan mudah mengambil alihnya melalui anotasi atau kode jika kita mau. Ini adalah contoh bagaimana kita dapat menggunakan konvensi sederhana dalam Kerangka Kerja Entitas Code-First ketika sesuai dengan kita, tetapi kita tidak dibatasi oleh mereka ketika tidak.

Selanjutnya, tambahkan kelas Pesanan (Order.cs) dengan kode berikut.

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; }
    }
}

Kelas ini melacak ringkasan dan informasi pengiriman untuk pesanan. Ini belum akan dikompilasi, karena memiliki properti navigasi OrderDetails yang tergantung pada kelas yang belum kita buat. Mari kita perbaiki sekarang dengan menambahkan kelas bernama OrderDetail.cs, menambahkan kode berikut.

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; }
    }
}

Kami akan membuat satu pembaruan terakhir ke kelas MusicStoreEntities kami untuk menyertakan DbSets yang mengekspos kelas Model baru tersebut, juga termasuk Artis> DbSet<. Kelas MusicStoreEntities yang diperbarui muncul seperti di bawah ini.

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; }
    }
}

Mengelola logika bisnis Kedai Belanja

Selanjutnya, kita akan membuat kelas ShoppingCart di folder Model. Model ShoppingCart menangani akses data ke tabel Keranjang. Selain itu, ini akan menangani logika bisnis untuk menambahkan dan menghapus item dari ke cart belanja.

Karena kami tidak ingin mengharuskan pengguna untuk mendaftar ke akun hanya untuk menambahkan item ke ke cart belanja mereka, kami akan menetapkan pengidentifikasi unik sementara kepada pengguna (menggunakan GUID, atau pengidentifikasi unik global) saat mereka mengakses ke cart belanja. Kami akan menyimpan ID ini menggunakan kelas Sesi ASP.NET.

Catatan: Sesi ASP.NET adalah tempat yang nyaman untuk menyimpan informasi spesifik pengguna yang akan kedaluwarsa setelah mereka meninggalkan situs. Meskipun penyalahgunaan status sesi dapat memiliki implikasi performa pada situs yang lebih besar, penggunaan cahaya kami akan bekerja dengan baik untuk tujuan demonstrasi.

Kelas ShoppingCart mengekspos metode berikut:

AddToCart mengambil Album sebagai parameter dan menambahkannya ke keranjang pengguna. Karena tabel Keranjang melacak kuantitas untuk setiap album, itu termasuk logika untuk membuat baris baru jika diperlukan atau hanya menambah kuantitas jika pengguna telah memesan satu salinan album.

RemoveFromCart mengambil ID Album dan menghapusnya dari keranjang pengguna. Jika pengguna hanya memiliki satu salinan album di ke cart mereka, baris akan dihapus.

EmptyCart menghapus semua item dari keranjang belanja pengguna.

GetCartItems mengambil daftar CartItems untuk ditampilkan atau diproses.

GetCount mengambil jumlah total album yang dimiliki pengguna di keranjang belanja mereka.

GetTotal menghitung total biaya semua item dalam ke cart.

CreateOrder mengonversi ke cart belanja menjadi pesanan selama fase checkout.

GetCart adalah metode statis yang memungkinkan pengontrol kami untuk mendapatkan objek keranjang. Ini menggunakan metode GetCartId untuk menangani pembacaan CartId dari sesi pengguna. Metode GetCartId memerlukan HttpContextBase sehingga dapat membaca CartId pengguna dari sesi pengguna.

Berikut kelas ShoppingCart lengkapnya:

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

Pengontrol Kelir Belanja kami perlu mengomunikasikan beberapa informasi kompleks ke tampilannya yang tidak dipetakan dengan bersih ke objek Model kami. Kami tidak ingin memodifikasi Model agar sesuai dengan pandangan kami; Kelas model harus mewakili domain kami, bukan antarmuka pengguna. Salah satu solusinya adalah meneruskan informasi ke Tampilan kami menggunakan kelas ViewBag, seperti yang kami lakukan dengan informasi dropdown Store Manager, tetapi meneruskan banyak informasi melalui ViewBag menjadi sulit dikelola.

Solusi untuk ini adalah menggunakan pola ViewModel . Saat menggunakan pola ini, kami membuat kelas dengan jenis kuat yang dioptimalkan untuk skenario tampilan spesifik kami, dan yang mengekspos properti untuk nilai/konten dinamis yang diperlukan oleh templat tampilan kami. Kelas pengontrol kami kemudian dapat mengisi dan meneruskan kelas yang dioptimalkan tampilan ini ke templat tampilan kami untuk digunakan. Ini memungkinkan keamanan jenis, pemeriksaan waktu kompilasi, dan editor IntelliSense dalam templat tampilan.

Kami akan membuat dua Model Tampilan untuk digunakan di pengontrol Keranjang Belanja kami: ShoppingCartViewModel akan menyimpan konten keranjang belanja pengguna, dan ShoppingCartRemoveViewModel akan digunakan untuk menampilkan informasi konfirmasi saat pengguna menghapus sesuatu dari keranjang mereka.

Mari kita buat folder ViewModels baru di akar proyek kita untuk menjaga semuanya tetap terorganisir. Klik kanan proyek, pilih Tambahkan/ Folder Baru.

Cuplikan layar jendela proyek memperlihatkan menu klik kanan dengan opsi Tambahkan dan Folder Baru disorot dengan warna kuning.

Beri nama folder ViewModels.

Cuplikan layar Penjelajah Solusi memperlihatkan folder yang baru dibuat dan baru diberi nama, Tampilkan Model, disorot dengan kotak hitam.

Selanjutnya, tambahkan kelas ShoppingCartViewModel di folder ViewModels. Ini memiliki dua properti: daftar item Ke cart, dan nilai desimal untuk menahan harga total untuk semua item di keliru.

using System.Collections.Generic;
using MvcMusicStore.Models;
 
namespace MvcMusicStore.ViewModels
{
    public class ShoppingCartViewModel
    {
        public List<Cart> CartItems { get; set; }
        public decimal CartTotal { get; set; }
    }
}

Sekarang tambahkan ShoppingCartRemoveViewModel ke folder ViewModels, dengan empat properti berikut.

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; }
    }
}

Pengontrol Kelir Belanja

Pengontrol Ke cart Belanja memiliki tiga tujuan utama: menambahkan item ke keramaian, menghapus item dari ke cart, dan melihat item di ke cart. Ini akan menggunakan tiga kelas yang baru saja kita buat: ShoppingCartViewModel, ShoppingCartRemoveViewModel, dan ShoppingCart. Seperti di StoreController dan StoreManagerController, kita akan menambahkan bidang untuk menyimpan instans MusicStoreEntities.

Tambahkan pengontrol Shopping Cart baru ke proyek menggunakan templat Pengontrol kosong.

Cuplikan layar jendela Tambahkan Pengontrol dengan Pengontrol Kedai Belanja di bidang Nama pengontrol dan disorot dengan warna biru.

Berikut adalah Pengontrol ShoppingCart lengkap. Tindakan Indeks dan Tambahkan Pengontrol akan terlihat sangat akrab. Tindakan Pengontrol Remove dan CartSummary menangani dua kasus khusus, yang akan kita bahas di bagian berikut.

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 Updates dengan jQuery

Selanjutnya kita akan membuat halaman Indeks Keranjang Belanja yang sangat ditik ke ShoppingCartViewModel dan menggunakan templat Tampilan Daftar menggunakan metode yang sama seperti sebelumnya.

Cuplikan layar jendela Tambahkan Tampilan memperlihatkan bidang Nama tampilan, menu dropdown Lihat mesin, Kelas Model, dan Perancah, dan pemilih Gunakan file tata letak.

Namun, alih-alih menggunakan Html.ActionLink untuk menghapus item dari kemudi, kita akan menggunakan jQuery untuk "menyambungkan" peristiwa klik untuk semua tautan dalam tampilan ini yang memiliki kelas HTML RemoveLink. Daripada memposting formulir, penanganan aktivitas klik ini hanya akan membuat panggilan balik AJAX ke tindakan pengontrol RemoveFromCart kami. RemoveFromCart mengembalikan hasil serial JSON, yang kemudian diurai oleh panggilan balik jQuery kami dan melakukan empat pembaruan cepat ke halaman menggunakan jQuery:

    1. Menghapus album yang dihapus dari daftar
    1. Updates jumlah kelir di header
    1. Menampilkan pesan pembaruan kepada pengguna
    1. Updates harga total ke cart

Karena skenario hapus sedang ditangani oleh panggilan balik Ajax dalam tampilan Indeks, kami tidak memerlukan tampilan tambahan untuk tindakan RemoveFromCart. Berikut adalah kode lengkap untuk tampilan /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>

Untuk menguji hal ini, kita harus dapat menambahkan item ke keramaian belanja kita. Kami akan memperbarui tampilan Detail Toko kami untuk menyertakan tombol "Tambahkan ke ke cart". Saat berada di dalamnya, kami dapat menyertakan beberapa informasi tambahan Album yang telah kami tambahkan sejak terakhir kali memperbarui tampilan ini: Genre, Artis, Harga, dan Album Art. Kode tampilan Detail Penyimpanan yang diperbarui muncul seperti yang ditunjukkan di bawah ini.

@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>

Sekarang kita dapat mengeklik toko dan menguji penambahan dan penghapusan Album ke dan dari ke cart belanja kami. Jalankan aplikasi dan telusuri ke Indeks Penyimpanan.

Cuplikan layar jendela Penyimpanan Musik memperlihatkan detail genre yang ditentukan dari semua data album yang dimasukkan ke dalam database.

Selanjutnya, klik Genre untuk melihat daftar album.

Cuplikan layar jendela Music Store memperlihatkan daftar album yang terkait dengan genre Disco dalam database album.

Mengklik judul Album sekarang menampilkan tampilan Detail Album kami yang diperbarui, termasuk tombol "Tambahkan ke ke cart".

Cuplikan layar jendela Penyimpanan Musik yang memperlihatkan tampilan Detail Album yang diperbarui dan tombol Tambahkan ke keramaian.

Mengeklik tombol "Tambahkan ke ke cart" menunjukkan tampilan Indeks Ke cart Belanja kami dengan daftar ringkasan ke cart belanja.

Cuplikan layar jendela Music Store yang memperlihatkan tampilan Ke cart Belanja dengan daftar ringkasan semua item di kerajang.

Setelah memuat ke cart belanja, Anda dapat mengeklik tautan Hapus dari ke cart untuk melihat pembaruan Ajax ke kedai belanja Anda.

Cuplikan layar jendela Music Store yang menampilkan tampilan Ke cart Belanja dengan album dihapus dari daftar ringkasan.

Kami telah membangun keliling belanja yang berfungsi yang memungkinkan pengguna yang tidak terdaftar untuk menambahkan item ke keliling mereka. Di bagian berikut, kami akan mengizinkan mereka untuk mendaftar dan menyelesaikan proses checkout.