MVC 47.SportsSore:完成购物车

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MVC 47.SportsSore:完成购物车相关的知识,希望对你有一定的参考价值。

作者:[美]Adam Freeman      来源:精通ASP.NET MVC 4》

本文将继续构建 SportsStore 示例应用程序。在上一章中,添加了对购物车的基本支持,现在打算改善并完成其功能。

1.使用模型绑定

 MVC 框架使用了一个叫作“模型绑定”的系统,以便通过 HTTP 请求来创建一些 C# 对象,目的是把它们作为参数值传递给动作方法。例如,MVC 处理表单的方式就是这样。框架会考察目标动作方法的参数,并用一个模型绑定器来获取表单中 input 元素的值,并把它们转换成同名的参数类型。

模型绑定器能够通过请求中可用的信息来创建 C# 类型,这是 MVC 框架的核心特性之一。本节将创建一个自定义模型绑定器来盖上 CartController 类。

人们喜欢使用 Cart 控制器中的会话状态特性来存储和管理 Cart 对象,但却不喜欢它要采取的工作方式。它不符合本应用程序模型的其余部分,而那是基于动作方法参数的(因为动作方法参数的操作以模型为基础,而会话状态的操作需要设置键值对,两者的工作方式不一致)。另外。除非模仿基类的 Session 参数,否则不能适当的对 CartController 类进行单元测试,而这意味着需要模仿 Controller 类(控制器的基类),以及其他一些不希望处理的东西。

为了解决这一问题,我们打算创建一个自定义模型绑定器,以获得包含在会话数据中的 Cart 对象(注意,常规的模型绑定器能够直接处理请求中的数据来创建模型对象,这里创建自定义绑定器的目的是为了处理会话中的数据,手工用会话数据创建 Cart 对象)。然后,MVC 框架能够创建 Cart 对象,并把它们作为参数传递给 Controller 类的动作方法。

创建自定义模型绑定器

通过实现 IModelBinder 接口,可以创建一个自定义模型绑定器。在 SportsStore.WebUI 项目中新建文件夹“Binders”,并新建类文件 CartModelBinder.cs ,代码如下:

 

using SportsStore.Domain.Entities;
using System.Web.Mvc;

namespace SportsStore.WebUI.Binders
{
    public class CartModelBinder : IModelBinder
    {
        private const string sessionKey = "Cart";
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            //通过会话获取 Cart
            Cart cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
            //若会话中没有 Cart ,则创建一个
            if (cart == null)
            {
                cart = new Cart();
                controllerContext.HttpContext.Session[sessionKey] = cart;
            }
            //返回 cart
            return cart;
        }
    }
}

这些 IModelBinder 接口定义了一个方法: BindModel 。所提供的两个参数使得创建域模型对象成为可能。 ControllerContext 对控制器类所具有的全部信息提供了访问,这些信息包含了客户端请求的细节。 ModelBindingContext 提供了要求建立的模型对象的信息,以及使绑定更易于处理的工具。

对于本例的目的而言,所关心的是 ControllerContext 类,它具有 HttpContext 属性,它又相应地有一个 Session 属性,该属性能够获取和设置会话数据。通过读取会话数据的键值可以获取 Cart,而在会话中还没有 Cart 时,又可以创建一个 Cart 。

需要告诉 MVC 框架,它可以使用 CartModelBinder 类来创建 Cart 的实例。这需要在 Global.asax 的 Application_Start 方法中进行注册:

using SportsStore.Domain.Entities;
using SportsStore.WebUI.Binders;
using SportsStore.WebUI.Infrastructure;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace SportsStore.WebUI
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());

            ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
        }
    }
}

现在可以更新 CartController 类,删除 GetCart 方法而依靠现在的模型绑定器,MVC 框架会自动地应用它,代码如下:

using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace SportsStore.WebUI.Controllers
{
    public class CartController : Controller
    {
        private IProductRepository repository;

        public CartController(IProductRepository repo)
        {
            repository = repo;
        }

        public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)
        {
            Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
            if (product != null)
            {
                cart.AddItem(product, 1);
            }
            return RedirectToAction("Index", new { returnUrl });
        }

        public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl)
        {
            Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
            if (product != null)
            {
                cart.RemoveLine(product);
            }
            return RedirectToAction("Index", new { returnUrl });
        }

        public ViewResult Index(Cart cart, string returnUrl)
        {
            return View(new CartIndexViewModel
            {
                Cart = cart,
                ReturnUrl = returnUrl
            });
        }
    }
}

