从头开始编写一个Orchard网上商店模块(6) - 创建购物车服务和控制器

原文地址: 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_ProductParts_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使它变更有趣点。

时间: 2024-10-12 03:28:00

从头开始编写一个Orchard网上商店模块(6) - 创建购物车服务和控制器的相关文章

从头开始编写一个Orchard网上商店模块(5) - 创建和渲染ProductCatalog的内容类型

原文地址: http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-part-5创建和渲染ProductCatalog的内容类型 这是从头开始编写一个新的Orchard模块的教程的第5篇.对于本教程的概述,请参阅介绍. 为了网站的访问者能够将产品添加到他们的购物车,我们需要一个产品目录.产品目录可以是一个简单的产品清单.然而,在本教程中,我们希望主题作者能够接管渲染(r

从头开始编写一个Orchard网上商店模块(3) - 创建Orchard.Webshop模块项目

原文地址:http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-part-3创建Orchard.Webshop模块项目 这是从头开始编写一个新的Orchard模块的教程的第3篇.对于本教程的概述,请参阅介绍. Orchard模块是一个真正的ASP.NET MVC的Area类库,同时遵循了ASP.NET MVC和Orchard的特定的规范.Orchard 规范提升了您的

手把手教你编写一个简单的PHP模块形态的后门

看到Freebuf 小编发表的用这个隐藏于PHP模块中的rootkit,就能持久接管服务器文章,很感兴趣,苦无作者没留下PoC,自己研究一番,有了此文 0×00. 引言 PHP是一个非常流行的web server端的script语言.目前很多web应用程序都基于php语言实现.由于php是个开源软件并易于扩展,所以我们可以通过编写一个PHP模块(module 或者叫扩展 extension)来实现一个Backdoor. 本文就简单介下如何一步步编写一个简单的php 动态扩展后门. 0×01. p

从头开始编写一个实时嵌入式操作系统的内核(二)

一.RTOS里面的重要数据结构----链表 很多RTOS包括Linux的内核在内,内核里面都大量使用了链表这一种数据结构.内核的链表一般都是双向循环链表,这是因为双向循环链表的效率是最高的,找头节点.尾节点,直接前驱.直接后继时间复杂度都是O(1),这是使用单链表.单向循环链表或其他形式的链表是不能完成的.我们平时上课所学的链表一般都是指针域和数据域,但是如果有研究过Linux内核里面链表的人应该知道和我之前见到的链表结构不一样,只有前驱和后继指针,而没有数据域.Linux内核链表在linux源

从头开始编写一个实时嵌入式操作系统的内核(一)

今年大四,在准备自己的毕业设计.因为毕设题目是一个比较复杂的多传感器监控的嵌入式系统,然后最近自己有使用一些rtos,比方说freertos和ucos,感觉比起单纯对单片机的裸机开发还是有很多好玩的地方.特别喜欢这种抢占式和时间片轮询这两种内核调度模式,所以最近在开始想自己尝试去写一个实时的操作系统的内核调度,看看用自己浅薄的技术,自己去实现会怎么弄,纯粹为了好玩哈哈哈.花了大概几天左右的时间,现在已完成了一个时间片轮询和优先级抢占的实时任务调度内核了,可能有些地方还有些bug,后面有空再慢慢修

MFC+WinPcap编写一个嗅探器之六(分析模块)

这一节是程序的核心,也是最复杂的地方 首先需要明白的一点是,一般对于一个有界面的程序来说,往往需要多线程.本程序中除了界面线程外,抓包需要另外创建一个新的线程.在写抓包函数之前,首先要将前面两个模块的结果返回到主对话框界面对应的类实现中,在SnifferDlg.cpp中,修改之前增加的两个模块的触发函数如下: 1 void CSnifferDlg::OnAdp() 2 { 3 // TODO: 在此添加命令处理程序代码 4 CAdpDlg adpdlg; 5 if(adpdlg.DoModal(

MFC+WinPcap编写一个嗅探器之五(过滤模块)

这一节主要介绍如何获设置捕获过滤,这里的过滤是指在捕获前过滤 设置捕获过滤主要是在CFilterDlg中完成,也就是对应之前创建的设置过滤规则对话框,如图: 首先要根据用户的选择来生成一个合法的过滤规则字符串,根据WinPcap的要求,合法的过滤规则可以是如下几种: 1) 表达式支持逻辑操作符,可以使用关键字 and.or.not对子表达式进行组合,同时支持使用小括号.2) 基于协议的过滤要使用协议限定符,协议限定符可以为ip.arp.rarp.tcp.udp等.3) 基于MAC地址的过滤要使用

MFC+WinPcap编写一个嗅探器之四(获取模块)

这一节主要介绍如何获取设备列表,比较简单 获取设备列表主要是在CAdpDlg中完成,也就是对应之前创建的选择适配器模块,如图: 当打开选择适配器对话框后,在列表视图控件中显示当前主机所有适配器及适配器的描述,当选中一个适配器时,在下方的编辑框中会显示当前选中的适配器,单击绑定会提示网卡绑定成功. 本节中代码都在AdpDlg.cpp中完成,当然变量的声明要在其对应的头文件中去做,这里就不再叙述. 首先要获取设备列表,在OnInitDialog函数中加入如下代码: 1 if (pcap_findal

Golang中使用heap编写一个简单高效的定时器模块

定时器模块在服务端开发中非常重要,一个高性能的定时器模块能够大幅度提升引擎的运行效率.使用Golang和heap实现一个通用的定时器模块,代码来自:https://github.com/xiaonanln/goTimer 也可以查看文档:http://godoc.org/github.com/xiaonanln/goTimer,下面是完整的代码,并进行适当的注释和分析. 从性能测试结果来看,基于heap的定时器模块在效率上并不会比时间轮(TimeWheel)的实现慢多少,但是逻辑上要简单很多.