原文地址: http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-part-6
创建购物车服务和控制器
这是从头开始编写一个新的Orchard模块的教程的第6篇。
对于本教程的概述,请参阅介绍。
在本篇,我们将使我们的用户可以添加商品到他们的购物车。
要创建这样的功能,我们需要:
- 一个“添加到购物车”按钮,要被添加我们的产品目录上,将产品添加到购物车
- 某种购物车服务,以存储添加的项
- 我们的ShoppingCart的概况,以及“继续结帐”按钮
- 在每个页面上都显示我们的ShoppingCart页面的链接,以及目前库存可用数量的小部件
让我们开始第一项:在我们的产品目录上显示“添加到购物车”按钮。
渲染“添加到购物车”按钮
正如在以前的帖子中看到,一个产品目录基本上是内容项目的列表。
反过来,在目录中每个单个项目被渲染,从面组成部分内容的集合。在本教程中,目录包含书的内容项目的列表,其中每个“书”又是由部件内容组成,我们已经在第4篇介绍过了 – 创建ProductParts。
Orchard渲染一个内容项时,它调用的内容项目的每个部件的部件驱动(Driver)。反过来,每个部分的驱动程序创建一个新的形状,然后用Razor模板渲染。
我们已经有了我们的“Parts_Product”形状模版,由ProductPart驱动程序创建的,让我们修改它添一个“添加”按钮。
在Visual Studio中,打开你的View文件夹的Parts.Product.cshtml:
修改标记的代码:
@{ var price = (decimal) Model.Price; var sku = (string) Model.Sku;}<article> Price: @price<br/> Sku: @sku <footer> <button>Add to shoppingcart</button> </footer></article>
现在我们的产品目录看起来像这样:
这真太简单了。现在,在现实世界中的主题中,你可能要自定义每个内容项的外观,并在列表中呈现。例如,你可能想在正文后面显示 “添加到购物车”按钮,但是价格和SKU字段占了他们的当前位置。我们可以通过至少两种方法实现:
A:我们完全接管我们的列表的渲染工作
B:我们创建一个新的形状,在ProductPart驱动中显示我们的按钮,并且用Placement.info把它放置在内容项中我们希望的任何位置上。
这两种方法都不错,但我还是会建议尽量使用方法B,因为它更灵活。例如,如果在某个阶段网站管理员决定扩展这个书的内容类型,添加一个字段或部件,这部分字段或部件将自动被渲染,就没有必要为这部分字段和部件修改模板。
如果您正在编写自定义主题,有特定的需求要影响整个内容项的外观和行为,那么你可以选择方法A,然后你有完全的自由。但是,每次添加部件或字段,您都要更新的模板,这将剥夺你赢得的自由。
我们将使用方法B在BodyPart下面显示这个按钮(你可以在这里阅读更多关于形状和区域)。
要创建一个新的形状,我们首先修改我们ProductPart的驱动:
To create a new shape, we first modify our ProductPart driver:
protected override DriverResult Display(ProductPart part, string displayType, dynamic shapeHelper) { return Combined( ContentShape("Parts_Product", () => shapeHelper.Parts_Product( Price: part.Price, Sku: part.Sku )), ContentShape("Parts_Product_AddButton", () => shapeHelper.Parts_Product_AddButton()) ); }
我们在这里创建一个额外的形状名为“Parts_Product_AddButton”并通过CombinedResult返回它,使用Combined方法创建。 CombinedResult类是从DriverResult上派生的一个简单的类,并持有的DriverResults的IEnumerable。
实际上,我们正在告诉Orchard同时渲染名为Parts_Product的形状以及名为Parts_Product_AddButton形状。
下一步,我们创建一个新的模板文件名为“Parts.Product.AddButton.cshtml”将包含形状的标记:
<button>Add to shoppingcart</button>
现在我们需要告诉Orchard这种形状的需要呈现在什么地方,修改的Placement.info的文件如下:
<Placement> <Place Parts_Product_Edit="Content:1" /> <Place Parts_Product="Content:0" /> <Place Parts_Product_AddButton="Content:after" /></Placement>
我们增加了第三个的<Place/>元素配置Parts_Product_AddButton形状,显示在内容区域后。关键是要了解这里所呈现的内容项目是一个形状,每个形状可以有子形状。 “Content”是一个内容形状内部的形状的本身的名称。为了可见,使用形状跟踪(Shape Tracer):
我们在左窗格中看到的是整个已创建的形状的树结构。
列表形状是由ContainerPart驱动程序创建的。每个内容的形状是Orchard创建的,形状的容器是由内容部件的驱动程序创建的。
正如你可以看到,我们的ProductPart驱动程序创建了两个形状:Parts_Product和Parts_Product_AddButton。还要注意的是Parts_Product_AddButton在列表中的最后。在Placement.info文件中配置的顺序,决定了一个形状添加到父形状的先后
要启用形状跟踪,请确保你已经安装的设计工具(Designer Tools)模块。一旦安装后,你就可以启用形状跟踪功能了。不要忘了在您的网站发布到生产服务器时,把该功能关闭,因为它不大怎么会提高您的站点的性能。
添加产品到ShoppingCart
现在,我们有一个按钮,我们需要使它被点击时,做一些有用的事!
我们是出色的MVC开发人员,我们将创建一个控制器(Controller)与处理POST请求的行动。每当用户点击“添加到购物车”按钮,我们将调用该操作。
我们将继续,开始创建一个Controllers(控制器)文件夹到我们的模块和添加名为ShoppingCartController控制器。
我们还将添加一个名为Add的Action(活动),包含一个id参数,代表要被添加到我们的购物车的产品。
我们还需要决定他按下按钮后,用户将看到什么。对于这个演示中,我们将用户重定向到购物车页面(我们将过一会儿创建)。
最初的代码应该看起来像这样:
请注意,我们使用一个HTTP POST请求。虽然这不是必需的,HTTP规范建议,要求修改服务器上的状态时,你应该发出一个POST,而不是GET。由于我们的“添加”的操作方法将会改变我们的用户的购物车,我们使用一个POST。
为了使按钮来调用这个方法,我们需要修改“Parts.Product.AddButton.cshtml”添加<FORM>元素的标记:
@using (Html.BeginForm("Add", "ShoppingCart", new { id = -1 })) { <button type="submit">Add to shoppingcart</button>}
请注意,我们目前的“id”硬编码值指定为-1。当我们要添加产品时,我们需要用产品ID来替换。
为了获得这些信息,我们需要把它包含到们的形状,所以我们需要修改ProductPart的驱动:
protected override DriverResult Display(ProductPart part, string displayType, dynamic shapeHelper) { return Combined( ContentShape("Parts_Product", () => shapeHelper.Parts_Product( Price: part.Price, Sku: part.Sku )), ContentShape("Parts_Product_AddButton", () => shapeHelper.Parts_Product_AddButton( ProductId: part.Id )) ); }
我们增加了一个参数来调用Parts_Product_AddButton名为ProductID,这将成为我们的模板模型的属性。
返回到我们的模板并修改它:
@{ var productId = (int) Model.ProductId;} @using (Html.BeginForm("Add", "ShoppingCart", new { id = productId })) { <button type="submit">Add to shoppingcart</button>}
这很容易。现在,我们已经把我们的按钮和我们的购物车控制器串了起来,现在是时候创建一个实际的ShoppingCart类,来管理我们的客户购物车!
让我们创建一个新的文件夹,名为Services(服务),并创建一个IShoppingCart接口和一个ShoppingCart类实现该接口。
虽然不是必需要定义一个接口,这被认为是很好的做法,使我们的控制器和其他类依赖于抽象,而不是具体的实现。这通常是可取的,当我们写我们的模块的单元测试,这使我们可以使用一个“假的”(mocked)版本作代理实现IShoppingCart。
我们的IShoppingCart的最初版本将看起来像这样:
using System.Collections.Generic;using Orchard.Webshop.Models; namespace Orchard.Webshop.Services { public interface IShoppingCart : IDependency { IEnumerable<ShoppingCartItem> Items { get; } void Add(int productId, int quantity = 1); void Remove(int productId); ProductRecord GetProduct(int productId); decimal Subtotal(); decimal Vat(); decimal Total(); decimal ItemCount(); }}
我们还将创建一个类ShoppingCartItem 到Models文件夹内,看起来像这样:
using System; namespace Orchard.Webshop.Models { [Serializable] public sealed class ShoppingCartItem { public int ProductId { get; private set; } private int _quantity; public int Quantity { get { return _quantity; } set { if (value < 0) throw new IndexOutOfRangeException(); _quantity = value; } } public ShoppingCartItem() { } public ShoppingCartItem(int productId, int quantity = 1) { ProductId = productId; Quantity = quantity; } }}
ShoppingCartItem类将包含已添加的产品的ID及其数量。
最初的ShoppingCart实现将看起来像这样:
using System.Collections.Generic;using System.Linq;using System.Web;using Orchard.Data;using Orchard.Webshop.Models; namespace Orchard.Webshop.Services { public class ShoppingCart : IShoppingCart { private readonly IWorkContextAccessor _workContextAccessor; private readonly IRepository<ProductRecord> _productRepository; public IEnumerable<ShoppingCartItem> Items { get { return ItemsInternal.AsReadOnly(); } } private HttpContextBase HttpContext { get { return _workContextAccessor.GetContext().HttpContext; } } private List<ShoppingCartItem> ItemsInternal { get { var items = (List<ShoppingCartItem>)HttpContext.Session["ShoppingCart"]; if (items == null) { items = new List<ShoppingCartItem>(); HttpContext.Session["ShoppingCart"] = items; } return items; } } public ShoppingCart(IWorkContextAccessor workContextAccessor, IRepository<ProductRecord> productRepository) { _workContextAccessor = workContextAccessor; _productRepository = productRepository; } public void Add(int productId, int quantity = 1) { var item = Items.SingleOrDefault(x => x.ProductId == productId); if (item == null) { item = new ShoppingCartItem(productId, quantity); ItemsInternal.Add(item); } else { item.Quantity += quantity; } } public void Remove(int productId) { var item = Items.SingleOrDefault(x => x.ProductId == productId); if (item == null) return; ItemsInternal.Remove(item); } public ProductRecord GetProduct(int productId) { return _productRepository.Get(productId); } public void UpdateItems() { ItemsInternal.RemoveAll(x => x.Quantity == 0); } public decimal Subtotal() { return Items.Select(x => GetProduct(x.ProductId).Price * x.Quantity).Sum(); } public decimal Vat() { return Subtotal() * .19m; } public decimal Total() { return Subtotal() + Vat(); } public decimal ItemCount() { return Items.Sum(x => x.Quantity); } private void Clear() { ItemsInternal.Clear(); UpdateItems(); } }}
它基本上只是一个HttpContext.Session集合的包装,我们用来存储ShoppingCartItems的列表。
注意,我们使用一个硬编码值指定增值税税率为19%,但我们稍后会将我们的模块让用户可配置。
另外请注意,我们需要添加一个System.Web(4.0版)的引用,以便能够使用HttpContextBase类型。
为了获取一个HttpContext,我们注入IWorkContextAccessor的实例,它可以使我们能够访问当前请求和相关数据。
为了我们的购物车来计算一些汇总,它需要能够从数据库中装载的产品实体。因此,我们注入IRepository <ProductRecord>。
如果在列表中不存在,Add方法创建一个新ShoppingCartItem实例,如果存在一个实例,它只会累加总量(Amount)属性。
要使用我们的ShoppingCart和ShoppingCartController,需要添它的一个实例的引用。最简单的方法是让Orchard注入一个构造器。但为了让Orchard能够注册我们的类到依赖容器中,我们需要继承IDependency。
让我们继续前进,我们将到IShoppingCart从IDependency继承:
namespace Orchard.Webshop.Services { public interface IShoppingCart : IDependency { ... }}
现在,我们可以修改ShoppingCartController.cs上IShoppingCart的依赖和完成“Add”方法:
using System.Web.Mvc;using Orchard.Webshop.Services; namespace Orchard.Webshop.Controllers { public class ShoppingCartController : Controller { private readonly IShoppingCart _shoppingCart; public ShoppingCartController(IShoppingCart shoppingCart) { _shoppingCart = shoppingCart; } [HttpPost] public ActionResult Add(int id) { _shoppingCart.Add(id, 1); return RedirectToAction("Index"); } }}
为了使我们的用户能够看到的购物车,我们需要为它创建一个视图。让我们给它起名叫“Index”:
public ActionResult Index() { return View();}
按逻辑,下一步将是创建一个“Index”的View。但是,我们希望主题开发人员能够完全“重载(override)”我们默认情况下呈现的HTML,所以他们应该能够“重载(override)”Index视图。
我尝试在我的模块里创建一个视图,同时也放到了我的自定义主题里,但发现,Orchard使用了模块的,而不是在自定义主题的视图。
我不能说,本身就是这样设计还是我哪个地方搞错了(它不会是第一次),但我认为最好的解决方式是,返回一个ShapeResult,而不是ViewResult,因为形状基本上是一个View,但是更强大(形状可以有替补)。
因此,让我们返回一个形状(Shape),而不是返回一个视图(View)。为了创建一个形状,我们将使用ShapeFactory帮助我们。我们可以通过Orchard注入一个ShapeFactory到构造函数。
修改后的代码现在看起来像这样:
using System.Web.Mvc;using Orchard.DisplayManagement;using Orchard.Mvc;using Orchard.Webshop.Services; namespace Orchard.Webshop.Controllers { public class ShoppingCartController : Controller { private readonly IShoppingCart _shoppingCart; private readonly IShapeFactory _shapeFactory; public ShoppingCartController(IShoppingCart shoppingCart, IShapeFactory shapeFactory) { _shoppingCart = shoppingCart; _shapeFactory = shapeFactory; } [HttpPost] public ActionResult Add(int id) { _shoppingCart.Add(id, 1); return RedirectToAction("Index"); } public ActionResult Index() { var shape = _shapeFactory.Create("ShoppingCart"); return new ShapeResult(this, shape); } }}
我们还需要创建一个“购物车”形状的模板文件。创建一个名为“ShoppingCart.cshtml”文件到视图文件夹:
让我们继续前进,当我们将项目添加到购物车,看看会发生什么:
点击“添加到购物车”按钮:
嗯,这看起来不正确。那么,为什么我们得到一个404?
答案是我们的模块是真的只是一个MVC的Area(区域),Orchard在Routes(路由)集合中包括我们的模块的名称作为area的路由值。
因此,正确的路径应该是:
/Orchard.WebShop/ShoppingCart/Add/21 而不是/Containers/ShoppingCart/Add/21.
我们通过修改AddButton模板,包含area的值来解决:
@using (Html.BeginForm("Add", "ShoppingCart", new { area = "Orchard.WebShop", id = productId })) { <button type="submit">Add to shoppingcart</button>}
现在,让我们再次尝试:
这还不是我们想要的,但至少我们朝前走了一步。
现在的问题是,Orchard通过AntiForgeryAuthorizationFilterAttribute验证POST的问题s。
现在,我们既可以关闭该功能,也可能添加防伪相关字段。我们将过会儿作,因为它可能正确的事。幸运的是,这很容易,因为Orchard提供了一个辅助方法,该方法可以生成一个表单(Form)将自动包括一个隐藏的防伪领域:
@using (Html.BeginFormAntiForgeryPost(Url.Action("Add", "ShoppingCart", new { area = "Orchard.WebShop", id = productId }))) { <button type="submit">Add to shoppingcart</button>}
这样没有伤害,不是吗?
现在,让我们再次尝试:
完全正确!可然,所有其他的东西,像主菜单,CSS和布局都哪去了?
我们需要为我们的ShoppingCart提供一个母版页什么的吗?
完全不必:我们只需要告诉Orchard这个形状应当补充内容区域布局形状。我们可以把[Theme]属性加到我们的“Index”方法上来实现:
[Themed]public ActionResult Index() { var shape = _shapeFactory.Create("ShoppingCart"); return new ShapeResult(this, shape);}
当我们再次尝试:
我们知道,生活是美好的。更妙的是,我们可以毫无限制的向页面上添加我们想要的功能!我们知道,最重要的部分:如何创建和渲染的形状,如何创建控制器返回的形状与布局,及部件的布局。
当然也有相当多的东西需要学习,如所有其他集成和可扩展点,如何扩展管理界面,或利用缓存模块,使用的ContentManager管理和查询的内容,等等。
但在这个阶段最重要的是,如果你知道如何构建ASP.NET MVC应用程序,你可以放心地开始创建Orchard模块。
渲染购物车
渲染购物车很容易,但我们将通过融入knockoutJS使它变更有趣点。