上面代码删除了 GetCart 方法,并对每个动作方法添加了 Cart 参数。当 MVC 框架接收到一个请求,比如,要求调用 AddToCart 方法时,会首先考察动作方法的参数,然后考察可用的绑定器列表,并试图找到一个能够创建每个参数类型实例的绑定器。这会要求自定义绑定器创建一个 Cart 对象,而这时通过利用会话状态特性来完成的。通过自定义绑定器的默认绑定器,MVC 框架能够创建一组调用动作方法所需要的参数,这让开发者能够重构控制器,以便在接收到请求时知道如何创建 Cart 对象。

像这样使用自定义模型绑定器有几个好处。第一个好处是把用来创建 Cart 与创建控制器的逻辑分离开来了,这让开发者能够修改存储 Cart 对象,而不需要修改控制器。第二个好处是任何使用 Cart 对象的控制器类,都能够简单地把这些对象声明为动作方法参数,并能够利用自定义模型绑定器。第三个好处是它能够对 Cart 控制器进行单元测试,而不需要模仿大量的ASP.NET 通道。

 

2.完成购物车

前面已经介绍了自定义模型绑定器,现在到了添加两个新特性来完成购物车功能的时候了。第一个特性将允许客户删除购物车物品,第二个特性将在页面的顶部显示购物车的摘要。

 

2.1 删除购物车物品

前面已经定义了控制器中的 RemoveFromCart 动作方法,因此,让客户删除物品只不过是在视图中将这个方法暴露出来的事情。本文打算在购物车摘要的每一行中添加一个“Remove”按钮来做这件事。对 Views/Cart/Index.cshtml 所做的修改如下:

    <tbody>
        @foreach (var line in Model.Cart.Lines)
        {
            <tr>
                <td class="aling_center">@line.Quantity</td>
                <td class="aling_left">@line.Product.Name</td>
                <td class="aling_right">@line.Product.Price.ToString("c")</td>
                <td class="aling_right">@((line.Quantity * line.Product.Price).ToString("c"))</td>
                <td>
                    @using (Html.BeginForm("RemoveFromCart", "Cart"))
                    {
                        @Html.Hidden("ProductId", line.Product.ProductID)
                        @Html.HiddenFor(x => x.ReturnUrl)
                        <input class="actionButtons" type="submit" value="Remove" />
                    }
                </td>
            </tr>
        }
    </tbody>

注:可以使用强类型的 Html.HiddenFor 辅助器方法,为 ReturnUrl 模型属性创建一个隐藏字段,但这需要使用基于字符串的 Html.Hidden 辅助器方法,对 ProductID 字段做同样的事情。 如果写成“Html.HiddenFor(x=>line.Product.ProductID)”,该辅助器方法便会渲染一个以“line.Product.ProductID”为名称的隐藏字段。该字段名与 CartController.RemoveFromCart 动作方法的参数名不匹配,这会使默认的模型绑定器无法工作,因此 MVC 框架便不能调用此方法了。

运行应用程序,可以看到效果:

 

2.2 添加购物车摘要

现在已经实现了一个功能化的购物车,但将该购物车集成到解密的方式还存在一个问题:只有通过查看购物车摘要屏幕,客户才能知道他们的购物车里有些什么。而且,他们只能通过把一个新的物品加入购物车,才能看到购物车的摘要屏幕。

为了解决这一问题,本节打算添加一个小部件,它汇总购物车的内容,并能够通过点击来显示购物车内容。下面将采用与添加导航不见十分相似的方式来完成这一工作——作为一个动作,把它的输出注入到 Razor 布局。

首先,需要对 CartController 类添加一个简单的方法,代码如下:

        public PartialViewResult Summary(Cart cart)
        {
            return PartialView(cart);
        }

可以看出,这是一个简单的方法。它只需要渲染一个视图,以当前 Cart (它是自定义模型绑定器获得的)作为视图数据。还需要一个分部视图,它在对这个 Summary 方法调用做出响应时被渲染。添加对应的 Summary 视图文件,代码如下:

@model SportsStore.Domain.Entities.Cart

<div id="cart">
    <span class="caption">
        <b>Your cart:</b>
        @Model.Lines.Sum(x => x.Quantity) item(s),
        @Model.ComputeTotalValue().ToString("c")
    </span>
    @Html.ActionLink("Checkout", "Index", "Cart",
    new { returnUrl = Request.Url.PathAndQuery }, null)
</div>

这是一个简单的视图,它显示了购物车的物品数、这些物品的总费用,以及把购物车内容显示给用户的一个链接。现在,已经定义了有 Summary 动作方法所返回的这个视图,可以在 _Layout.cshtml 文件中包含它的渲染结果,代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="~/Content/Site.css" type="text/css" rel="stylesheet" />
</head>
<body>
    <div id="header">
        @{Html.RenderAction("Summary", "Cart");}
        <div class="title">SPORTS STORE</div>
    </div>
    <div id="categories">
        @{Html.RenderAction("Menu", "Nav");}
    </div>
    <div id="content">
        @RenderBody()
    </div>
