首先Orchard是一个建立在ASP.NET MVC框架上的CMS应用框架。Orchard在呈现内容的时候也遵循MVC的规律,也是通过Controller来处理Url请求并决定用那个View来呈现那种Model。不过这个Model就比较有讲究了,因为在Orchard中,一个页面上呈现的数据可能是多种多样的,有文章、有评论,有博客等等。而且这些数据都是可以通过后台设置任意组合的,也就是说我们不可能为每一个页面都创建一个类型的Model。那么在Orchard中是如何解决这一问题的呢?Orchard引入了形状(Shape)的概念,一个形状是一个动态类型的数据。引入形状的目的是为了取代原有静态的ASP.NET MVC视图的数据模型(Model),让数据模型的数据类型可以在运行时更改。这样就可以很好的解决Orchard需要处理多种不可预知的数据类型问题。
本文介绍了形状的概念,并解释它们是如何工作的。本文适合Orchard模块和主题开发者阅读,所以在学习本文前你需要先了解一些基本的模块和主题开发的知识。关于模块和主题的相关知识,你可以在《Orchard学习计划》中查看相应文章,当然查看官网文档也是不错的选择。同时形状是一个动态对象,所以你也有必要先了解一下动态对象的相关信息,关于动态对象请查看《Creating and Using Dynamic Objects》。
形状介绍(Introducing Shapes)
形状是一个动态对象,利用形状模板,可以使数据以你想要的方式呈现给用户。形状模板是一段呈现形状的标记。形状的例子有:菜单、菜单项、内容项、文档和消息等。
一个形状是一个数据模型对象,它继承于Orchard.DisplayManagement.Shapes.Shape类。形状类是没有实例化的。相反,形状是在运行时由一个形状工厂创建的。默认的形状工厂是Orchard.DisplayManagement.Implementation.DefaultShapeFactory。形状是形状工厂创建的动态对象。
动态对象是.NET Framework 4的一个新特性。作为一个动态对象,形状在运行时公开其成员,而不是在编译时。与此相反,一个ASP.NET MVC的模型对象是在编译时就被定义为一个静态的对象。
关于形状的信息是包含在自身的ShapeMetadata 属性里面。这些信息包括形状的类型,显示类型,位置,前缀,包装,替换名称,子内容和一个是否已执行的标记。你可以通过以下方法来访问形状的元数据:
var shapeType = shapeName.Metadata.Type;
在形状对象创建以后,形状里面的数据就可通过形状模板中的Help方法来呈现出来。一个形状模板是一个段Html标记(部分视图,partial view)是负责显示形状的。此外,你也可以通过代码来呈现形状。如:定义一个方法并加上Shape属性即可,你可以在CoreShapes.cs中找到这种写法。
创建形状(Creating Shapes)
对于模块开发者,最常见的形状是通过驱动器将数据通过一个模板呈现出来。一个驱动器是继承Orchard.ContentManagement.Drivers.ContentPartDriver类的,并重写基类的Display和Editor方法。Display和Editor方法返回一个ContentShapeResult对象,类似于ASP.NET中Action所返回的ActionResult。ContentShape方法可以帮助你创建形状并把它放到一个ContentShapeResult对象中返回。
虽然ContentShape方法有很多重载,但通常只使用它有两个参数的那个重载。这两个参数分别是形状类型(shape type)和一个用于定义形状的动态对象的表达式。形状的类型名称将用于构建一个形状并结合该形状的模板一起用于呈现。形状类型的命名规则,我们将在稍后的“命名形状和模板”一节中讨论。
表达式能够很好的通过一个例子来描述。以下示例显示了一个驱动器的Display方法,它返回了一个形状的结果,是用于显示一个地图部件的。
protected override DriverResult Display( MapPart part, string displayType, dynamic shapeHelper) { return ContentShape("Parts_Map", () => shapeHelper.Parts_Map( Longitude: part.Longitude, Latitude: part.Latitude)); }
表达式用于一个动态对象(shapeHelper)去定义一个Parts_Map 形状和它的属性。这个表达式添加了一个Longitude属性可以设置地图的经度,添加了一个Latitude属性用于显示地图的维度。ContentShape方法创建了Display方法所需要返回的结果对象。
在接下来的例子中显示了一个完整的地图部件驱动器所要返回的形状。Display方法用于显示地图,标记了Get的Editor方法用于显示编辑地图数据的画面,标记了Post的Editor方法用于处理编辑画面所提交的数据。这两个Editor方法分别使用了不同的Editor方法重载。
using Maps.Models; using Orchard.ContentManagement; using Orchard.ContentManagement.Drivers; namespace Maps.Drivers { public class MapPartDriver : ContentPartDriver<MapPart> { protected override DriverResult Display( MapPart part, string displayType, dynamic shapeHelper) { return ContentShape("Parts_Map", () => shapeHelper.Parts_Map( Longitude: part.Longitude, Latitude: part.Latitude)); } //GET protected override DriverResult Editor( MapPart part, dynamic shapeHelper) { return ContentShape("Parts_Map_Edit", () => shapeHelper.EditorTemplate( TemplateName: "Parts/Map", Model: part)); } //POST protected override DriverResult Editor( MapPart part, IUpdateModel updater, dynamic shapeHelper) { updater.TryUpdateModel(part, Prefix, null, null); return Editor(part, shapeHelper); } } }
标记了Get的Editor方法使用ContentShape去创建一个形状用于编辑模板。在这个案例中,形状类型名称是Parts_Map_Edit并且shapeHelper对象创建了一个EditorTemplate的形状。这是一个特殊的形状,它有一个模板名称属性和一个Model属性。模板名称属性指定了一个模板的部分路径,在这个案例中,“Parts/Map”将使Orchard在Views/EditorTemplates/Parts/Map.cshtml路径中查找相应的编辑模板。Model属性设置了这个模板所用到的数据。
命名形状和模板(Naming Shapes and Templates)
如前所述,一个形状的类型名称将用于构建一个形状并结合该形状的模板一起用于呈现。例如:假设你创建了一个名为Map的部件用于显示地图的经纬度。形状类型的名称就肯能是Parts_Map。根据命名规则,所有的部件形状需要以Parts_开头,然后就是这个部件的名称(这此例中是Map)。鉴于此名称(Parts_Map),Orchard将在views/parts/Map.cshtml路径中查找相应的模板。
下表总结了形状类型和模板的命名规则:
Applied To | Shape Naming Convention | Shape Type Example | Template Example |
---|---|---|---|
Content shapes | Content__[ContentType] | Content__BlogPost | Content-BlogPost |
Content shapes | Content__[Id] | Content__42 | Content-42 |
Content shapes | Content__[DisplayType] | Content__Summary | Content.Summary |
Content shapes | Content_[DisplayType]__[ContentType] | Content_Summary__BlogPost | Content-BlogPost.Summary |
Content shapes | Content_[DisplayType]__[Id] | Content_Summary__42 | Content-42.Summary |
Content.Edit shapes | Content_Edit__[DisplayType] | Content_Edit__Page | Content-Page.Edit |
Content Part templates | [ShapeType]__[Id] | Parts_Common_Metadata__42 | Parts/Common.Metadata-42 |
Content Part templates | [ShapeType]__[ContentType] | Parts_Common_Metadata__BlogPost | Parts/Common.Metadata-BlogPost |
Field templates | [ShapeType]__[FieldName] | Fields_Common_Text__Teaser | Fields/Common.Text-Teaser |
Field templates | [ShapeType]__[PartName] | Fields_Common_Text__TeaserPart | Fileds/Common.Text-TeaserPart |
Field templates | [ShapeType]__[ContentType]__[PartName] | Fields_Common_Text__Blog__TeaserPart | Fields/Common.Text-Blog-TeaserPart |
Field templates | [ShapeType]__[PartName]__[FieldName] | Fields_Common_Text__TeaserPart__Teaser | Fields/Common.Text-TeaserPart-Teaser |
Field templates | [ShapeType]__[ContentType]__[FieldName] | Fields_Common_Text__Blog__Teaser | Fields/Common.Text-Blog-Teaser |
Field templates | [ShapeType]__[ContentType]__[PartName]__[FieldName] | Fields_Common_Text__Blog__TeaserPart__Teaser | Fields/Common.Text-Blog-TeaserPart-Teaser |
LocalMenu | LocalMenu__[MenuName] | LocalMenu__main | LocalMenu-main |
LocalMenuItem | LocalMenuItem__[MenuName] | LocalMenuItem__main | LocalMenuItem-main |
Menu | Menu__[MenuName] | Menu__main | Menu-main |
MenuItem | MenuItem__[MenuName] | MenuItem__main | MenuItem-main |
Resource | Resource__[FileName] | Resource__flower.gif | Resource-flower.gif |
Style | Style__[FileName] | Style__site.css | Style-site.css |
Widget | Widget__[ContentType] | Widget__HtmlWidget | Widget-HtmlWidget |
Widget | Widget__[ZoneName] | Widget__AsideSecond | Widget-AsideSecond |
Zone | Zone__[ZoneName] | Zone__AsideSecond | Zone-AsideSecond |
你应该把你的模板按照以下规则放在项目中:
- 内容项形状模板在 views/items目录下。
- 部件形状模板在views/parts目录下。
- 字段形状模板在views/fields目录下。
- 编辑模板在相应的 views/EditorTemplates/{相应模板名称} 目录下。例如:部件的编辑模板需要在 views/EditorTemplates/Parts目录下。
- 其他的形状模板都直接在views 目录下。
注:模板的扩展名可以是任何所支持的视图引擎文件扩展名,如:cshtml,vbhtml或ascx等。
从模板文件名称到形状名称(From Template File Name to Shape Name)
更普遍的是,冲一个模板文件的名称到相应形状名称的映射规则如下:
- 将点(.)和反斜杠(\)改为下划线(_)。注意这点不包含后缀名中的点,如.cshtml。形状的模板文件需要在约定的目录中(见上文)。
- 短划线(-)改为双下划线(__)。
例如:Views/Hello.World.cshtml将用于呈现名为Hello_World的形状;Views/Hello.World-85.cshtml将用于呈现名为Hello_World__85的形状。
形状的可替换性呈现(Alternate Shape Rendering)
如前所述,一个Html widget在AsideSecond 区域显示的时候可以通过一个widget.cshtml模板,或一个widget-htmlwidget.cshtml模板,或一个widget-asidesecond.cshtml模板,只要它们在当前主题中。当存在各种可能呈现同一内容的模板,这些就可称作为形状的可替换性,这些可替换的模板能让显示更加丰富多彩。
一组替换可对应于相的同形状,他们只是通过加一个有双下划线的后缀。如:Hello_World,Hello_World__85和Hello_World__DarkBlue就是一组可替换Hello_World形状的形状。而Hello_World_Summary就不属于这组可替换Hello_World的形状。这就是下划线(_)和双下划线(__)的不同。
那一个可用替换将被呈现(Which Alternate Will Be Rendered?)
如有一个形状有多个可用的替换,那么那个将被选择为最终呈现的。例如:Hello_World,在主题中给出的附加可选模板超出了默认的模板(例如hello.world.cshtml)。系统将会选择一个最特别的模板做为该形状的可替换模板。所以,如果hello.world-orange.cshtml模板存在他将比 hello.world.cshtml 模板优先采用。
内置内容项的可替换模板
通过上表可知模板还能为某一内容项单独定义。如一个显示内容摘要形状的模板(Content_Summary),系统可使用内容类型和内容Id作为替换,比如:Content_Summary__Page 和 Content_Summary__42。更多关于替换规则的信息,可以查看《Alternates》一文。
使用模板呈现形状(Built-In Content Item Alternates)
一个形状模板是一段用于呈现形状的标记语言片段。在Orchard中默认使用Razor视图引擎。因此,形状的模板默认使用Razor语法,关于Razor语法的介绍可以查看《Template File Syntax Guide》。
下面是一段显示地图部件的模板:
<img alt="Location" border="1" src="http://maps.google.com/maps/api/staticmap? &zoom=14 &size=256x256 &maptype=satellite&markers=color:blue|@Model.Latitude,@Model.Longitude &sensor=false" />
这个例子显示了一个img标签,这个图片用于呈现相应经纬度的地图。在相应的图片地址中,@Model代表形状传递给模板的数据。因此,@Model.Latitude和@Model.Longitude就分别是这个形状的两个属性,存储了经纬值。
接下来的一段代码,是地图部件用于编辑画面的模板,这个模板允许用户输入地图的经纬值。
@model Maps.Models.MapPart <fieldset> <legend>Map Fields</legend> <div class="editor-label"> @Html.LabelFor(model => model.Longitude) </div> <div class="editor-field"> @Html.TextBoxFor(model => model.Latitude) @Html.ValidationMessageFor(model => model.Latitude) </div> <div class="editor-label"> @Html.LabelFor(model => model.Longitude) </div> <div class="editor-field"> @Html.TextBoxFor(model => model.Longitude) @Html.ValidationMessageFor(model => model.Longitude) </div> </fieldset>
@Html.LabelFor表达式用于创建一个lable标签来显示形状属性的名称。@Html.TextBoxFor表达式用于创建一个input标签,让用户可以输入相应的值。@Html.ValidationMessageFor表达式则是用于显示验证相应属性输入出错信息的。
更多关于模板和Razor语法的信息,可查看《Template Files and their Locations》。
包装(Wrappers)
包装可以让你在形状周围添加一些标记来自定义形状呈现。比如:Document.cshtml是一个Layout 形状的包装,因为它在Layout形状外围有一些特殊的标记。更多关于Document.cshtml和Layout的关系可以查看《Template File Syntax Guide》。
通常,你添加一个包装文件到你主题的Views目录下。例如:为Widget添加一个包装,就可以添加一个Widget.Wrapper.cshtml文件到你主题的Views目录下。如果你使用Shape Tracing工具,你就可以看见一个形状都有哪些可用的包装名称了。你也可以在placement.info文件中为形状指定一个包装。有关更多如何指定包装的知识,可以查看《理解Orchard中的placement.info文件》。
创建一个形状方法(Creating a Shape Method)
另外一个呈现形状的方法就是通过为形状添加一个方法来定义并呈现形状。这个方法必须有一个Shape属性(Orchard.DisplayManagement.ShapeAttribute类)。这个方法还必须返回一个IHtmlString对象,而不是用模板来展现。这个返回的对象包含了要呈现形状的Html代码。
下面的代码显示了DateTimeRelative 形状。这个形状可以让一个Datetime值显示为一个已过多少时间的字符串,如:1分钟前,2小时前之类的。
public class DateTimeShapes : IDependency { private readonly IClock _clock; public DateTimeShapes(IClock clock) { _clock = clock; T = NullLocalizer.Instance; } public Localizer T { get; set; } [Shape] public IHtmlString DateTimeRelative(HtmlHelper Html, DateTime dateTimeUtc) { var time = _clock.UtcNow - dateTimeUtc; if (time.TotalDays > 7) return Html.DateTime(dateTimeUtc, T("‘on‘ MMM d yyyy ‘at‘ h:mm tt")); if (time.TotalHours > 24) return T.Plural("1 day ago", "{0} days ago", time.Days); if (time.TotalMinutes > 60) return T.Plural("1 hour ago", "{0} hours ago", time.Hours); if (time.TotalSeconds > 60) return T.Plural("1 minute ago", "{0} minutes ago", time.Minutes); if (time.TotalSeconds > 10) return T.Plural("1 second ago", "{0} seconds ago", time.Seconds); return T("a moment ago"); } }