转自 http://firechun.blog.163.com/blog/static/3180452220110314454425/
我们允许用户在没注册的情况下把唱片放进他们的购物车,但是当他们要结算时就必须注册为会员。购物和结算将使用两个不同的控制 器:ShoppingCartController允许匿名用户把商品放进购物车,CheckoutController则处理结算过程。这一章我们从购 物车开始,下一章我们创建结算。
添加Cart、Order和OrderDetails模型类
购物和结算都要使用某些新的类,在Models文件夹上单击右键添加Cart类(Card.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是表的整形主键。习惯上,实体框架“代码优先”会认为Cart表的主键是CartId或ID,而如果需要的话,我们很容易通过注释或代 码忽略这一点。上面就是一个例子,当实体框架“代码优先”的约定适合我们时,我们就使用,反之,我们也不会被这些约定限制。
接下来添中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导航属性依赖的类我们还没创建,让我们添加一个OrderDetails类(OrderDetails.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类中添加这些新模型类的DbSet,更新后的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,全球唯一标识符),使用ASP.NET的Session来保存这个ID。
注: ASP.NET Session 能够方便地保存用户特定信息,并且在用户离开网站后自动过期。虽然滥用Sission会对较大站点产生影响,为了演示目的的轻量级使用没有任何问题。
ShoppingCart类公开以下方法:
AddToCart:以一个Album对象为参数,并把它添加到用户的购物车中。因为Cart表记录每种唱片的数量,方法中包括在需要时创建一个新行或者当用户拿取已有的唱片时增加该唱片的数量的业务逻辑。
RemoveFromCart:以Album ID为参数,并从用户的购物车中把它移除。如果用户的购物车中该唱片只有一份,则删除一行。
EmptyCart:从用户的购物车中移除所有商品
GetCartItems:返回购物车中的商品列表
GetCount:返回用户购物车中的唱片的总数
GetTotal:计算用户购物车中商品的总金额
CreateOrder:在结算阶段从购物车生成一张订单
GetCart:这是一个静态方法,允许控制器获得一个Cart对象。它调用GetCartId方法从用户的Session中读取CartId。GetCartId需要HttpContextBase以从用户的Sission中读取CartId。
下面是完整的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 orderDetails = 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);
}
// 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传递相关信息,就象之前在StoreManager中为下拉框传递数据一样。但是通过 ViewBag传递太多信息会变得难以管理。
另一种方案是使用视图模型(ViewModels),这种方式可以为指定的视图创建最优的强类型类,并且在视图模板中为所需的值/内容公开属性。控制器可以填充和传递这些“视图优化”类给视图模板使用,而且在视图模板中支持类型安全、编译检查和智能感应器。
我们创建两个视图模型供ShoppingCartController使用:ShoppingCartViewModel保存用户购物车的内容,ShoppingCartRemoveViewModel在用户从购物车移除物品时显示确认信息。
在项目中添加新文件夹ViewModels。
接下来在ViewModels文件夹中添加类ShoppingCartViewModel,它有两个属性:购物车物品集合和购物车所有物品的总金额。
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; }
}
}
Shopping Cart控制器
Shopping Cart控制器有三个主要功能:往购物车添加物品,从购物车中移除物品以及显示购物车的物品。它将使用我们刚刚创建的三个 类:ShoppingCartViewModel, ShoppingCartRemoveViewModel和 ShoppingCart。和StoreController发及StoreManagerController一样,我们为它添加一个 MusicStoreEntities类的实例字段。
在项目中添加一个Shopping Cart控制器:
下面是完整的ShoppingCartController的代码。Index和Add控制器操作看起来非常眼熟。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");
}
}
}
使用Ajax.ActionLink实现Ajax更新
为ShoppingCartViewModel类创建一个强类型的Shopping Cart Index页面,并且象以前一样使用List视图模板。
(注:这里实际上不能在“支架模板”中选择List,否则生成视图后直接运行会出错。因为Index方法返回的是单个ShoppingCartViewModel对象而不是集合。当然你可以直接复制网页上的代码来覆盖自动生成的代码,不过你还是要明白这一点)
使用Ajax.ActionLink代替Html.ActionLink来生成从购物车移除物品的链接。
@Ajax.ActionLink("Remove from cart",
"RemoveFromCart",
new { id = item.RecordId }, new AjaxOptions { OnSuccess = "handleUpdate"
})
Ajax.ActionLink 和Html.ActionLink方法非常相似,不同的是表单提交时为RemoveFromCart方法生成一个Ajax回调。 RemoveFromCart方法返回一个序列化的JsonResult对象(关于JsonResult对象,请参见:http://msdn.microsoft.com/zh-cn/library/system.web.mvc.jsonresult.aspx) ,它被自动传递给AjaxOptions的OnSuccess参数指定的JavaScript方法,在本例中,这个方法是handleUpdate。handleUpdate使用JQuery为页面执行四个快速更新:
- 1.从列表中移除删掉的唱片
- 2.更新标题中的数量
- 3.为用户显示更新信息
- 4.更新购物车总金额
由于Index视图使用Ajax回调来处理“移除”情况,我们不需要为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视图,添加一个“Add to cart”按纽,我们还可以为唱片追加一些相关信息: Genre, Artist, Price, t和Album Art。更新后的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”来测试对购物车添加和移除物品,运行应用程序并访问/Store:
点击某个类别查看唱片列表:
点击唱片标题进入更新后的Store Details视图:
点击“Add to cart”按纽,在Shopping Cart的首页中显示购物车清单:
购物车被加载之后,你可以点击“Remove from cart”链接查看Ajax的更新效果。
我们建立了购物车,让用户无需注册就可以往购物车中添加物品,下一章,我们将允许他们注册以完成结算过程。