</body>
</html>

最后一步是添加一些 CSS规则,对该分部视图中的元素进行格式化。对 SportsStore.WebUI 项目中的 Site.css 文件添加样式文件如下:

div#cart {float:right;margin:.8em;color:silver;background-color:#555;padding:.5em .5em .5em 1em;}
div#cart a {text-decoration:none;padding:.4em 1em .4em 1em;line-height:2.1em;margin-left:.5em;background-color:#333;color:white;border:1px solid black;}

运行程序即可以看到效果,对购物车添加物品时,物品数以及总费用都会增加,如下图所示:

利用这个附件,现在可以让客户知道自己的购物车中有什么。这个附件也显示的提供了一个结算离店的办法。从中再一次看到用 RenderAction 把一个动作方法所渲染的输出组合到一个 Web 页面是多么容易。这是将应用程序功能分解成清晰可重用模块的一种很好的技术。

 

3.递交订单

现在到了显示 SportsStore 最后一个客户特性的时候了:结算并完成订单的能力。下面将扩充域模型,以提供收集用户送货细节的支持,并添加一个处理这些细节的特性。

 

3.1 扩充域模型

在 SportsStore.Domain 项目的 Entities 文件夹中新建类 ShippingDetails 。这是用来表示客户送货细节的类,具体代码如下:

 

using System.ComponentModel.DataAnnotations;

namespace SportsStore.Domain.Entities
{
    public class ShippingDetails
    {
        [Required(ErrorMessage = "Please enter a name")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Please enter the first address line")]
        public string Line1 { get; set; }
        public string Line2 { get; set; }
        public string Line3 { get; set; }

        [Required(ErrorMessage = "Please enter a city name")]
        public string City { get; set; }

        [Required(ErrorMessage = "Please enter a state name")]
        public string State { get; set; }

        public string Zip { get; set; }

        [Required(ErrorMessage = "Please enter a country name")]
        public string Country { get; set; }

        public bool GiftWrap { get; set; }
    }
}

上述代码利用了 System.ComponentModel.DataAnnotations 命名空间的验证注解属性,正如之前文章 【MVC 4】1.第一个 MVC 应用程序 所做的那样。

 

3.2 添加结算过程

本例的目的是到达一个程序节点,以此作为用户输入其送货细节并递交订单的入口。为此,需要在购物车摘要视图上添加一个“Checkout now”按钮。修改 Views/Cart/Index.cshtml 文件,修改代码如下:

...
<p class="aling_center actionButtons">
    <a href="@Model.ReturnUrl">Continue shopping</a>
    @Html.ActionLink("Checkout now", "Checkout")
</p>
...

黄色为修改部分,这一修改生成了一个链接,点击这个链接时,调用 Cart 控制器的 Checkout 动作方法。显示效果如下:

 

现在需要在 CartController 类中定义这个 Checkout 方法:

public ViewResult Checkout()
{
   return View(new ShippingDetails());
}

此 Checkout 方法返回默认视图,并传递一个新的 ShippingDetails 对象作为视图模型。为了创建相应的视图文件 Views/Cart/Checkout.cshtml ,视图内容如下:

@model SportsStore.Domain.Entities.ShippingDetails

@{
    ViewBag.Title = "SportsStore: Checkout";
}

<h2>Check out now</h2>
Please enter your details, and we\'ll ship your goods right away!
@using (Html.BeginForm())
{ 
    <h3>Ship to</h3>
    <div>Name:@Html.EditorFor(x => x.Name)</div>
    
    <h3>Address</h3>
    <div>Line 1: @Html.EditorFor(x => x.Line1)</div>
    <div>Line 2: @Html.EditorFor(x => x.Line2)</div>
    <div>Line 3: @Html.EditorFor(x => x.Line3)</div>
    <div>City: @Html.EditorFor(x => x.City)</div>
    <div>State: @Html.EditorFor(x => x.State)</div>
    <div>Zip: @Html.EditorFor(x => x.Zip)</div>
    <div>Country: @Html.EditorFor(x => x.Country)</div>
    
    <h3>Options</h3>
    <label>
        @Html.EditorFor(x => x.GiftWrap)
        Gift wrap these items
    </label>
    
    <p class="align_center">
        <input class="actionButtons" type="submit" value="Complete order" />
    </p>
}

运行程序,可以看到该视图是如何渲染的,效果如下图所示,该视图为收集客户的送货细节渲染了一个表单。

本例用 Html.EditorFor 辅助器方法为每个表单字段渲染了一个 input 元素。该方法是模板化辅助器方法的一个例子(注意,对模型对象的每个属性使用的都是 Html.EditorFor 辅助器方法,并未针对各个属性的类型,去选定特定的辅助器方法)。它让 MVC 框架去决定一个视图模型属性需要采用哪种 input 元素,而不是进行明确的指定(例如,使用 Html.TextBoxFor)。

从显示效果可以看出, MVC 框架为布尔属性渲染了一个复选框,如“Gift wrap these items”选择,而对那些字符串属性渲染了文本框。

 

3.3 实现订单处理器

