在这一节,我们将创建结账的控制器 CheckoutController 来收集用户的地址和付款信息,我们需要用户在结账前注册账户,因为这个控制器需要授权。
当用户点击结账 Checkout 按钮的时候,用户将会被导航到结账的处理流程中。

如果用户没有登录,将会被提示需要登录。

一旦用户成功登陆,用户就可以看到地址和付款的视图。

一旦用户填写了这个表单并提交,他们将会看到订单的确认页面。

视图访问不存在的订单,或者不属于你的订单,将会看到错误页面。

合并购物车
在匿名购物的时候,当用户点击结账 Checkout 按钮,用户会被要求注册和登陆,用户会希望继续使用原来的购物车,所以,在匿名用户登录之后,我们需要维护购物车。
实际上非常简单,因为 ShoppingCart 类已经提供了一个方法,通过当前的用户名来获取购物车中所有的项目,在用户注册登录以后,我们只需要调用这个方法。
打开在成员管理和授权中添加的 AccountController 类,增加一个 using 来引用 MvcMusicStore.Models,然后,增加 MigrateShoppingCart 方法。
private void MigrateShoppingCart(string UserName)
{
// Associate shopping cart items with logged-in user
var cart = ShoppingCart.GetCart(this.HttpContext);
cart.MigrateCart(UserName);
Session[ShoppingCart.CartSessionKey] = UserName;
}
然后,修改 LonOn 的 Post 处理方法,在用户通过验证之后,调用 MigrateShoppingCart 方法。
//
// POST: /Account/LogOn
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
MigrateShoppingCart(model.UserName);
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
&& !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
在 Register 的 Post 处理方法中,一旦用户成功创建帐户,也进行类似的修改,
//
// POST: /Account/Register
[HttpPost]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Attempt to register the user
MembershipCreateStatus createStatus;
Membership.CreateUser(model.UserName, model.Password, model.Email, "question", "answer", true, null, out createStatus);
if (createStatus == MembershipCreateStatus.Success)
{
MigrateShoppingCart(model.UserName);
FormsAuthentication.SetAuthCookie(model.UserName, false /* createPersistentCookie */);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", ErrorCodeToString(createStatus));
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
就这样,现在匿名用户登录之后,购物车将会被自动传送过来。
创建结账 CheckoutController 控制器
在 Controller 文件夹上右键,添加一个新的控制器,命名为 CheckoutController ,使用空的控制器模板。

首先,在控制器上增加授权的标注 [Authorize],来确定用户必须在登录之后才能访问。
namespace MvcMusicStore.Controllers
{
[Authorize]
public class CheckoutController : Controller
注意:这一步很像我们前面在 StoreManager 控制器上的工作,但是,在那个时候,我们要求用户必须拥有 Administrator 的角色。在结账控制器中,我们不需要用户必须是 Administrator ,而是必须登录。
出于简化的考虑,在这个教程中没有处理付款的信息,作为替代,我们允许用户输入一个促销代码,这里促销代码定义在常量 PromoCode。
像在 StoreController 中一样,在控制器中,我们也需要定义 MusicStoreEntities 的字段,将它命名为 storeDB,结账的开始部分如下。
using MvcMusicStore.Models;
namespace MvcMusicStore.Controllers
{
[Authorize]
public class CheckoutController : Controller
{
MusicStoreEntities storeDB = new MusicStoreEntities();
const string PromoCode = "FREE";
结账的控制器将包含下面的控制器方法:
AddressAndPayment ( Get ) 用来显示一个用户输入信息的表单
AddressAndPayment ( Post ) 验证用户的输入,处理订单。
Complete 用来在在用户完成订单之后显示,这个视图包含用户的订单账号和确认信息。
首先,将 Index 方法改名为 AddressAndPayment, 这个 Action 方法用来显示结账表单,所以,不需要任何的模型信息。
//
// GET: /Checkout/AddressAndPayment
public ActionResult AddressAndPayment()
{
return View();
}
AddressAndPayment 的 Post 处理方法使用我们在 StoreManagerController 中类似的模式:如果成功了就完成订单,如果失败了就重新显示表单。
在验证了表单之后,我们将会直接检查促销代码,假设所有的信息都是正确的,我们将会在订单中保存信息,告诉购物车对象完成订单处理,最后,重定向到完成的 Complete Action 方法。
[HttpPost]
public ActionResult AddressAndPayment(FormCollection values)
{
var order = new Order();
TryUpdateModel(order);
try
{
if (string.Equals(values["PromoCode"], PromoCode,
StringComparison.OrdinalIgnoreCase) == false)
{
return View(order);
}
else
{
order.Username = User.Identity.Name;
order.OrderDate = DateTime.Now;
//Save Order
storeDB.Orders.Add(order);
storeDB.SaveChanges();
//Process the order
var cart = ShoppingCart.GetCart(this.HttpContext);
cart.CreateOrder(order);
return RedirectToAction("Complete",
new { id = order.OrderId });
}
}
catch
{
//Invalid - redisplay with errors
return View(order);
}
}
一旦完成了结账处理,用户将被重定向到 Complete 方法, 这个 Action 方法将会进行简单的检查,在显示订单号之前,检查订单是否属于当前登录的用户。
//
// GET: /Checkout/Complete
public ActionResult Complete(int id)
{
// Validate customer owns this order
bool isValid = storeDB.Orders.Any(
o => o.OrderId == id &&
o.Username == User.Identity.Name);
if (isValid)
{
return View(id);
}
else
{
return View("Error");
}
}
注意,错误视图创建项目的时候,保存在 /Views/Shared 文件夹中的 error.cshtml 生成。
完整的 CheckoutController 如下所示.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcMusicStore.Models;
namespace MvcMusicStore.Controllers
{
[Authorize]
public class CheckoutController : Controller
{
MusicStoreEntities storeDB = new MusicStoreEntities();
const string PromoCode = "FREE";
//
// GET: /Checkout/AddressAndPayment
public ActionResult AddressAndPayment()
{
return View();
}
[HttpPost]
public ActionResult AddressAndPayment(FormCollection values)
{
var order = new Order();
TryUpdateModel(order);
try
{
if (string.Equals(values["PromoCode"], PromoCode,
StringComparison.OrdinalIgnoreCase) == false)
{
return View(order);
}
else
{
order.Username = User.Identity.Name;
order.OrderDate = DateTime.Now;
//Save Order
storeDB.Orders.Add(order);
storeDB.SaveChanges();
//Process the order
var cart = ShoppingCart.GetCart(this.HttpContext);
cart.CreateOrder(order);
return RedirectToAction("Complete",
new { id = order.OrderId });
}
}
catch
{
//Invalid - redisplay with errors
return View(order);
}
}
//
// GET: /Checkout/Complete
public ActionResult Complete(int id)
{
// Validate customer owns this order
bool isValid = storeDB.Orders.Any(
o => o.OrderId == id &&
o.Username == User.Identity.Name);
if (isValid)
{
return View(id);
}
else
{
return View("Error");
}
}
}
}
增加 AddressAndPayment 视图
现在,我们创建 AddressAndPayment 视图,在 AddressAndPayment 控制器的某个 Action 中点击鼠标的右键,增加名为 AddressAndPayment 的强类型 Order 视图,使用编辑模板。

这个视图使用我们在 StoreManager 的 Edit 视图中使用的两个技术:
1.使用 Html.EditorForModel() 来显示订单模型的字段
2.使用 Order 模型的验证标签定义验证规则
我们先使用 Html.EditorForModel() 方法,然后,增加额外的输入框用来输入促销码,完成的视图如下。
@model MvcMusicStore.Models.Order
@{
ViewBag.Title = "Address And Payment";
}
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm())
{
<h2>
Address And Payment</h2>
<fieldset>
<legend>Shipping Information</legend>
@Html.EditorForModel()
</fieldset>
<fieldset>
<legend>Payment</legend>
<p>
We're running a promotion: all music is free with the promo code: "FREE"</p>
<div class="editor-label">
@Html.Label("Promo Code")
</div>
<div class="editor-field">
@Html.TextBox("PromoCode")
</div>
</fieldset>
<input type="submit" value="Submit Order" />
}
定义订单的验证规则
现在,我们的视图已经创建了,类似于专辑,我们定义订单的验证规则。在 Models 文件夹上点击鼠标的右键,增加名为 Order 的模型类,我们使用专辑中使用过的验证标注,我们还将使用正则表达式来验证用户的电子邮件地址。
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace MvcMusicStore.Models
{
[Bind(Exclude = "OrderId")]
public partial class Order
{
[ScaffoldColumn(false)]
public int OrderId { get; set; }
[ScaffoldColumn(false)]
public System.DateTime OrderDate { get; set; }
[ScaffoldColumn(false)]
public string Username { get; set; }
[Required(ErrorMessage = "First Name is required")]
[DisplayName("First Name")]
[StringLength(160)]
public string FirstName { get; set; }
[Required(ErrorMessage = "Last Name is required")]
[DisplayName("Last Name")]
[StringLength(160)]
public string LastName { get; set; }
[Required(ErrorMessage = "Address is required")]
[StringLength(70)]
public string Address { get; set; }
[Required(ErrorMessage = "City is required")]
[StringLength(40)]
public string City { get; set; }
[Required(ErrorMessage = "State is required")]
[StringLength(40)]
public string State { get; set; }
[Required(ErrorMessage = "Postal Code is required")]
[DisplayName("Postal Code")]
[StringLength(10)]
public string PostalCode { get; set; }
[Required(ErrorMessage = "Country is required")]
[StringLength(40)]
public string Country { get; set; }
[Required(ErrorMessage = "Phone is required")]
[StringLength(24)]
public string Phone { get; set; }
[Required(ErrorMessage = "Email Address is required")]
[DisplayName("Email Address")]
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}",
ErrorMessage = "Email is is not valid.")]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
[ScaffoldColumn(false)]
public decimal Total { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
}
}
在没有提交必要的信息或者提交错误信息的时候,我们将会看到客户端的验证信息。

现在,我们已经完成了结账中最困难的部分,还有一些小的工作要完成。我们增加两个简单的视图,还需要注意在登录过程中购物车信息。
增加完成结账视图
完成结账的视图非常简单,仅仅需要显示订单的编号,在控制器中的 Complete 方法上点击右键,增加名为 Complete 的强类型 int 视图。

现在,修改视图显示订单的编号。
@model int
@{
ViewBag.Title = "Checkout Complete";
}
<h2>
Checkout Complete</h2>
<p>
Thanks for your order! Your order number is: @Model</p>
<p>
How about shopping for some more music in our @Html.ActionLink("store", "Index", "Home")
</p>
更新错误视图
项目的默认模板中,包含了定义在 /Shared Views 文件夹中的错误页面,可以在整个站点中使用。这个页面仅仅包含简单的信息,也没有使用我们的布局,我们更新一下。
由于这是通用的错误页面,内容非常简单,我们仅仅包含一个提示信息,和用来重做工作的导航到上一个页面的链接。
@{ ViewBag.Title = "Error"; }
<h2>
Error</h2>
<p>
We're sorry, we've hit an unexpected error. <a href="javascript:history.go(-1)">Click
here</a> if you'd like to go back and try that again.
示例查询最多的应用场合是组合查询,我们常常需要在界面上提供若干的查询选项,然后根据用户的输入返回符合条件的结果。
使用代码直接进行处理往往需要涉及到复杂的条件,由于组合条件并不确定,导致逻辑判断语句结构复杂。对于多个可选的参数,情况会变得更加严重。
使用示例查询可以很方便地处理这种问题。
在查询的时候,将收集到的查询条件赋予一个对象的属性,当然,这个对象的类型就是需要查询的实体对象。
例如,在 NHibernate 中存在一个 User 的类型,我们需要对它的姓名和口令进行组合查询,User 的定义如下:
namespace Demo.Dao.Domain
{
// 用户对象
public class User
{
public virtual int UserId { set; get; }
public virtual string Username { set; get; }
public virtual string Password { set; get; }
public virtual Address Address { set; get; }
}
}
在界面中,我们提供用户输入的输入框。
<p>
<label for="<%= this.tbxName %>">
用户名:</label>
<asp:TextBox ID="tbxName" runat="server"></asp:TextBox>
</p>
<p>
<label for="<%= this.tbxPassword %>">
口令:</label>
<asp:TextBox ID="tbxPassword" runat="server"></asp:TextBox>
</p>
<p>
<asp:Button ID="btnSearch" runat="server" Text="查询" OnClick="btnSearch_Click"/>
</p>
在按钮的点击事件中,我们将用户的输入组合为一个示例对象。然后提交给 NHibernate 进行查询。
protected void btnSearch_Click(object sender, EventArgs e)
{
if (this.IsValid)
{
Demo.Dao.Domain.User example
= new Demo.Dao.Domain.User()
{
Username = this.tbxName.Text,
Password = this.tbxPassword.Text
};
IList<Demo.Dao.Domain.User> list = this.UserService.GetUsers(example);
this.Repeater1.DataSource = list;
this.Repeater1.DataBind();
}
}
在后台的服务中,我们使用示例查询进行处理。
示例查询类型定义在命名空间 NHibernate.Criterion 中,使用静态方法 Create 可以创建示例对象。
首先创建一个查询条件对象,然后使用 Example 的 Create 方法创建出查询示例对象。最后就是实际的查询。
public IList<Demo.Dao.Domain.User> GetUsers( Demo.Dao.Domain.User exampleUser)
{
// 创建查询标准
global::NHibernate.ICriteria criteria
= this.Session.CreateCriteria<Demo.Dao.Domain.User>();
// 创建查询示例
global::NHibernate.Criterion.Example example
= global::NHibernate.Criterion.Example.Create(exampleUser);
// 设置使用示例对象的查询条件
criteria.Add(example);
example.EnableLike(global::NHibernate.Criterion.MatchMode.Anywhere);
IList< Demo.Dao.Domain.User> list = criteria.List<Demo.Dao.Domain.User>();
return list;
}
对于字符串类型的条件来说,默认使用精确匹配,但是,可以通过示例的 EnableLike 进行设置。NHibernate.Criterion.MatchMode 是一个枚举,支持下面的匹配模式。
// 默认字符串是完全匹配,可以通过 EnableLike 进行设置
// Anywhere 表示任何位置匹配
// End 出现在最后位置
// Start 出现在起始位置
// Exact 完全匹配,默认设置
example.EnableLike(global::NHibernate.Criterion.MatchMode.Anywhere);
当在姓名的输入框中输入 a 的时候,NHibernate 生成的 SQL 语句如下:
SELECT this_.UserId as UserId0_0_, this_.Username as Username0_0_, this_.Password as Password0_0_, this_.AddressId as AddressId0_0_
FROM tbl_Users this_
WHERE (this_.Username like @p0)
除了强大的 EnableLike 之外,还提供了几个设置参数,比较常用的有 ExcludeZeroes() 方法,它可以排除属性中值为 0 的属性。
比如,在 User 中增加一个 Age 的整数类型属性,还导致查询语句变成下面的 SQL
SELECT this_.UserId as UserId0_0_, this_.Username as Username0_0_, this_.Password as Password0_0_,
this_.Age as Age0_0_, this_.AddressId as AddressId0_0_
FROM tbl_Users this_
WHERE (this_.Username like @p0 and this_.Age = @p1)
使用排除 0 的设置
// 排除值为 0 的属性
example.ExcludeZeroes();
现在的 SQL 又成为
SELECT this_.UserId as UserId0_0_, this_.Username as Username0_0_, this_.Password as Password0_0_,
this_.Age as Age0_0_, this_.AddressId as AddressId0_0_
FROM tbl_Users this_
WHERE (this_.Username like @p0)
不过,还有两个设置就要小心了。
ExcludeNulls() 方法,看起来是排除值为 null 的属性,不过,代码中是先调用了 ToString() 方法,然后再看字符串的长度,所以会使得前面的 ExcludeZeroes() 失效。
SELECT this_.UserId as UserId0_0_, this_.Username as Username0_0_, this_.Password as Password0_0_,
this_.Age as Age0_0_, this_.AddressId as AddressId0_0_
FROM tbl_Users this_
WHERE (this_.Username like @p0 and this_.Age = @p1)
ExcludeNone() 方法,表示需要使用示例对象的所有属性,也会使得 ExcludeZeroes() 失效。
SELECT this_.UserId as UserId0_0_, this_.Username as Username0_0_, this_.Password as Password0_0_,
this_.Age as Age0_0_, this_.AddressId as AddressId0_0_
FROM tbl_Users this_
WHERE (this_.Username like @p0 and this_.Password is null and this_.Age = @p1)
最后,还有一个 IgnoreCase() 方法, 使用不区分大小写的方法比较字符串,对于 Oracle 是有效的,对于 SQL Server 就用不到了。
SELECT this_.UserId as UserId0_0_, this_.Username as Username0_0_, this_.Password as Password0_0_,
this_.Age as Age0_0_, this_.AddressId as AddressId0_0_
FROM tbl_Users this_
WHERE (lower(this_.Username) like lower(@p0) and this_.Age = @p1)
在这个项目中,我们将允许用户在没有注册登录的情况下将专辑加入购物车,但是,在完成结账的时候必须完成注册工作。购物和结账将会被分离到两个控制器中:一个 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 执行下面的四个更新。
- 从列表中删除专辑
- 更新头部的购物车中的数量
- 向用户显示更新信息
- 更新购物车中的总价
因为在 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 更新购物车的效果。

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