Shapes¶
Orchard Core builds its user interface from shapes. A shape is an object that
contains the data to display and metadata that describes how to display it. Shapes
are assembled into a tree, then each shape is rendered independently into
IHtmlContent.
This indirection lets a module provide a default rendering while an application or theme changes a specific content type, display type, field, or individual shape without changing the module.
Create and render a first shape¶
In a Razor shape template, New creates shapes and DisplayAsync renders them:
@{
dynamic car = await New.Car(Brand: "Toyota", Color: "Blue");
}
@await DisplayAsync(car)
New.Car(...) creates a shape whose type is Car and whose properties include
Brand and Color. Orchard Core looks for a binding for that shape type. The
simplest binding is a Razor file named Views/Car.cshtml:
<div>A @Model.Color @Model.Brand car.</div>
The same operation in Liquid uses shape_new, shape_add_properties, and
shape_render:
{% assign car = "Car" | shape_new %}
{% shape_add_properties car Brand: "Toyota", Color: "Blue" %}
{{ car | shape_render }}
From a service, inject IShapeFactory and IDisplayHelper:
dynamic car = await _shapeFactory.CreateAsync("Car");
car.Brand = "Toyota";
car.Color = "Blue";
var html = await _displayHelper.ShapeExecuteAsync((IShape)car);
The dynamic New property exposed by IShapeFactory is shorthand for the same
operation. Both await New.Car() and await New.CarAsync() create a shape with
the type Car.
The Display Management
reference also describes the <shape> tag helper and the equivalent Liquid
filters.
Understand the shape contract¶
All shapes implement IShape. The default implementation is Shape, which is a
dynamic object: assigning an unknown member stores it in the Properties
dictionary, and reading that member retrieves the same value.
dynamic car = await _shapeFactory.CreateAsync("Car");
car.Brand = "Toyota";
var shape = (IShape)car;
var brand = shape.Properties["Brand"]; // "Toyota"
The important members of IShape are:
| Member | Purpose |
|---|---|
Metadata |
Rendering, placement, grouping, alternates, wrappers, and cache information. |
Properties |
Data exposed dynamically to templates. |
Id, TagName, Classes, Attributes |
Optional hints for generating an HTML element. |
Items |
Ordered child items contained by the shape. |
AddAsync() |
Adds a child item and its position. |
Shape derives from Composite, which provides its dynamic property behavior.
ShapeViewModel is another IShape implementation, but it is not dynamic.
Shape type and CLR type are independent¶
Metadata.Type is the logical shape type used for binding resolution. It is not
the CLR type of the object. A shape whose logical type is Car can be the default
dynamic Shape, a custom IShape implementation, or a strongly typed model.
For strongly typed templates, CreateAsync<TModel>() creates a proxy that derives
from TModel and implements IShape:
public class Car
{
public string Brand { get; set; }
public string Color { get; set; }
}
var shape = await _shapeFactory.CreateAsync<Car>("Car", car =>
{
car.Brand = "Toyota";
car.Color = "Blue";
});
var car = (Car)shape;
var metadata = shape.Metadata;
The model class must be proxyable: use a non-sealed class with an accessible
parameterless constructor. If the model already implements IShape, Orchard Core
creates it directly instead of generating a proxy.
Display drivers normally create strongly typed shapes with Initialize<TModel>(),
as shown later in this guide.
Metadata¶
Every shape has a ShapeMetadata instance. The most commonly used properties are:
| Property | Purpose |
|---|---|
Type |
Logical shape type used to resolve a binding. |
DisplayType |
Display context, such as Detail, Summary, or Edit. Placement and alternate providers can use it. |
Name |
Name of this shape inside its parent. |
Differentiator |
Distinguishes multiple uses of the same shape type, such as two fields. |
Position |
Ordering position inside the parent shape. |
Prefix |
HTML field prefix used while rendering editors. |
Alternates |
More specific binding names, ordered from lowest to highest priority. |
Wrappers |
Shape types rendered around this shape. |
TabGrouping, CardGrouping, ColumnGrouping |
Grouping information used by content zones. |
ChildContent |
The current rendered content of the shape. |
DisplayType does not by itself change the template name. Display drivers and
shape table providers use it to add display-type alternates and to select
placement rules.
HTML properties¶
The Id, TagName, Classes, and Attributes members let a shape carry HTML
hints without carrying HTML. GetTagBuilder() turns these hints into a
TagBuilder:
var shape = await _shapeFactory.CreateAsync("Car");
shape.Id = "featured-car";
shape.TagName = "article";
shape.Classes.Add("car");
shape.Classes.Add("car--featured");
shape.Attributes["data-brand"] = "Toyota";
The template can use those values consistently:
@using OrchardCore.DisplayManagement
@{
var shape = (IShape)Model;
var tagBuilder = shape.GetTagBuilder("div");
}
@tagBuilder.RenderStartTag()
A @Model.Color @Model.Brand car.
@tagBuilder.RenderEndTag()
GetTagBuilder() defaults to a span when neither TagName nor a default tag
name is supplied.
Follow the rendering pipeline¶
Rendering @await DisplayAsync(shape) invokes IDisplayHelper, which delegates
to IHtmlDisplay. The default pipeline performs these steps:
- Ignore a
nullshape or an empty deferred zone. - Run global, descriptor, and shape-instance
Displayinghandlers. - When
ChildContentwas not already supplied by a cache, run descriptorProcessinghandlers and resolve the first available binding by checking alternates from highest to lowest priority, then the shape type and its fallback types. - Run shape-instance
Processinghandlers and execute the binding. - Render each wrapper around the current
ChildContent. - Run
Displayedhandlers and return the finalIHtmlContent. - Run global finalization handlers even if rendering throws.
If no binding can be resolved, rendering throws an InvalidOperationException
that names the missing shape type and active theme. There is no implicit
catch-all HTML rendering for a typed shape.
Bindings, descriptors, and the shape table¶
A shape binding is the delegate that turns one shape into IHtmlContent. It
can come from:
- A Razor template.
- A Liquid template.
- A method marked with
[Shape]. - A dynamic
IShapeBindingResolver.
A shape descriptor stores configuration for a fundamental shape type: creation and display handlers, bindings, default placement, and wrappers.
The shape table contains the descriptors and bindings available for the
current tenant and active theme. IShapeTableProvider implementations build and
alter that table. The startup application has the highest override priority,
followed by the active theme and its base themes, then modules in dependency
order.
For normal customization, add a template to the active theme rather than working with the shape table directly.
Name templates and shape types correctly¶
Templates can be placed in these directories:
ViewsViews/ItemsViews/PartsViews/FieldsViews/Elements
Templates below Views/Parts, Views/Fields, and Views/Elements automatically
receive the corresponding Parts_, Fields_, or Elements_ prefix.
Template filenames use punctuation that is converted to canonical shape names:
- A hyphen (
-) becomes a double underscore (__). This is a breaking separator that creates fallback levels. - A dot (
.) becomes a single underscore (_). This is a non-breaking separator. - When the final dot appears after the final hyphen, the suffix is interpreted as
a display type and inserted before the first
__.
| Canonical binding name | Razor or Liquid filename |
|---|---|
Car |
Car.cshtml or Car.liquid |
Car_Color__Blue |
Car.Color-Blue.cshtml or Car.Color-Blue.liquid |
Content__BlogPost |
Content-BlogPost.cshtml or Content-BlogPost.liquid |
Content_Summary__BlogPost |
Content-BlogPost.Summary.cshtml or Content-BlogPost.Summary.liquid |
Fields_Common_Text__FirstName |
Views/Fields/Common.Text-FirstName.cshtml or .liquid |
For example, Content-BlogPost.Summary.cshtml is harvested as the binding
Content_Summary__BlogPost, not Content__BlogPost_Summary.
Note
Use canonical names such as Content_Summary__BlogPost in C# APIs,
placement.json, and shape metadata. Use the filename form such as
Content-BlogPost.Summary.cshtml only for template files.
See Templates for the complete list of content, part, field, widget, and editor alternates.
Specialize rendering with alternates¶
An alternate is another fully qualified binding name that may render a shape. Alternates are useful when the shape type is reusable but one context needs a more specific template.
var shape = await _shapeFactory.CreateAsync<Car>("Car", car =>
{
car.Brand = "Toyota";
car.Color = "Blue";
});
shape.Metadata.Alternates.Add("Car_Color__Blue");
shape.Metadata.Alternates.Add("Car_Brand__Toyota");
Alternates added later have higher priority. The resolver therefore checks:
Car_Brand__ToyotaCar_Color__BlueCar
The matching template filenames are:
Car.Brand-Toyota.cshtmlCar.Color-Blue.cshtmlCar.cshtml
Do not add the base type itself as an alternate; the resolver checks it after all alternates.
Shape types containing __ also fall back by removing the last segment. For
example, a type or alternate named Car__Toyota__Hybrid falls back through:
Car__Toyota__HybridCar__ToyotaCar
This fallback is separate from the explicit Alternates collection.
Orchard Core adds many alternates automatically. Content shapes, for example, can receive alternates for their content type, display type, alias, slug, and other context. Prefer those standard alternates to conditionals inside a shared template.
Add alternates consistently with a shape table provider¶
When shapes of the same type are created in multiple places, an
IShapeTableProvider centralizes their conventions:
public sealed class CarShapeTableProvider : IShapeTableProvider
{
public ValueTask DiscoverAsync(ShapeTableBuilder builder)
{
builder.Describe("Car")
.OnDisplaying(context =>
{
if (context.Shape is Car car)
{
context.Shape.Metadata.Alternates.Add($"Car_Color__{car.Color}");
context.Shape.Metadata.Alternates.Add($"Car_Brand__{car.Brand}");
}
});
return ValueTask.CompletedTask;
}
}
Register it with the feature:
services.AddShapeTableProvider<CarShapeTableProvider>();
OnDisplaying runs before binding resolution, so it can derive alternates from
initialized shape data. Values used in binding names should be controlled,
canonical identifiers rather than arbitrary user input.
Wrap a shape¶
Wrappers render around a shape without replacing its selected template:
shape.Metadata.Wrappers.Add("Panel");
Views/Panel.cshtml receives the same shape. Its
Model.Metadata.ChildContent contains the output rendered so far:
<section class="panel">
@Model.Metadata.ChildContent
</section>
Wrappers are processed in insertion order. The first wrapper added is the innermost; each later wrapper surrounds the previous result.
Alternates and wrappers solve different problems:
| Feature | Effect |
|---|---|
| Alternate | Selects a different binding for the shape itself. |
| Wrapper | Renders another binding around the selected shape output. |
Both can be added from code, a metadata tag helper, or placement.json.
Compose shapes with items¶
A shape can contain ordered child items. AddAsync() accepts shapes,
IHtmlContent, strings, and other objects that can be wrapped as positioned
items:
var garage = await _shapeFactory.CreateAsync("Garage");
var firstCar = await _shapeFactory.CreateAsync("Car");
var secondCar = await _shapeFactory.CreateAsync("Car");
await garage.AddAsync(secondCar, "2");
await garage.AddAsync(firstCar, "1");
The Items collection is sorted lazily when it is read. Positions use numeric,
hierarchical ordering rather than string ordering: 1, 1.1, 2, and 10
appear in that order. before and after place items at the beginning and end,
and an empty position is treated as 0.
A container binding normally renders each item:
@foreach (var item in Model.Items)
{
@await DisplayAsync(item)
}
The built-in Zone binding follows this pattern.
Understand layouts and zones¶
Many important shapes are IZoneHolding shapes. A zone-holding shape exposes
named child containers through its Zones property:
Layout
|-- Header
| |-- Branding
| `-- Menu
|-- Content
| `-- Content
| |-- TitlePart
| `-- HtmlBodyPart
`-- Footer
`-- Copyright
ILayoutAccessor.GetLayoutAsync() gets or creates the request's Layout shape.
Layout zones are regular shapes with the type Zone.
dynamic layout = await _layoutAccessor.GetLayoutAsync();
var menu = await _shapeFactory.CreateAsync("Menu");
await layout.Header.AddAsync(menu, "5");
Zones are created on demand. Accessing a missing zone returns a null-like
ZoneOnDemand; adding the first non-null item creates the real zone and stores it
on the parent.
@if (Model.Zones["Header"] != null)
{
@await DisplayAsync(Model.Zones["Header"])
}
Zones.IsNotEmpty("Header") and the IShape.IsNullOrEmpty() extension method are
strongly typed alternatives for checking whether a zone has materialized.
The content shapes built by display managers also hold zones, but those child
zones use the type ContentZone. A ContentZone renders items directly unless
one or more children have tab, card, or column grouping metadata. When grouping
is present, it builds the corresponding grouping container shapes in this order:
tabs, then cards, then columns.
The active theme defines which layout zones are meaningful. Placement determines which generated shapes are added to those zones.
Produce shapes with display drivers¶
Most content, content part, and content field shapes come from display drivers.
A driver returns an IDisplayResult; ShapeResult is the result that lazily
creates, configures, and places one shape.
public sealed class ProductPartDisplayDriver
: ContentPartDisplayDriver<ProductPart>
{
public override IDisplayResult Display(
ProductPart part,
BuildPartDisplayContext context)
{
return Initialize<ProductViewModel>("ProductPart", model =>
{
model.Title = part.Title;
model.Price = part.Price;
})
.Location("Detail", "Content:5")
.Location("Summary", "Content:1");
}
}
Initialize<TModel>() creates a strongly typed shape. The string
ProductPart is its logical shape type; ProductViewModel is its CLR base type.
The locations are driver defaults for the Detail and Summary display types.
Drivers can use:
| Method | Purpose |
|---|---|
Initialize<TModel>() |
Create and initialize a strongly typed shape. |
Dynamic() |
Create and initialize a dynamic Shape. |
Copy() |
Copy public properties from an object to a dynamic shape. |
View() |
Wrap an existing model in ShapeViewModel<TModel>. |
Shape() |
Return an existing IShape. |
Factory() |
Create a shape lazily with a custom factory. |
Combine() |
Return multiple display results. |
Before creating the shape, ShapeResult resolves placement. A hidden or
non-matching result is therefore not created. When it is created, ShapeResult
sets its name, differentiator, display type, prefix, grouping, alternates,
wrappers, and position, then adds it to the selected zone.
Differentiators¶
A differentiator identifies one use of a reusable shape type. For example, all
text fields may use the TextField shape type, while differentiators such as
Article-Subtitle and Article-Description let placement target each field
independently.
Content part and field driver base classes set their standard differentiators.
Custom display results can call .Differentiator("value").
Control location with placement¶
Driver locations are defaults. A module, theme, or startup application can
override them with placement.json:
{
"ProductPart": [
{
"place": "Content:3",
"displayType": "Detail",
"alternates": [ "ProductPart_Featured" ],
"wrappers": [ "Panel" ]
},
{
"place": "-",
"displayType": "Summary"
}
]
}
The first rule moves the detail shape, adds an alternate, and wraps its output. The second rule hides the summary shape. Rules can also filter by differentiator, content type, content part, and request path.
Placement can substitute the logical shape type:
{
"ProductPart": [
{
"place": "Content:3",
"shape": "ProductCard"
}
]
}
This changes Metadata.Type to ProductCard. It clears the shape's existing
alternates and wrappers before applying any alternates and wrappers from the
matching placement rule. The object itself is not converted to another CLR type.
See Placement for its complete syntax, precedence, position rules, grouping, and differentiator patterns.
Morph a shape while rendering¶
Razor's DisplayAsAsync() renders an existing shape as another logical type:
@await DisplayAsAsync(Model, "CompactCar")
It clears the shape's alternates by default, assigns the new
Metadata.Type, clears ChildContent, and renders the same shape instance again.
Pass clearAlternates: false to preserve its alternates:
@await DisplayAsAsync(Model, "CompactCar", clearAlternates: false)
Because this operation changes the shape instance, later renders use the new type. Use it when the new binding expects the same data contract. Creating a new shape is clearer when the child has different data or an independent lifecycle.
Shape morphing at render time and placement's shape substitution both change
the logical shape type; neither changes the CLR object.
Define a binding in code¶
When a small rendering function is more appropriate than a file, implement
IShapeAttributeProvider and mark a method with [Shape]:
public sealed class CarShapes : IShapeAttributeProvider
{
[Shape("Car")]
public IHtmlContent Car(IShape Shape)
{
var car = (Car)Shape;
var tagBuilder = Shape.GetTagBuilder("div");
tagBuilder.InnerHtml.Append($"A {car.Color} {car.Brand} car.");
return tagBuilder;
}
}
Register the provider:
services.AddShapeAttributes<CarShapes>();
When [Shape] has no argument, the method name is the shape type. Method
parameters can receive shape data and display services through the shape binding
parameter binder. The returned value can be IHtmlContent,
Task<IHtmlContent>, or a value that can be converted to HTML content.
An IShapeTableProvider can also define a binding with BoundAs(). Attribute
providers are usually simpler for code-rendered shapes, while shape table
providers are useful when creation, placement, alternates, wrappers, and
lifecycle handlers must be configured together.
Use lifecycle hooks carefully¶
Orchard Core exposes hooks at several scopes:
| Scope | API |
|---|---|
| One shape instance | ShapeMetadata.OnDisplaying(), OnProcessing(), and OnDisplayed() |
| One display result | ShapeResult.Displaying() and Processing() |
| One shape type | IShapeTableProvider with OnCreating(), OnCreated(), OnDisplaying(), OnProcessing(), and OnDisplayed() |
| Every rendered shape | IShapeDisplayEvents |
| Every created shape | IShapeFactoryEvents |
Use Displaying to add alternates or attributes that are needed for binding
resolution. Use Processing to load state only when the binding will actually
render rather than use cached child content. Use Displayed to inspect or replace
the rendered ChildContent.
IShapeDisplayEvents.DisplayingFinalizedAsync() runs from a finally block and
is intended for cleanup that must happen after every attempted render.
ChildContent¶
ShapeMetadata.ChildContent belongs to the rendering pipeline:
- A binding assigns the initial rendered content.
- Each wrapper reads the current value and replaces it with wrapped content.
- A cache can supply it before binding resolution, which skips binding execution.
- Displayed handlers can replace it.
Do not use ChildContent as the shape's input data. Put input values in strongly
typed model properties or IShape.Properties.
Diagnose binding and placement problems¶
Enable Settings > Debugging > Write shape debug information to add comments around rendered shapes:
<!--shape-start type:Menu bindings:Menu__Main => Themes/Contoso/Views/Menu-Main.cshtml (razor) -->
...
<!--shape-end type:Menu-->
The comments show the logical type and the Razor, Liquid, or code binding that rendered it. They are the fastest way to confirm which alternate won.
When an override is not selected, check:
- The shape's
Metadata.Type,DisplayType, and orderedAlternates. - Whether code uses a canonical binding name while the file uses the filename form.
- Whether the template is under a harvested
Viewsdirectory. - Whether a later-added alternate has higher priority.
- Whether placement changed the shape type or cleared alternates and wrappers.
- Whether the active theme or startup application overrides the module binding.
For location problems, check the shape type and differentiator against the
matching placement.json rule.
Summary¶
A shape is an IShape carrying display data, ordered child items, HTML hints, and
rendering metadata. IShapeFactory creates it, display drivers and placement
compose it into zones, and IDisplayHelper resolves its most specific available
binding. Alternates replace a binding, wrappers surround its output, and shape
type substitution or morphing reuses the same object under another logical type.
The central naming rule is:
- Use canonical names with
_and__in APIs and metadata. - Use dots and hyphens in Razor and Liquid filenames.
- Add alternates from least to most specific because the last alternate wins.