瑞雪年

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: :: 订阅 订阅 :: 管理 ::

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

继续到第7篇 - 渲染ShoppingCart和ShoppingCartWidget

posted on 2012-02-18 00:26  瑞雪年  阅读(2800)  评论(6编辑  收藏  举报