ASP.NET MVC 音乐商店 - 8. 使用 Ajax 更新的购物车

来源:岁月联盟 编辑:exp 时间:2011-11-22

 

 

在这个项目中,我们将允许用户在没有注册登录的情况下将专辑加入购物车,但是,在完成结账的时候必须完成注册工作。购物和结账将会被分离到两个控制器中:一个ShoppingCart 控制器,允许匿名用户使用购物车,另一个Checkout 控制器处理结账。我们先从购物车的控制器开始,然后在下一部分来处理结帐。

加入购物车,订单和订单明细的模型类

 

在购物车和结账的处理中将会使用到一些新的类,在Models 文件夹上右键,然后使用下面的代码增加一个新的类Cart.

       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 的标识,用来允许匿名用户使用购物车,但是,CartId 并不是表的主键,表的主键是整数类型的名为RecordId的字段,根据约定,EF CodeFirst 将会认为表的主键名为CartId 或者Id,不过,如果需要的话,我们可以很容易地通过标注或者代码来重写这个规则。这里例子演示了在使用EF CodeFirst 的时候。当我们的表不是约定的样子时,我们也不必被约定所局限。

下一步,使用下面的代码增加订单Order 类。

       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 类。

       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 更新一下,以便包含我们新定义的模型类,包括艺术家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,或者被称为全局唯一标识符,对于已经登录的用户,我们直接使用他们的名称,这个表示我们保存在Session 中。

注意:Session 会话可以很方便地存储用户的信息,在用户离开站点之后,这些信息将会过期,滥用Session 信息会对大型站点产生影响,我们这里使用Session 达到演示目的。

ShoppingCart 类提供了如下的方法:

AddToCart, 将专辑作为参数加入到购物车中,在Cart 表中跟踪每个专辑的数量,在这个方法中,我们将会检查是在表中增加一个新行,还是仅仅在用户已经选择的专辑上增加数量。

RemoveFromCart,通过专辑的标识从用户的购物车中将这个专辑的数量减少1,如果用户仅仅剩下一个,那么就删除这一行。

EmptyCart,删除用户购物车中所有的项目。

GetCartItems,获取购物项目的列表用来显示或者处理。

GetCount,获取用户购物车中专辑的数量

GetTotal,获取购物车中商品的总价

CreateOrder,将购物车转换为结账处理过程中的订单。

GetCart ,这是一个静态方法,用来获取当前用户的购物车对象,它使用GetCartId 方法来读取保存当前Session 中的购物车标识,GetCartId 方法需要HttpContextBase 以便获取当前的Session。

实际的代码如下:

 

       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();

        }

    }

}

 

视图模型

 

我们的ShoppingCart 控制器需要向视图传递复杂的信息,这些信息与现有的模型并不完全匹配,我们不希望修改模型来适应视图的需要;模型类应该表示领域信息,而不是用户界面。一个解决方案是使用ViewBag 来向视图传递信息,就像我们在Store Manager 中的下列列表处理中那样,但是通过ViewBag 来传递大量信息就不好管理了。

另外一个解决方案是使用视图模型模式,使用这个模式,我们需要创建强类型的用于视图场景的类来表示信息,这个类拥有视图所需要的值或者内容。我们的控制器填充信息,然后传递这种类的对象供视图使用,这样就可以得到强类型的、编译时检查支持,并且在视图模板中带有智能提示。

我们将会创建两个视图模型用于我们的ShoppingCart 控制器:ShoppingCartViewModel 将会用于用户的购物车,而ShoppingCartRemoveViewModel 会用于在购物车中删除内容时的确认提示信息。首先在项目中创建 ViewModels 文件夹来组织我们的项目文件,在项目上点击鼠标的右键,然后选择添加 –⟩新文件夹。

/

                       

命名为 ViewModels

/

下一步,在ViewModels 文件夹中增加ShoppingCartViewModel 类,它包括两个属性,一个CartItem 的列表,另外一个属性是购物中的总价。

       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 类,它包括五个属性。

       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 控制器有三个主要的目的:增加项目到购物车,从购物车中删除项目,查看购物车中的项目。控制器使用到我们刚刚创建的三个类:ShoppingCartViewModel,ShoppingCartRemoveViewModel 和ShoppingCart,像StoreController 和StoreManagerController 一样,我们在控制器中增加一个MusicStoreEntities 字段来操作数据。

在项目中使用空的控制器模板创建Shopping Cart 控制器

 

/

下面是已经完成的控制器代码,Index 和 Add 方法看起来非常熟悉。Remove 和 CartSummary 这两个 Action 方法处理两种特定的场景,我们将在后面讨论。

 

 

 

       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 更新

 

下面我们将创建Shopping Cart 的Index Action 视图,这个视图使用强类型的ShoppingCartViewModel ,像以前的视图一样,使用List 视图模板。

 

/

在这里,我们不使用 Html.ActionLink 从购物车中删除项目,我们将会使用 JQuery 来包装客户端使用 RemoveLink 的类所有超级链接元素的事件,不是提交表单,而是通过客户端的事件向 RemoveFromCart 控制器方法发出 Ajax 请求,然后 RemoveFromCart 返回 JSON 格式的结果,这个结果被发送到我们在 AjaxOptions 的 OnSucess 参数中创建的 JavaScript 函数,在这里是 handleUpdate,handleUpdate 函数解析 JSON 格式的结果,然后通过 jQuery 执行下面的四个更新。

  1. 从列表中删除专辑
  2. 更新头部的购物车中的数量
  3. 向用户显示更新信息
  4. 更新购物车中的总价

因为在 Index 视图中我们处理了删除的场景,我们就不再需要为 RemoveFromCart 方法增加额外的视图。下面是视图的完整代码。

 

 

       @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 + ')');

});

            }

        });

    });

    function handleUpdate() {

        // Load and deserialize the returned JSON data

        var json = context.get_data();

        var data = Sys.Serialization.JavaScriptSerializer.deserialize(json);

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

 

为了测试一下,我们需要向购物车中增加一些项目,更新Store 的Details 视图包含添加到购物车按钮,在这里,我们还需要包含我们后来增加的专辑的一些额外信息,流派,艺术家,价格等等。更新后的视图如下所示。

 

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

 

摘自 冠军

 

 

 

现在,我们可以在商店中通过购物车来购买和删除一些项目了。运行程序,浏览 Store 控制器的 Index 。 

/

然后,点击某个分类来查看专辑的列表。

/

点击某个专辑来显示专辑的详细内容,现在已经有了加入购物车的按钮。

/

点击加入购物车之后,可以在购物车中看到。

/

在购物车中,可以点击从购物车中删除的链接,将会看到 Ajax 更新购物车的效果。

/

现在的购物车允许没有注册的用户使用购物车添加项目,在下一部分,我们将允许匿名用户注册和完成结账的处理