 在这个应用程序中还需要一个组件,以便能够对订单的细节进行处理。为了与 MVC 模型原理保持一致,本例打算为此功能定义一个借口、编写该接口的一个实现,然后用 DI 容器 Ninject 把两者关联起来。

 

定义接口

在 SportsStore.Domain 项目的 Abstract 文件夹中新建接口 IOrderProcessor ,内容如下:

using SportsStore.Domain.Entities;

namespace SportsStore.Domain.Abstract
{
    public interface IOrderProcessor
    {
        void ProcessOrder(Cart cart, ShippingDetails shippingDetails);
    }
}

 

实现接口

IOrderProcessor 的实现打算采用的订单处理方式是向网站管理员发送订单右击。当然,这简化了销售过程。大多数电子商务网站不会简单的发送订单邮件,而且也没有提供信用卡处理或其他支付形式的支持,只是希望把事情维持在关注 MVC 方面,因此采用了这种发送邮件作为订单处理的方式。

在 SportsStore.Domain 项目的 Concrete 文件夹中新建类 EmailOrderProcessor,这个类使用了包含在 .NET 框架中内建的 SMTP(简单邮件传输协议)支持,以发送一份电子邮件。具体代码如下:

 

using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System.Net;
using System.Net.Mail;
using System.Text;
namespace SportsStore.Domain.Concrete
{
    public class EmailSettings
    {
        public string MailToAddress = "orders@example.com";
        public string MailFromAddress = "sportsstore@example.com";
        public bool UserSsl = true;
        public string Username = "MySmtpUsername";
        public string Password = "MySmtpPassword";
        public string ServerName = "smtp.example.com";
        public int ServerPort = 587;
        public bool WriteAsFile = false;
        public string FileLocation = @"c:\\sports_store_emails";
    }

    public class EmailOrderProcessor : IOrderProcessor
    {
        private EmailSettings emailSettings;

        public EmailOrderProcessor(EmailSettings settings)
        {
            emailSettings = settings;
        }

        public void ProcessOrder(Cart cart, ShippingDetails shippingInfo)
        {
            using (var smtpClient = new SmtpClient())
            {
                smtpClient.EnableSsl = emailSettings.UserSsl;
                smtpClient.Host = emailSettings.ServerName;
                smtpClient.Port = emailSettings.ServerPort;
                smtpClient.UseDefaultCredentials = false;
                smtpClient.Credentials = new NetworkCredential(emailSettings.Username, emailSettings.Password);

                if (emailSettings.WriteAsFile)
                {
                    smtpClient.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
                    smtpClient.PickupDirectoryLocation = emailSettings.FileLocation;
                    smtpClient.EnableSsl = false;
                }

                StringBuilder body = new StringBuilder()
                    .AppendLine("A new order has been submitted")
                    .AppendLine("---")
                    .AppendLine("Items:");

                foreach (var line in cart.Lines)
                {
                    var subTotal = line.Product.Price * line.Quantity;
                    body.AppendFormat("{0} x {1} (subtotal: {2:c})", line.Quantity, line.Product.Name, subTotal);
                }

                body.AppendFormat("Total order value: {0:c}", cart.ComputeTotalValue())
                    .AppendLine("---")
                    .AppendLine("Ship to:")
                    .AppendLine(shippingInfo.Name)
                    .AppendLine(shippingInfo.Line1)
                    .AppendLine(shippingInfo.Line2 ?? "")
                    .AppendLine(shippingInfo.Line3 ?? "")
                    .AppendLine(shippingInfo.City)
                    .AppendLine(shippingInfo.State ?? "")
                    .AppendLine(shippingInfo.Country)
                    .AppendLine(shippingInfo.Zip)
                    .AppendLine("---")
                    .AppendFormat("Gift wrap: {0}", shippingInfo.GiftWrap ? "Yes" : "No");

                MailMessage mailmessage = new MailMessage(
                    emailSettings.MailFromAddress,//Form
                    emailSettings.MailToAddress,//To
                    "New order submitted!",//Subject
                    body.ToString());//Body

                if (emailSettings.WriteAsFile)
                {
                    mailmessage.BodyEncoding = Encoding.ASCII;
                }
                smtpClient.Send(mailmessage);
            }
        }
    }
}

为了使事情更简单些,也定义了 EmailSettings 类。 EmailOrderProcessor 的构造器需要这个类(EmailSettings 类)的一个实例,该实例包含了配置 .NET 邮件类所需要的全部设置信息。

提示:如果没有可用的 SMTP 服务器也没关系,可以将 EmailSettings.WriteAsFile属性设置为true,这样会把邮件消息作为文件写到由 FileLocation 属性指定的目录。该目录必须依据存在且是可写入的。邮件文件的扩展名将为 .eml ,但它们可以被任何文本编辑器所读取。

 

3.4 注册(接口)实现

现在,有了 IOrderProcessor 接口的一个实现以及配置它的手段,便可以用 Ninject 来创建它的实例。编辑 SportsStore.WebUI 项目中的 NinjectController 类(在 Infrastructure 文件夹中),对 AddBindings 方法进行修改:

using Moq;
using Ninject;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
using SportsStore.Domain.Concrete;
using System.Configuration;


namespace SportsStore.WebUI.Infrastructure
{
    public class NinjectControllerFactory : DefaultControllerFactory
    {
        private IKernel ninjectKernel;

