请注意,本站并不支持低于IE8的浏览器,为了获得最佳效果,请下载最新的浏览器,推荐下载 Chrome浏览器
欢迎光临。交流群:166852192

编写Orchard网上商店模块(4) - 创建购物车


这是从头开始编写一个新的Orchard模块的教程的第4篇。本章所讲的内容,为开发模块教基础的部分,请按照教程认真学习,这样才能更好的理解模块的开发; 上一篇文章链接地址:编写Orchard网上商店模块(3) - 定义ProductCatalog内容类型;
在本篇,我们将使我们的用户可以添加商品到他们的购物车。
要创建这样的功能,我们需要:
  • 一个“添加到购物车”按钮,这个按钮需要被添加我们的产品目录上,将产品添加到购物车
  • 某种购物车服务,以存储添加的项
  • 可以看到购物车内的概况,以及“继续结帐”等按钮
  • 在每个页面上都显示我们的购物车页面的链接,以及目前库存可用数量的部件
   
下面让我们开始第一项:在我们的产品目录上显示“添加到购物车”按钮。

渲染“添加到购物车”按钮


正如在以前的文章中看到的,一个产品目录基本上是内容项的列表。
现在目录中每个单个项目都会被渲染,从面组成由内容元件组成的集合。在本教程中,目录包含书的内容项目的列表,其中每个“书”又是由元件内容组成,我们已经在第3篇介绍过了 – 创建ProductParts
Orchard渲染一个内容项时,它调用的内容项目的每个元件的部件驱动(Driver)。反过来每个元件的驱动程序创建一个新的形状,然后用Razor模板渲染。
我们已经有了我们的“Parts_Product”形状模版,由ProductPart驱动程序创建的,让我们修改它添一个“添加”按钮。
在Visual Studio中,打开你的View文件夹的Parts.Product.cshtml:

修改View的代码,如下:
@{
    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的驱动:
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)可以安装Designer tools:

我们在左窗格中看到的是整个已创建的形状的树结构。
列表形状是由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的问题。
现在,我们既可以关闭该功能,也可能添加防伪相关字段。我们将过会儿作,因为它可能正确的事。幸运的是,这很容易,因为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使它变更有趣点。
原文:http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-part-6 
感谢瑞雪年
下一篇 编写Orchard网上商店模块(5) - 渲染购物车和部件

作者原创内容不容易,如果觉得内容不错,请点击右侧“打赏”,赏俩给作者花花,也算是对作者付出的肯定,也可以鼓励作者原创更多更好内容。
更多详情欢迎到QQ群 166852192 交流。