原文地址:http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-part-4
定义的ProductPart
这是从头开始编写一个新的Orchard模块的教程的第4篇。
对于本教程的概述,请参阅介绍。
Orchard中的内容项(Content Items)由Content Parts组成。使用我们的网上商店模块,我们希望管理员可以通过把ProductPart附加到任何内容类型(ContentType)上,组成商品(Product)。
例如,用户可以定义一个“书”的内容类型,附加上ProductPart,就把“书”转化成了“商品”,然后就可以添加到购物车和订单及其明细中。
创建一个ProductPart,从而可以存储数据到数据库中,我们需要创建ProductRecord,并继承自Orchard.ContentManagement.Records.ContentPartRecord。
Orchard使用后缀“Record”作为规范,使用NHibernate ORM持久化。
- 添加对Orchard.Framework项目的引用,以便能够从Orchard.ContentManagement.Records.ContentPartRecord上继承。
- 添加新文件夹命名为”Models”
- 在”Models”文件夹中,创建一个新的类命名为ProductRecord。
- 在”Models”文件夹中,创建一个新的类命名为ProductPart,并从ContentPart <ProductRecord>继承。
- ProductPart将有以下属性:Price(价格)和SKU。
您的解决方案现在应该是下面的样子的:
ProductRecord.cs:
在Orchard可以映射,加载和保存ProductRecord对象的实例到数据库之前,我们需要告诉它数据库表结构是什么样子。
我们通过创建一个叫Migration的类,一个继承自Orchard.Data.Migration.DataMigrationImpl,并调用一些方法来定义数据库结构(schema)的类。
在Migration里,我们告诉Orchard哪些表要创建和哪些ContenteTypes 和 ContentParts要创建。
在你的模块的跟目录下,创建一个名为Migrations.cs的新类,并敲入如下代码:
using Orchard.Data.Migration;
namespace Orchard.Webshop {
public class Migrations : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("ProductRecord", table => table
.ContentPartRecord()
.Column<decimal>("Price")
.Column<string>("Sku", column => column.WithLength(50))
);
return 1;
}
}
}
“Create”是Orchard使用一个规范,在启用模块时,它将为调用。
ContentPartRecord方法是一种简便的方法,用于创建一个ID列,并设置为主键:
/// <summary>
/// Defines a primary column as for content parts
/// </summary>
public CreateTableCommand ContentPartRecord() {
Column<int>("Id", column => column.PrimaryKey().NotNull());
return this;
}
在我们的例子中,我们已经启用了模块。但果Orchard很聪明,它检测到现有一个DataMigration可用, 然后它会检查当前存储的Migration版本号,我们的模块启用时,没有Migration可运行,所以在Orchard_Framework_DataMigrationRecord表也找不相应的版本号:
当您刷新Orchard Admin页面,Orchard将显示一个通知,显示需要升级一些功能:
点击Orchard.Webshop链接,您将直接调转到模块页面,并显示Orchard.Webshop功能:
当您单击“Upgrade”,Orchard将调用我们的Migrations类的Create方法,从而创建名为Orchard_Webshop_ProductRecord表:
正如你可以看到,Orchard也在Migrations表插入一个新的记录,其中包含了我们的Migration类的类名和它返回的最后一个版本号。
Create方法返回值1。
现在我们创建了一个表,可以存储的产品信息,但是这还不够,我们需要告诉Orchard ProductPart是“可以被附加的(attachable)”:这是组合ContentTypes的关键,从而使用例子的“书”可以变成商品。
要做到这一点,我们要在Migration类中添加一个名为UpdateFrom1的方法,使ProductPart成为attachable:
public int UpdateFrom1() {
ContentDefinitionManager.AlterPartDefinition(typeof(ProductPart).Name, part => part
.Attachable()
);
return 2;
}
为了能够使用AlterPartDefinition方法和Attachable方法,我们需要导入的命名空间和引用Orchard.Core项目。
小提示:ReSharper的会告诉你哪些程序集要引用和命名空间要引用,在大量的项目中,这还是真挺有帮助的。
完整的migration,现在看起来像这样:
using Orchard.ContentManagement.MetaData;
using Orchard.Core.Contents.Extensions; using Orchard.Data.Migration;
namespace Orchard.Webshop {
public class Migrations : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("ProductRecord", table => table
.ContentPartRecord()
.Column<decimal>("Price")
.Column<string>("Sku", column => column.WithLength(50))
);
return 1;
}
public int UpdateFrom1() {
ContentDefinitionManager.AlterPartDefinition(typeof(ProductPart).Name, part => part
.Attachable()
);
return 2;
}
}
}
当你修改了你的模块,就像我们只是刚刚作的,你只需要保存文件:当你刷新页面,Orchard将重新编译模块。
当你刷新管理页面,Orchard将再次显示一个通知说,一些功能需要进行升级:
我们将继续并更新功能。这样做将导致migration记录的版本号被更新为2,并且使ProductPart成为了attachable:它将在Settings_ContentPartDefinitionRecord表创建一条新的记录,并设置为true:
为了验证,我们有一个ProductPart可用,我们导航到Content -> Content Parts,很神奇,我们干了这么多工作:
很好!现在,让我们继续前进,创建一个新的ContentType,就叫“书”,并给它附上ProductPart:
创建一个新的内容类型:
- 转到Content -> Content Type,并点击”Create new Type”按钮;
- 输入“Book”作为显示名称和内容类型ID(这个会为你自动完成),然后按“Create”按钮;
- 这个叫”Book”的内容类型已经创建好了。现在,我们要真正定义这个书的类型,(说到底,任何内容类型都是一组ContentParts的集合), 所以我们的挑选的以下部分,作为书的类型,使它更一个书的商品:Body, Comments, Containable, Product, Route和Tags, 然后点击“保存”。
现在我们有一个书的Content Type,这也是一种商品类型。我们还附加了Containable Part,这样我们可以把Book添加列表或其他包含Container Part的Content Tyeps中。
由于我们还附上了Route,我们还有了书的标题和URL。Comments让网站的访问者对本书发表评论和Tags Part,允许管理员给书添加标签。
然而,当我们试图建立一个实际的书,我们看到各种输入字段,但是还没有Price和SKU字段:
这是怎么回事?Orchard的工作方式是这样的,为了呈现任何内容,Orchard调用每个ContentPart为Driver。一个Driver很类似与一个MVC Controller,但它只负责处理Part的内容,而不是整个HTTP请求本身。
Driver通常有3个操作方法:一个用于网站的前端显示部分,一个用于该网站的后端显示编辑模式,还有一个处理当管理员保存内容项时的回发(Postback)。
Driver通常返回的ActionResult是一个ShapeResult。ShapeResult告诉Orchard使用Razor模板呈现这个Part,还包含了动态对象将作为的Razor模板的模型。这种模型被称为Shape(形状),是一个动态”粘土”对象。
因此,一个模板可以被看作是“皮肤”的形状,使用Rqazor视图(.cshtml)作为实现。形状本身则是Razor视图模型。
在编辑模板的情况下,该模板将包含特定部分内容的编辑字段。
这种Shapes和Drivers的概念可能是Orchard的最强大的功能之一,也是最具挑战性的概念,真正挑战你的大脑(如果你像我一样反应慢,那还真有挑战)。
但实际上,当一旦你看到它是如何工作的,它还是很简单的,让我们继续。
让我们为我们的ProductPart创建Driver程序:
- 建一个新的文件夹,起名叫Drivers
- 在该文件夹内,创建一个新类名为ProductDriver
ProductDriver类看起来像这样:
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
namespace Orchard.Webshop.Drivers {
public class ProductDriver : ContentPartDriver<ProductPart> {
protected override DriverResult Editor(ProductPart part, dynamic shapeHelper)
{
return ContentShape("Parts_Product_Edit", () => shapeHelper.EditorTemplate(TemplateName: "Parts/Product", Model: part, Prefix: Prefix));
}
protected override DriverResult Editor(ProductPart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
}
}
Driver程序类,目前有两种方法:编辑(Edit)和处理回发的重载方法。现在我跳过了显示(Display)方法,但我们会稍后补上。
当Orchard想要显示ProductPart的编辑页面进,它调用Driver程序的Edit(编辑)方法。当管理员提交的编辑结果时,重载的Editor(编辑)方法(带IUpdateModel参数)将被调用。
在ContentShape方法被定义在ContentPartDriver的基类中,并从MVC ActionResult类继承。一个ContentShape实例告诉Orchard Shape的名称以及这个Shape(形状)看起来像什么:这个Shape有一个TempateName属性,一个Model属性和一个Prefix属性。Orchard要用到这些属性,因此它可以计算出实际使用哪个模板来呈现的形状,并给你一个机会,为要使用的模板提供Model(模型)。
在我们的例子中,我们的形状的名称是“Parts_Product_Edit”,它的模板放在“Parts/Product”下就可以被找得到。因为我们在谈论的编辑一个Content Part, Orchard 将在路上加上前缀“~//Orchard.Webshop/Views/EditorTemplates”,所以完整的路径将是:“~/ Orchard.Webshop /Views/EditorTemplates /Parts/Product.cshtml”。
然后我们就在那里创建Razor模板文件,它看起来像这样:
@model Orchard.Webshop.Models.ProductPart
<fieldset>
<legend>Product Fields</legend>
<div class="editor-label">@Html.LabelFor(x => x.Sku)</div>
<div class="editor-field">
@Html.EditorFor(x => x.Sku)
@Html.ValidationMessageFor(x => x.Sku)
</div>
<div class="editor-label">@Html.LabelFor(x => x.Price)</div>
<div class="editor-field">
@Html.EditorFor(x => x.Price)
@Html.ValidationMessageFor(x => x.Price)
</div>
</fieldset>
在Orchard调用Editor方法返回并实际呈现形状之前,我们需要定义形状的位置。
Placement是一个可以帮助确定在什么位置,和在什么区域(Zone) (其实它也是一个形状) 呈现一定的形状的系统。
我们可以通过在我们的模块项目的根目录下创建一个名为”Placement.info”的文本文件, 来 定义我们的“Parts_Product_Edit”(记住,我们在ProductDriver的编辑方法定义的形状的名称),它看起来像这样:
<Placement>
<Place Parts_Product_Edit="Content:1" />
</Placement>
这是告诉Orchard把任何名为“Parts_Product_Edit”的形状放置在第二的位置上被称为“内容”区域中的(第一的位置从0开始,由RoutablePart占用。尝试不同的位置,找一种你认为最好的样子)。
为了提高Razor模板的IntelliSense(智能感知),我们现在应该添加一个web.config项目,以及引用System.Web.Mvc组件(在Orchard源码在lib文件夹下)。
web.config看起来像这样:
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<remove name="host" />
<remove name="pages" />
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
</sectionGroup>
</configSections>
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<pages pageBaseType="Orchard.Mvc.ViewEngines.Razor.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="System.Linq"/>
<add namespace="System.Collections.Generic"/>
<add namespace="Orchard.Mvc.Html"/>
</namespaces>
</pages>
</system.web.webPages.razor>
<system.web>
<compilation targetFramework="4.0">
<assemblies>
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</assemblies>
</compilation>
</system.web>
</configuration>
当我们现在再添加一本书的内容项时,我们将看到,我们的产品编辑模板像我们预计的一样显示在那里:
让我们来测试并创造3本书:
- The Hobbit, $50, SKU-1001
- First Wizards Rule, $39, SKU-1002
- The Hunger Games, $29, SKU-1003
这的确会创建3个新的书的内容项。然而,当你编辑其中之一,你将看到该Price和SKU是空的!
这是怎么回事儿呢?
问题是,Orchard不知道哪里来存储信息。我们需要在ContentHandler中添加一个StorageFilter(这是类似于MVC的ActionFilter),并使用IRepository <ProductRecord>,保存产品零件。
要创建一个ContentHandler,增加了一个StorageFilter
1。创建一个新文件夹命名为Handlers
2。创建一个新的类名为ProductHandler从ContentHandler的派生
编写以下代码:
using Orchard.ContentManagement.Handlers;
using Orchard.Data;
using Orchard.Webshop.Models;
namespace Orchard.Webshop.Handlers {
public class ProductHandler : ContentHandler {
public ProductHandler(IRepository<ProductRecord> repository)
{
Filters.Add(StorageFilter.For(repository));
}
}
}
我们在这里看到超级棒的依赖注入模式在工作着:Orchard注入了一个ProductRecord的repository到我们的处理程序,我们要作的只是简单地定义我们的类的构造函数包含到它的依赖。
接下来,我们添加了一个StorageFilter到Filters集合中,使Orchard保存和载入我们ProductPart的信息时调用到ProductPart Driver。
当我们尝试更新我们的书籍之一,我们注意到,Price(价格)和SKU字段真的保存了。
总的来说,要创建ContentPart,并能坚持到数据库中,有几个步骤要遵循:
- 创建一个Record类来表示你的实体
- 创建ContentPart类从ContentPart <TRecord>上派生
- 为你的Content Part创建Migration,用它定义数据库结构(schema)
- 创建一个为您的Content Part的Driver程序
- 创建Content Part的编辑模板
- 使用Handler(处理程序),添加Content Part的StorageFilter
有时你可能会发现,你只是想创建一个Content Part, 并不需要保存自定义属性到数据库。例如,Orchard Profile Model定义了ProfilePart, 但没定义ProfilePart类。在这种情况下,你只需要一个步骤:
1. 创建Migration来定义这个Content Part
或者,当你想呈现一个Content Part,你可以定义一个Driver程序,它将创建Shapes(形状)。
再或者,你可以创建一个ShapeTableProvider,简简单单的从Orchard.DisplayManagement.Descriptors.IShapeTableProvider类上派生。
刚开始,这看起来可能有点模糊,直到你找到它的实际需要。我们将很快就会用到的!