A shape is a dynamic data model. The purpose of a shape is to replace the static view model of ASP.NET MVC by using a model that can be updated at runtime -- that is, by using a dynamic shape. You can think of shapes as the blobs of data that get handed to templates for rendering.
This article introduces the concept of shapes and explains how to work with them. It's intended for module and theme developers who have at least a basic understanding of Orchard modules. For information about creating modules, see the Getting Started with Modules course. For information about dynamic objects, see Creating and Using Dynamic Objects.
Introducing Shapes
Shapes are dynamic data models that use shape templates to make the data visible to the user in the way you want. Shape templates are fragments of markup for rendering shapes. Examples of shapes include menus, menu items, content items, documents, and messages.
A shape is a data model object that derives from the Orchard.DisplayManagement.Shapes.Shape
class.
The Shape
class is never instantiated. Instead, shapes are created at run time by a shape factory.
The default shape factory is Orchard.DisplayManagement.Implementation.DefaultShapeFactory
.
The shapes created by the shape factory are dynamic objects.
Note
Dynamic objects are new to the .NET Framework 4. As a dynamic object, a shape exposes its members at run time instead of at compile time. By contrast, an ASP.NET MVC model object is a static object that's defined at compile time.
Information about the shape is contained in the ShapeMetadata
property of the shape itself.
This information includes the shape's type, display type, position, prefix, wrappers, alternates,
child content, and a WasExecuted
Boolean value.
You can access the shape's metadata as shown in the following example:
var shapeType = shapeName.Metadata.Type;
After the shape object is created, the shape is rendered with the help of a shape template.
A shape template is a piece of HTML markup (partial view) that is responsible for displaying the shape.
Alternatively, you can use a shape attribute (Orchard.DisplayManagement.ShapeAttribute
)
that enables you to write code that creates and displays the shape without using a template.
Creating Shapes
For module developers, the most common need for shapes is to transport data from a driver to a template for rendering.
A driver derives from the Orchard.ContentManagement.Drivers.ContentPartDriver
class
and typically overrides that class's Display
and Editor
methods.
The Display
and Editor
methods return a ContentShapeResult
object, which is analogous to
the ActionResult
object returned by action methods in ASP.NET MVC.
The ContentShape
method helps you create the shape and return it in a ContentShapeResult
object.
Although the ContentShape
method is overloaded, the most typical use is to pass it two
parameters -- the shape type and a dynamic function expression that defines the shape.
The shape type names the shape and binds the shape to the template that will be used to render it.
The naming conventions for shape types are discussed later in
Naming Shapes and Templates.
The function expression can be described best by using an example.
The following example shows a driver's Display
method that returns a shape result,
which will be used to display a Map
part.
protected override DriverResult Display(
MapPart part, string displayType, dynamic shapeHelper)
{
return ContentShape("Parts_Map",
() => shapeHelper.Parts_Map(
Longitude: part.Longitude,
Latitude: part.Latitude));
}
The expression uses a dynamic object (shapeHelper
) to define a Parts_Map
shape and its attributes.
The expression adds a Longitude
property to the shape and sets it equal to the part's Longitude
property.
The expression also adds a Latitude
property to the shape and sets it equal to the part's Latitude
property.
The ContentShape
method creates the results object that is returned by the Display
method.
The following example shows the entire driver class that sends a shape result to a template either
to be displayed or edited in a Map
part. The Display
method is used to display the map.
The Editor
method marked "GET" is used to display the shape result in editing view for user input.
The Editor
method marked "POST" is used to redisplay the editor view using the values provided by the user.
These methods use different overloads of the Editor
method.
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);
}
}
}
The Editor
method marked "GET" uses the ContentShape
method to create a shape for an editor template.
In this case, the type name is Parts_Map_Edit
and the shapeHelper
object creates an EditorTemplate
shape.
This is a special shape that has a TemplateName
property and a Model
property.
The TemplateName
property takes a partial path to the template.
In this case, "Parts/Map"
causes Orchard to look for a template in your module at the following path:
Views/EditorTemplates/Parts/Map.cshtml
The Model
property takes the name of the part's model file, but without the file-name extension.
Naming Shapes and Templates
As noted, the name given to a shape type binds the shape to the template that will be used to render the shape.
For example, suppose you create a part named Map
that displays a map for the specified longitude and latitude.
The name of the shape type might be Parts_Map
. By convention, all part shapes begin with Parts_
followed by the name of the part (in this case Map
). Given this name (Parts_Map
), Orchard looks for a template in your module at the following path:
views/parts/Map.cshtml
The following table summarizes the conventions that are used to name shape types and templates.
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 |
You should put your templates in the project according to the following rules:
- Content item shape templates are in the
Views/Items
folder. Parts_
shape templates are in theViews/Parts
folder.Fields_
shape templates are in theViews/Fields
folder.- The
EditorTemplate
shape templates are in theViews/EditorTemplates/<templatename>
folder.
For example, anEditorTemplate
with a template name ofParts/Routable.RoutePart
has its template atViews/EditorTemplates/Parts/Routable.RoutePart.cshtml
. - All other shape templates are in the
Views
folder.
Note
The template extension can be any extension supported by an active view engine, such as .cshtml
, .vbhtml
, or .ascx
.
From Template File Name to Shape Name
More generally, the rules to map from a template file name to the corresponding shape name are the following:
- Dot (
.
) and backslash (\
) change to underscore (_
). Note that this does not mean that anexample.cshtml
file in amyviews
subdirectory ofViews
is equivalent to amyviews
example.chtmlfile in
Views_. The shape templates must still be in the expected directory (see above). - Hyphen (
-
) changes to a double underscore (__
).
For example, Views/Hello.World.cshtml
will be used to render a shape named Hello_World
,
and Views/Hello.World-85.cshtml
will be used to render a shape named Hello_World__85
.
Alternate Shape Rendering
As noted, an HTML widget in the AsideSecond
zone (for example) could be rendered
by a widget.cshtml
template, by a widget-htmlwidget.cshtml
template,
or by a widget-asidesecond.cshtml
if they exist in the current theme.
When various possibilities exist to render the same content,
these are referred to as alternates of the shape,
and they enable rich template overriding scenarios.
Alternates form a group that corresponds to the same shape if they differ only by a double-underscore suffix.
For example, Hello_World
, Hello_World__85
, and Hello_World__DarkBlue
are an alternate group
for a Hello_World
shape. Hello_World_Summary
, conversely, does not belong to that group
and would correspond to a Hello_World_Shape
shape, not to a Hello_World
shape.
(Notice the difference between "__
" and "_
".)
Which Alternate Will Be Rendered?
Even if it has alternates, a shape is always created with the base name, such as Hello_World
.
Alternates give additional template name options to the theme developer beyond the default
(such as hello.world.cshtml
).
The system will choose the most specialized template available among the alternates,
so hello.world-orange.cshtml
will be preferred to hello.world.cshtml
if it exists.
Built-In Content Item Alternates
The table above shows possible template names for content items.
It should now be clear that the shape name is built from Content
and the display type (for example Content_Summary
).
The system also automatically adds the content type and the content ID as alternates
(for example Content_Summary__Page
and Content_Summary__42
).
For more information about how to use alternates, see Alternates.
Rendering Shapes Using Templates
A shape template is a fragment of markup that is used to render the shape. The default view engine in Orchard is the Razor view engine. Therefore, shape templates use Razor syntax by default. For an introduction to Razor syntax, see Template File Syntax Guide.
The following example shows a template for displaying a Map
part as an image.
<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" />
This example shows an img
element in which the src
attribute contains a URL
and a set of parameters passed as query-string values.
In this query string, @Model
represents the shape that was passed into the template.
Therefore, @Model.Latitude
is the Latitude
property of the shape,
and @Model.Longitude
is the Longitude
property of the shape.
The following example shows the template for the editor. This template enables the user to enter values for the latitude and 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>
The @Html.LabelFor
expressions create labels using the name of the shape properties.
The @Html.TextBoxFor
expressions create text boxes where users enter values for the shape properties.
The @Html.ValidationMessageFor
expressions create messages that are displayed if users enter an invalid value.
Wrappers
Wrappers let you customize the rendering of a shape by adding markup around the shape.
For example, Document.cshtml
is a wrapper for the Layout
shape, because it specifies
the markup code that surrounds the Layout
shape.
For more information about the relationship between Document
and Layout
,
see Template File Syntax Guide.
Typically, you add a wrapper file to the Views
folder of your theme.
For example, to add a wrapper for Widget
, you add a Widget.Wrapper.cshtml
file to
the Views
folder of your theme.
If you enable the Shape Tracing feature, you'll see the available wrapper names for a shape.
You can also specify a wrapper in the Placement.info
file.
For more information about how to specify a wrapper,
see Understanding the Placement.info File.
Creating a Shape Method
Another way to create and render a shape is to create a method that both defines and renders the shape.
The method must be marked with the Shape
attribute (the Orchard.DisplayManagement.ShapeAttribute
class).
The method returns an IHtmlString
object instead of using a template;
the returned object contains the markup that renders the shape.
The following example shows the DateTimeRelative
shape.
This shape takes a DateTime
value in the past and returns a string that relates the value to the current time.
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");
}
}