第 9 部分:注册和结帐

作者 :Jon Galloway

MVC 音乐应用商店是一个教程应用程序,介绍并逐步说明如何使用 ASP.NET MVC 和 Visual Studio 进行 Web 开发。

MVC 音乐商店是一个轻量级的示例商店实现,用于在线销售音乐专辑,并实现基本的网站管理、用户登录和购物车功能。

本系列教程详细介绍了生成 ASP.NET MVC 音乐应用商店示例应用程序所执行的所有步骤。 第 9 部分介绍注册和签出。

在本部分中,我们将创建一个 CheckoutController,用于收集购物者的地址和付款信息。 我们将要求用户在签出之前注册到我们的站点,因此此控制器需要授权。

用户将通过单击“结帐”按钮从购物车导航到结帐过程。

“音乐应用商店”窗口的屏幕截图,其中显示了“结帐”视图,其中红色箭头突出显示了“结帐”按钮。

如果用户未登录,系统会提示他们登录。

“音乐应用商店”窗口的屏幕截图,其中显示了“用户名”和“密码”字段的登录视图。

成功登录后,用户会显示“地址和付款”视图。

“音乐应用商店”窗口的屏幕截图,其中显示了地址和付款视图,其中包含用于收集送货地址和付款信息的字段。

填写表单并提交订单后,将显示订单确认屏幕。

“音乐应用商店”窗口的屏幕截图,其中显示了通知用户订单已完成的结帐完整视图。

尝试查看不存在的订单或不属于的订单时,将显示“错误”视图。

“音乐应用商店”窗口的屏幕截图,其中显示了当用户尝试查看其他人的订单或虚构订单时的错误视图。

迁移购物车

虽然购物过程是匿名的,但当用户单击“结帐”按钮时,需要注册和登录。 用户预期我们会在两次访问之间维护其购物车信息,因此,我们需要在用户完成注册或登录时将购物车信息与用户相关联。

这实际上非常简单,因为我们的 ShoppingCart 类已经有一个方法,该方法会将当前购物车中的所有项目与用户名相关联。 我们只需在用户完成注册或登录时调用此方法。

打开我们在设置成员资格和授权时添加的 AccountController 类。 添加引用 MvcMusicStore.Models 的 using 语句,然后添加以下 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;
}

接下来,修改 LogOn 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);
 }

在成功创建用户帐户后立即对“注册后”操作进行相同的更改:

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

右键单击 Controllers 文件夹,并使用空控制器模板将新的控制器添加到名为 CheckoutController 的项目。

“添加控制器”窗口的屏幕截图,其中填充了文本“签出控制器”的“控制器名称”字段。

首先,在 Controller 类声明上方添加 Authorize 属性,要求用户在签出前注册:

namespace MvcMusicStore.Controllers
{
    [Authorize]
    public class CheckoutController : Controller

注意:这类似于我们之前对 StoreManagerController 所做的更改,但在这种情况下,Authorize 属性要求用户具有管理员角色。 在签出控制器中,我们要求用户登录,但不要求他们是管理员。

为简单起见,我们不会在本教程中处理付款信息。 相反,我们允许用户使用促销代码检查。 我们将使用名为 PromoCode 的常量存储此促销代码。

与在 StoreController 中一样,我们将声明一个字段来保存名为 storeDB 的 MusicStoreEntities 类的实例。 为了使用 MusicStoreEntities 类,我们需要为 MvcMusicStore.Models 命名空间添加 using 语句。 签出控制器的顶部如下所示。

using System;
using System.Linq;
using System.Web.Mvc;
using MvcMusicStore.Models;
 
namespace MvcMusicStore.Controllers
{
    [Authorize]
    public class CheckoutController : Controller
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();
        const string PromoCode = "FREE";

CheckoutController 将具有以下控制器操作:

AddressAndPayment (GET 方法) 将显示一个表单,允许用户输入其信息。

AddressAndPayment (POST 方法) 将验证输入并处理订单。

用户成功完成签出过程后,将显示“完成”。 此视图将包含用户的订单号,作为确认。

首先,让我们将索引控制器操作重命名 (该操作是在将控制器) 创建到 AddressAndPayment 时生成的。 此控制器操作仅显示签出窗体,因此不需要任何模型信息。

//
// GET: /Checkout/AddressAndPayment
public ActionResult AddressAndPayment()
{
    return View();
}

AddressAndPayment POST 方法将遵循我们在 StoreManagerController 中使用的相同模式:它将尝试接受表单提交并完成订单,如果失败,将重新显示表单。

验证表单输入是否符合订单的验证要求后,我们将直接检查 PromoCode 表单值。 假设一切正确,我们将保存订单中的更新信息,告知 ShoppingCart 对象完成订单过程,并重定向到“完成”操作。

//
// POST: /Checkout/AddressAndPayment
[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");
    }
}

注意:启动项目时,“错误”视图是在 /Views/Shared 文件夹中自动创建的。

完整的 CheckoutController 代码如下所示:

using System;
using System.Linq;
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();
        }
        //
        // POST: /Checkout/AddressAndPayment
        [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 控制器操作,添加名为 AddressAndPayment 的视图,该视图强类型化为“订单”并使用“编辑”模板,如下所示。

“添加视图”窗口的屏幕截图,其中“视图名称”字段、“创建视图”复选框以及“模型类”和“基架”下拉列表以红色突出显示。

此视图将使用我们在生成 StoreManagerEdit 视图时查看的两种技术:

  • 我们将使用 Html.EditorForModel () 显示订单模型的表单字段
  • 我们将使用具有验证属性的 Order 类来利用验证规则

首先,更新表单代码以使用 Html.EditorForModel () ,然后是促销代码的附加文本框。 AddressAndPayment 视图的完整代码如下所示。

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

尝试提交包含缺失或无效信息的表单现在将显示使用客户端验证的错误消息。

“音乐应用商店”窗口的屏幕截图,其中显示了地址和付款视图,电话和电子邮件字段中有一串无效信息。

好的,我们已经为结帐过程做了大部分的艰苦工作:我们只是有几个赔率和结束结束。 我们需要添加两个简单的视图,并且我们需要在登录过程中处理购物车信息的移交。

添加“签出完成”视图

“结帐”视图非常简单,因为它只需要显示订单 ID。 右键单击“完成”控制器操作,并添加名为“完成”的视图,该视图强类型化为 int。

“添加视图”窗口的屏幕截图,其中以红色矩形突出显示了“视图名称”字段和“模型类”下拉列表。

现在,我们将更新视图代码以显示订单 ID,如下所示。

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

更新错误视图

默认模板在“共享视图”文件夹中包含“错误”视图,以便可以在网站中的其他位置重用该视图。 此错误视图包含一个非常简单的错误,并且不使用我们的网站布局,因此我们将对其进行更新。

由于这是一个通用错误页面,因此内容非常简单。 如果用户想要重新尝试其操作,我们将包含一条消息和一个指向历史记录中上一页的链接。

@{
    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.</p>