首页 > 代码库 > 详细分析Orchard的Content、Drivers, Shapes and Placement 类型
详细分析Orchard的Content、Drivers, Shapes and Placement 类型
本文原文来自:http://skywalkersoftwaredevelopment.net/blog/a-closer-look-at-content-types-drivers-shapes-and-placement
在本文中,我们将看看那些引让我夜不能寐的概念,因为我的生活不能找出与:shapes, content types, parts, fields, drivers and placement 等类型对应的东西。如果你有些使用Orchard的经验,但仍然觉得有点笨拙的控制shapes,那么这篇文章是为你而准备的。
首先让我们看看,当用户的请求(request)到来时,Orchard是如何呈现Content的,drivers和placement是如何负责绘制页面,shapes的Content(值)是如何转化为HTML并发送给用户的。
本文是为那些模块和主题开发者,并对drivers、content、shapes、placement有一定使用经验人,希望更确切的领会到这一切是如何工作的人准备的。
如果准备好了,那我们将从shapes说起。
一、什么是 shape
那么什么是shape,是我们通常谈论的物体的形状?例如,地球的形状好似椭球,而埃及的金字塔是一个锥体结构多面体形装。shape这个词的意思是什么,Orchard的“shape”吗?,嗯,shape是一个实例类或静态类的动态对象。我们可以把各种各样的信息附加到运行时的shape,甚至可以添加一个列表。本质上我们可塑造我们想要的东西。我认为“shape”是:对象的动态形状能力(注:shape的本意是形状,但中文形状这一词并不能完全表达shape的意思,shape不仅包括外观,还有要演染的数据以及演染的方法等,比如:WCF中通道形状(Channel Shape),就是这个意思,如果Orchard的shape最终还能从视觉上感知到,那么Channel Shape只是一种概念,是实现具体的消息交换模式)。shape类看起就是:
1: public class Shape : Composite, IShape, IEnumerable<object> {
2: public ShapeMetadata Metadata { get; set; }
3: public Id { get; set; }
4: public IList<string> Classes { get; }
5: public IDictionary<string, string> Attributes { get; }
6: public IEnumerable<dynamic> Items { get; }
7:
8: public virtual Shape Add(object item, string position = null) {}
9: public virtual Shape AddRange(IEnumerable<object> items, string position = "5") {}
10: IEnumerator<object> IEnumerable<object>.GetEnumerator() {}
11: public virtual IEnumerator GetEnumerator() {}
12: public override bool TryConvert(ConvertBinder binder, out object result) {}
13: public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) {}
14: public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) {}
15: }
它的魔力就是使我们能通过Shape实现 IEnumerable<object>(可以遍历子Shape),经由TryInvokeMember方法,调用用一个内联的键值对字典(由Shape的基类实现称为Composite的类),当你调用TryInvokeMember,就能获取一个动态实现的Shape,代码可写成:
1: @{
2: var mySampleShape = New.MySampleShape();
3: mySampleShape.Color = "Green";
4: }
第2行,通过Shape工厂类创建了一个新的Shape,这个新的属性存储在View上,新的属性是一个dynamic类型
第3行,为刚创建的shape,设置了一个Color的属性,当我们执行代码的时候,通过TryInvokeMember来查询字典一个叫“Color”的对应的值。
这相当智能。
下面的文章有一些有趣的关于shape的额外信息: Using shapes as Html helpers in Orchard.http://www.szmyd.com.pl/blog/using-shapes-as-html-helpers-in-orchard
现在我们知道了更多关于shape知识,接下来让我们来谈谈他们用于什么。答案很简单:当然是渲染的目的。但这到底意味着什么呢?
二、渲染shape —— 命名语法
当我们谈论呈现一个shape,我们真正的意思是:使用shape对象模型来呈现一个Razor视图,。渲染操作的结果是一个字符串,该字符串发送到HTTP响应输出,就象Razor视图呈现Razor视图引擎那样做的。但Orchard是如何知道Razor怎样对待Shape时渲染呢?,我们可以控制哪些可用的Razor视图?是的,答案在于shape的Metadata(元数据)属性(注:一个ShapeMetadata类型的Metadata属性)。Metadata对象的字符串属性称为类型,并可能类似于“MySampleShape”。基于这个名字,Orchard将指示Razor视图引擎找到一个名为“MySampleShape.cshtml”的视图,使用Shape模型执行这一视图。Metadata属性还有一个称为 Alternates (替代)属性的IList<string>集合,我们将稍后讨论。
重要的是要知道,必须使用c#成员和变量命名规则命名shape的名称。这样做的原因是,我们可以创建shape,就好像是shape工厂方法名(正如我们之前看到的),而不需要确定的指定它的名字作为一个字符串值,当使用静态类型作为IShapeFactory的shape工厂(稍后详细介绍)。我们可以创建shape,好像我们是调用一个方法。例如,下面是有效的shape的名字:
MySampleShape
My_Sample_Shape
My__Sample_Shape
下面是非法的名称:
My.Sample-Shape
My_Sample-Shape
My-SampleShape
这些无效的原因是因为这些名字不会在c#编译。
然而,Orchard开发人员可能不喜欢强制使用这样的文件名(理应如此),所以他们想出了一个约定的Shape类型名称映射到Razor视图的文件名。
约定如下:
1、一个单一的下划线“_”被替换为一个点(.)。
2、双下划线“__”被替换为一个连字符(-)。
所以如果你有一个Shape命名为My_Sample__ShapeRazor文件,应查找“My.Sample-Shape.cshtml”。
三、Shapes从哪里来?
Shape实际上是从哪里来的呢?它是如何创建的?我们自己如何创建它们?这些问题的答案是:Shape是由一个叫做IShapeFactory接口实现类来创建。
正如我们已经看到的,在一个Razor视图中,你可以通过每个继承于Razor视图的Shape工厂访问新的属性。新属性是dynamic的静态类型,但它是一个实现了IShapeFactory类的实例。这新的属性类型是dynamic的原因是:让它更容易通过Shape工厂处理。例如,如果您有IShapeFactory静态类,下面代码演示的就是如何创建一个Shape:(注:以下代码应该放在一个 Razor文件。你可以选择你主题的Layout.cshtml文件,New工厂方法是 Orchard.Mvc.ViewEngines.Razor的WebViewPage类的一个方法);
1: @{
2:
3: var shapeFactory = (IShapeFactory)New; //为了演示,创建一个新的 IShapeFactory
4:
5: var myShape = (dynamic)shapeFactory.Create("MySampleShape", Parameters.From({
6: MyProperty1 = "Some property value",
7: MyProperty2 = 42
8: }));
9:
10: }
比较下面我们通过作为dynamic的shape工厂工作时:
1: @{
2:
3: var shapeFactory = New;
4:
5: var myShape = shapeFactory.MySampleShape(
6: MyProperty1: "Some property value",
7: MyProperty2: 42
8: );
9:
10: }
虽然的代码行数是相同,但语法更简单,看起来更好。还需要注意的是,我们能够就像shapeFactory上实现了“MySampleShape”方法一样创建一个Shape。当然不是这样(注:这里的意思是我们还没实现这样一个类,仍可以这样使用,但如果要让代码能正常运行,还是要实现的),但Shape工厂实现其动态行为拦截方法调用和基于被调用的方法名创建一个Shape,其参数和他们变成了Shape特性。非常整洁。
所以它是:shape来自IShapeFactory。只要你有一个IShapeFactory,您就可以创建shape。Razor视图通过访问shape工厂的New属性,并通过IShapeFactory注入您自己的代码。
四、渲染Shape —— Alternates(替代)
我们简要地提及shape alternates。它们是什么?技术上来说,他们只是一个字符串列表,存储在一个shape alternates属性中的元数据。当Orchard即将指示视图引擎查找视图呈现,它检查所有的alternates是否有一个视图与之匹配。Orchard适用相同的文件名映射shape名称语法,Razor视图文件名称语法如前所述。那么填充alternates的列表吗?答案是:shape对象可以添加alternates的任何代码。通常是使用所谓的Shape Table Providers来操作的。
五、Shape Table Providers
Shape Table Providers是实现了IShapeTableProvider接口的类,并将事件处理程序附加到Shape的一种方法。可能的Shape事件:
1、Creating
2、Created
3、Displaying
4、Displayed
通常做的是提供一个显示事件处理程序,并在这个处理程序可以添加alternates的Shape。让我们看看一个简单的例子。
首先,让我们想象我们创建一个有Color属性的Shape。我们的目的是:渲染Shape时,提供两个可以使用的Razor视图。使用的视图应该基于Shape的Color值。创建和渲染的Shape看起来像这样:
1: @{
2: var myBlueColoredShape = New.ColorShape(Color: "Blue");
3: var myRedColoredShape = New.ColorShape(Color: "Red");
4: }
5:
6: @Display(myBlueColoredShape)
7: @Display(myRedColoredShape)
为了让代码工作,我们至少应该创建一个视图文件,名称和ColorShape相匹配的名称。让我们创建一个默认的视图,显示指定的颜色:
Views/ColorShape.cshtml:
1: @{
2: var color = Model.Color;
3: }
4: <p>My color is: @color</p>
看看ColorShape.cshtml视图是如何访问到在Model中的Color属性,当我们通过Shape调用 @Display时,Orchard 的视图引擎会查找视图文件, 并通过Shape执行视图的模型。
现在,我们想使用一个基于Color的完全不同的视图文件。为此,我们在我们的模块中创建一个新的类文件如下:
1: public class ColorShapeTableProvider : IShapeTableProvider {
2: public void Discover(ShapeTableBuilder builder) {
3: builder.Describe("ColorShape")
4: .OnDisplaying(displaying => {
5: var shape = displaying.Shape;
6: var color = shape.Color;
7:
8: displaying.ShapeMetadata.Alternates.Add(String.Format("ColorShape__{0}", color));
9: });
10: }
11: }
我们做的第一件事是“Describe”(描述)shape(第3行),表示:“配置如下的shape”。
我们通过配置一个特别的当Orchard即将渲染我们的shape时会调用OnDisplaying方法的handler 。如果你对此表是疑惑,你不必担心覆盖其他事件处理程序,API将为shape注册多个配置项,并且工作得很好。
请注意,我们可以访问我们的shape和Color属性(第6行)。使用这个属性的值来生成另一种shape,并将其添加到的元数据Aternates集合中(第8行)。
与这种形状表提供者匹配,我们现在可以创建以下两个视图文件:
1、ColorShape-Blue.cshtml
2、ColorShape-Red.cshtml
有了这种处理方式,Orchard变得更好使用。例如:通过提供基于内容的Content类型名称和所使用的显示类型的alternates来渲染Content shapes。稍后将进行更详细的讨论。
我们已经看到如何创建一个使用shape工厂和使用继承于每一个视图Display渲染方法。我们继续之前我想表明,你现在可以在一行中创建和渲染这个自定义shape了。它看起来像这样:
1: @Display.ColorShape(Color: "Yellow")
这样只需一行去创建!
六、添加Shapes到Zone(区域)
这很酷!我们有了Shape是什么的更好的理解,以及它们是如何工作的。事实证明,这并不是说把事性搞复杂了(注:这里指的是上面需一行去创建Shape,而要增加的那些辅助类工作)。
让我们谈谈Zone。在Orchard里,当我们讨论Zone,我们讨论的是呈现在一些视图中(通常在Layouts.cshtml),可以固定shape的东西。那么这个东西到底是什么呢?
好亲爱的同学们(注:原文如此^o^),事实证明,Zone只是另一个shape,唯一的区别在于,它继承自称为ZoneHolding的类(注:定义在Orchard.UI.Zones),而ZoneHolding又是继承自shape。很快我们将会看到为什么这很重要。
我们通常谈论全局Zone和局部Zone。在root-of-all-shapes中义的区域定义的是全局Zone,是布局shape。Orchard为我们创造了这个shape,可以在每个视图中通过WorkContext属性访问。
如果您正在修改视图的视图布局shape,然后你就可以通过视图的Model属性直接访问这些Zone。再次重申,当你想要从其它views中的布局shape的zone增加shapes,您可以访问通过此布局shape的WorkContext对象。
Views/Layout.cshtml
1: @{
2: // 创建一个新的 shape.
3: var mySampleShape = New.SampleShape();
4: // 把这个 shape加入到 AsideSecond zone
5: // 这是一个Layout shape视图
6: // 因此我们能通过视图的Model属性访问到Layout shape
7: // 而不是通过WorkContext.Layout访问Layout shape
8: // 因为在 Layout.cshtml 视图中 Model=WorkContext.Layout
9: Model.AsideSecond.Add(mySampleShape);
10: }
11:
12: @*在视图的某个地方, 呈现到AsideSecond zone*@
13: @Display(Model.AsideSecond)
上面的代码将创建一个示例形状(确保你创建一个相应的视图文件时这一点),将其添加到一个区域称为AsideSecond,然后显示zone。结果将是,我们的示例形状会呈现在 AsideSecond zone。
七、ZoneHolding
因此这里有一个问题:我们可以取能想出任意区域的名字,还是我们必须用某种方式来定义他们?
实际上,布局shape是一种特殊类型的shape ,称为ZoneHolding shape 。ZoneHolding类来源于shape类,将创建一个zone shape动态布局的shape作为访问属性。
谢天谢地ZoneHolding shape类型,将为我们自动完成这一切。
但是等等——那么为什么我们必须名称在theme(主题)清单列表(theme.txt)文件创建一个zone ?
非常好的问题。之所以这样,原因是我们在主题列表清单指定的一个区域名称,实际上并不意味着我们有将它呈现在任意区域的能力,相反,这个列表是用于渲染Widgets的可用区部分。例如,让我们看看TheThemeMachine。
下面的区域名称定义的 Theme.txt 文件:
Header, Navigation, Featured, BeforeMain, AsideFirst, Messages, BeforeContent, Content, AfterContent, AsideSecond, AfterMain, TripelFirst, TripelSecond, TripelThird, FooterQuadFirst, FooterQuadSecond, FooterQuadThird, FooterQuadFourth, Footer
这些区域名称会导致他们出现在Widgets 配置页上:
当您创建一个Widgets并将它添加到一个区域,呈现Widgets的代码实际上是使用存储区域名称来添加Widgets形状区域。非常好。
八、呈现的Content(Rendering Content)
现在我们已经看到什么是shape,,以及我们如何使用和渲染,让我们真正深入研究如何呈现一个Content项。为了更好的理解如何呈现一个Content项,我们必须深入了解、drivers和content handlers是什么,Placement.info发挥什么作用。
九、深入了解的Content Item(Anatomy of a Content Item)
一个Content Item基本上由以下组成:
1、它有一个Content类型:Content类型本质上是一个Content项的蓝图( blueprint)。例如:Page content 类型,它有一个专业名称叫做“Page”和显示名称(也称为友好名称)称为“Page”。
2、它有一个Content Parts的集合
如:
1)、TitlePart
2)、BodyPart
这些Content Parts定义Content Item的Content类型的数据和行为
因此,一个Page content项是一个拥有“Page”的content项,并拷贝了content类型的Content Parts。Content Parts的一个重要特点是,任何给定的Content类型只能有一个实例相同的Content Parts类型。例如,没有Content项可以有多个TitlePart。但是,Content Item可以既有TitlePar和BodyPart。
Parts自己可以拥有属性。例如,TitlePart有一个标题属性的类型字符串,而BodyPart文本属性的类型字符串。
除此之外,所有Parts集合属性称为Fields,这是一个Content Fields集合。与Content Item只能有一个某种类型的一部分相比,Content Parts可以有许多相同类型的fields的Content。几个content fields的例子:
Text Field
Boolean Field
Media Library Picker Field
Taxonomy Field
Date Time Field
Content Picker Field
Enumeration Field
用户可以使用控制面版(Orchard管理后端)给Content类型添加Content Parts和给Content Parts添加content fields。将属性添加到Content部分,开发人员必须创建一个与content part专业名称一至的c#类。
下面是一个简单的树形视图,它显示Content类型的层次结构,它的parts,每个parts的fields, 以及Content项的结构,它本质上是一样的Content类型:
正如你所看到的,一个Content类型可以有多个部分,每个部分可以有多个字段。
现在让我们使用代码创建一个新Page Content item,并使用view来按照我们的想法渲染它。我们需要有一个controller 和一个action ,看起来像下面一样:
Controller:
1: public class ItemController: Controller {
2: private IContentManager _contentManager;
3:
4: public ItemController(IContentManager contentManager) {
5: _contentManager = contentManager;
6: }
7:
8: public ActionResult Display(int id) {
9: var contentItem = _contentManager.Create("Page", VersionOptions.Published);
10: var titlePart = contentItem.As<TitlePart>();
11: var bodyPart = contentItem.As<BodyPart>();
12:
13: titlePart.Title = "Hello Orchard";
14: bodyPart.Text = "<p>Welcome to my new page!</p>";
15:
16: return View(contentItem);
17: }
18: }
View:
1: @{
2: var contentItem = (ContentItem)Model; // 通过model来访问content item
3:
4: // 得到一个引用TitlePart和BodyPart,这样我们就可以正确渲染它们。
5: var titlePart = (TitlePart)contentItem.As<TitlePart>();
6: var bodyPart = contentItem.As<BodyPart>();
7: }
8:
9: <h1>@titlePart.Title</h1>
10: @Html.Raw(bodyPart.Text)
第一个代码片段中我们所看到,创建一个新的Content item的Content类型“Page”和访问其TitlePart,BodyPart与一些初始化它们的值,最后返回一个Content item的 Model的view。
第二个代码片段显示了我们如何通过他们的属性值来访问两个part的Content项。
非常简单。然而,有一个警告:像这样的代码需要硬编码Content项。然而Orchard的真正力量是,您可以将可重用Content Part和Fields附加到任何Content类型,不必为这些Content类型创建特定的视图。如何做?答案是:让每个Content Part和Content Fields来决定自己如何渲染。
十、Content Part and Field Drivers
很早之前Orchard人就想出了一个API,可以作为输入Content项,并返回一个shape作为输出。shape可以渲染。这个API IContentManager被实现为一个方法称为BuildDisplay,及其签名如下:
1: /// <summary>
2: /// Builds the display shape of the specified content item
3: /// </summary>
4: /// <param name="content">The content item to use</param>
5: /// <param name="displayType">The display type (e.g. Summary, Detail) to use</param>
6: /// <param name="groupId">Id of the display group (stored in the content item‘s metadata)</param>
7: /// <returns>The display shape</returns>
8: dynamic BuildDisplay(IContent content, string displayType = "", string groupId = "");
默认实现IContentManager(DefaultContentManager),本身并不做太多;相反,它代表shape的构建IContentDisplay的实例
在一个高级别,这是IContentDisplay的默认实现:
1、创建一个和Stereotype的shape一样的新的Content类型。默认情况下,这是一个“Content”,一个新的“Content”的shape 类型的将被创建。这个Shape的基类是ZoneHolding,因此能够自动创建zone shapes和Layout shape。
2、Shape的DisplayType参数设置为任何显示类型。这显示类型用于提供一个基于此显示类型的替代。如果没有显示指定,则使用“Detail”。
3、BuildDisplay方法调用的ContentHandler的实现。
4、有两种ContentHandler的实现,用来实现content part 的 driver 系统:一个是content part的ContentPartDriverCoordinator和一个是content fields的ContentFieldDriverCoordinator。这些处理程序的目的是遍历每个part和fields的Content项,找到他们的driver。对于每个找到的driver,BuildDisplayShape方法在被调用时,反过来调用其受保护的Display方法,其实现是由具体的driver来实现的。
5、这个具体的Display实现,最终返回DriverResult,DriverResult通常是ContentShapeResult的一个实例。ContentShapeResult持有一个渲染shape的名字,和所谓的会创建shape渲染shape工厂方法。文档演示了一个Content part driver的示例实现。
6、注意我说driver返回持有渲染shape的名字的content shape的值,以及将创建实际的形状和引用方法。我们不马上返回创建的shape的原因是,如果我们在某种程度上决定不渲染它,我们就不用创建shape。有一个叫做Placement的系统,将确定shape应该渲染在什么地方,或者根本就不渲染。不久我们将讨论这个,但是我想说,这是我们分别提供shape name和shape factory方法原因(因此shape是晚绑定的)。
7、因为每个driver的Display方法被调用时,调用代码将决定shape需要去的地方,如果它需要渲染。会文读取Placement.info的XML文件,它包含shape名称和位置信息。例如,如果一个driver返回一个名为“Parts_Title”的shape,placement.info文件有一个元素如<Place Parts_Title = "Header:1 " / >,将形状在一个zone称为“Header”位置,这个shape将在shale第1步创建它。
我们最终得到的是一个item的shape,俱有层次结构的 part和 field的shapes。Orchard所需要做的就是使这个shape,进而将渲染其子shape。正如你所看到的,这是非常强大的,每个part和field只有关心渲染自己,Placement.info用于外部控制这些shape应该显示在什么地方。这是特别强大的,并且你可以覆盖Placement.info主题,以满足不同的需求。
下面是一个流程图表示的代码执行路径ItemController的Content模块(Orchard.Core发现)的drive,和Controller,View和渲染的shape:
我不知道你是什么的感觉,但是我感到令人激动人心的,当我终于能够用我的head通过drivers,在请求Content项的管道(当要求使用它的路由,因为渲染挂件widgets从一个ActionFilter而不是一个控制器。但它是所有相同的流程),以及理解shape真的可以来自任何地方:从控制器controllers, drivers, action filters,甚至从视图上创建(也称为特殊shape)。
详细分析Orchard的Content、Drivers, Shapes and Placement 类型