        public NinjectControllerFactory()
        {
            ninjectKernel = new StandardKernel();
            AddBindings();
        }

        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            return controllerType == null
                ? null
                : (IController)ninjectKernel.Get(controllerType);
        }

        private void AddBindings()
        {
            //put bindings here
            ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>();

            EmailSettings emailSettings = new EmailSettings
            {
                WriteAsFile = bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false")
            };
            ninjectKernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings);
        }
    }
}

上述代码创建了一个 EmailSettings 对象,将其用于 Ninject 的 WithConstructorArgument 方法,以便在需要创建一个新实例对 IOrderProcessor 接口的请求进行服务时,把它注入到 EmailOrderProcessor 构造器中。上述代码只为 EmailSettings 中一个属性 WriteAsFiles 指定了值。使用 ConfigurationManager.AppSettings 属性来读取该属性的值,这让用户能够访问已经放在 Web.config 文件中的应用程序,在应用程序配置文件 Web.config 中读取这个 WriteAsFile 属性的设置值,这也是从应用程序配置文件 Web.config 中读取某个属性设置值的方法。配置文件修改如下:

  <appSettings>
    <add key="webpages:Version" value="2.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="PreserveLoginUrl" value="true" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusivejavascriptEnabled" value="true" />
    <add key="Email.WriteAsFile" value="true"/>
  </appSettings>

 

3.5 完成购物车控制器

为了完成 CartController 类,需要修改构造器,以使它要求 IOrderProcessor 接口的一个实现,并添加一个新的动作方法,它将在客户点击“Complete order”按钮时,处理 HTTP 表单的 POST 请求。

using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace SportsStore.WebUI.Controllers
{
    public class CartController : Controller
    {
        private IProductRepository repository;
        private IOrderProcessor orderProcessor;

        public CartController(IProductRepository repo, IOrderProcessor proc)
        {
            repository = repo;
            orderProcessor = proc;
        }

        public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)
        {
            Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
            if (product != null)
            {
                cart.AddItem(product, 1);
            }
            return RedirectToAction("Index", new { returnUrl });
        }

        public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl)
        {
            Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
            if (product != null)
            {
                cart.RemoveLine(product);
            }
            return RedirectToAction("Index", new { returnUrl });
        }

        public ViewResult Index(Cart cart, string returnUrl)
        {
            return View(new CartIndexViewModel
            {
                Cart = cart,
                ReturnUrl = returnUrl
            });
        }

        public PartialViewResult Summary(Cart cart)
        {
            return PartialView(cart);
        }


        [HttpPost]
        public ViewResult Checkout(Cart cart, ShippingDetails shippi

以上是关于MVC 47.SportsSore:完成购物车的主要内容,如果未能解决你的问题,请参考以下文章

如何从淘汰赛 mvc 购物车样本中计算购物车总价值

asp.netcore3.0mvc如何添加订餐系统购物车模块

使用MVC / EF总结购物车中的重复订单项

PHP match_mvc的用法

仍然可以在 ASP.NET mvc 中使用 Session 变量,或者对于某些事情(比如购物车)有更好的选择

php_mvc实现步骤三,四