OrchardCore.DisplayManagement¶
This article is about Display management and placement files.
Placement files¶
Any module or theme can contain an optional placement.json file providing custom placement logic.
Note
The placement.json file must be added at the root of a module or a theme.
Format¶
A placement.json file contains an object whose properties are shape types. Each of these properties is an array of placement rules.
In the following example, we describe the placement for the TextField and Parts_Contents_Publish shapes.
{
"TextField": [ ... ],
"Parts_Contents_Publish" : [ ... ]
}
A placement rule contains two sets of data:
- Filters - defines what specific shapes are targeted.
- Placement information - the placement information to apply when the filter is matched.
Currently you can filter shapes by:
- Their original type, which is the property name of the placement rule, like
TextFieldorContentPart. displayType(Optional): The display type, likeSummaryandDetailfor the most common ones.differentiator(Optional): The differentiator which is used to distinguish shape types that are reused for multiple elements, like field names.
Note
Shape type (placement.json property name) DOES NOT necessarily align with with your part type. For instance, if you created a Content Part GalleryPart without a part driver, your shape type will be ContentPart with differentiator GalleryPart. So your placement.json would look like
{
"ContentPart": [{
"place":"SomeZone",
"differentiator":"GalleryPart"
}],
"GalleryPart": [{...}] //this wont work unless you registered a driver for the part
}
The same distinction applies to editors. Content part editors are wrapped in a ContentPart_Edit shape, so hiding or moving the whole editor row in the admin UI should target ContentPart_Edit, not only the inner part editor shape.
For example, to hide the Services named part on the LandingPage editor:
{
"ContentPart_Edit": [{
"place":"-",
"differentiator":"LandingPage-Services"
}]
}
If you target only an inner editor shape such as BagPart_Edit, the wrapper may still render unless its child shapes are also removed.
Differentiator patterns¶
The differentiator to use depends on which shape type you are targeting, not just on the content part or field you are thinking about.
| Shape type | Typical usage | Differentiator pattern | Example |
|---|---|---|---|
BagPart, FlowPart, TitlePart |
Part display shape, or inner XxxPart_Edit shape from a part driver |
{PartName} |
Services, FlowPart, TitlePart |
ContentPart |
Display wrapper for parts without their own display driver | {PartName} |
GalleryPart |
ContentPart_Edit |
Admin editor wrapper for a content part | {ContentType}-{PartName} |
PlacementTest-BagPart, LandingPage-Services, Article-TitlePart |
TextField, HtmlField, ContentPickerField, etc. |
Standard field display/editor shapes | {PartName}-{FieldName} |
Article-Subtitle, Address-City |
TextField_Display, HtmlField_Display, etc. |
Field display-mode shapes | {PartName}-{FieldName}-{FieldType}_Display__{DisplayMode} |
Blog-Subtitle-TextField_Display__Header |
PartName is the name of the attached part. For non-named parts this is usually the part type, such as BagPart, FlowPart, WidgetsListPart, or TitlePart. For named parts, use the custom part name such as Services.
Examples by shape type¶
Content part display shapes¶
To hide a named Services BagPart on the front end:
{
"BagPart": [{
"differentiator":"Services",
"place":"-"
}]
}
To hide a TitlePart display shape on the front end:
{
"TitlePart": [{
"differentiator":"TitlePart",
"place":"-"
}]
}
Content part editor wrappers¶
To hide the whole BagPart editor row on the PlacementTest editor:
{
"ContentPart_Edit": [{
"differentiator":"PlacementTest-BagPart",
"place":"-"
}]
}
To hide the whole FlowPart editor row:
{
"ContentPart_Edit": [{
"differentiator":"PlacementTest-FlowPart",
"place":"-"
}]
}
To hide the whole WidgetsListPart editor row:
{
"ContentPart_Edit": [{
"differentiator":"PlacementTest-WidgetsListPart",
"place":"-"
}]
}
To hide the whole TitlePart editor row:
{
"ContentPart_Edit": [{
"differentiator":"PlacementTest-TitlePart",
"place":"-"
}]
}
Use ContentPart_Edit when you want to move or hide the whole editor row, including its label, description, and wrapper. Shapes such as BagPart_Edit, FlowPart_Edit, or TitlePart_Edit only target the inner editor content.
Additional custom filter providers can be added by implementing IPlacementNodeFilterProvider.
For shapes that are built from a content item, you can filter by the following built in filter providers:
contentType(Optional): A single ContentType or Stereotype, or an array of ContentTypes and / or Stereotypes that the content item from which the shape was built should match.*maybe used to match all content types starting with the preceding value, i.e.Art*.contentPart(Optional): A single ContentPart or an of array of ContentParts that the content item from which the shape was built should contain.path(Optional): A single path or an of array of paths that should match the request path.
Placement information consists of:
place(Optional): The actual location of the shape in the rendered zone. A value of-will hide the shape, and a value starting with/will move the shape to a layout zone.alternates(Optional): An array of alternate shape types to add to the current shape's metadata.wrappers(Optional): An array of shape types to use as wrappers for the current shape.shape(Optional): A substitution shape type.
Position format¶
The place value follows the format Zone:Position, where:
- Zone is the target zone name (e.g.,
Content,Parts). - Position is an optional numeric value that determines the ordering of shapes within the zone.
Examples:
| Place Value | Zone | Position | Description |
|---|---|---|---|
Content |
Content | (empty, treated as 0) | Places in Content zone at default position |
Content:5 |
Content | 5 | Places in Content zone at position 5 |
Content:5.1 |
Content | 5.1 | Places in Content zone at position 5.1 (between 5 and 6) |
Content:before |
Content | before | Places at the very beginning of the zone |
Content:after |
Content | after | Places at the very end of the zone |
Position ordering rules¶
Shapes within a zone are sorted by their position using these rules:
- Positions are compared numerically, not alphabetically (e.g.,
2comes before10). - Dot notation is supported for sub-positioning:
1.1comes after1but before2. Positions like1.5can be used to insert shapes between1and2. - The special keyword
beforeplaces a shape before all numbered positions (internally mapped to-9999). - The special keyword
afterplaces a shape after all numbered positions (internally mapped to9999). - A shape with no position (empty string) is treated as position
0. - When multiple shapes share the same position, they maintain their registration order (stable sort).
The full ordering is: before, 0 (empty), 1, 1.1, 1.2, 2, 10, then after.
Note
Position ordering within a zone is determined solely by the position value. The module or project name does not affect the order of shapes within a zone.
{
"TextField": [
{
"displayType": "Detail",
"differentiator": "Article-MyTextField",
"contentType": [ "Page", "BlogPost" ],
"contentPart": [ "HtmlBodyPart" ],
"path": [ "/mypage" ],
"place": "Content",
"alternates": [ "TextField_Title" ],
"wrappers": [ "TextField_Title" ],
"shape": "AnotherShape"
}
]
}
Placement precedence¶
The placement info chosen for a shape is based on the following order:
- The main startup project (This can act as a super theme)
- Active theme (This will be the active front end theme if you're viewing the front end, or the active admin theme if you're viewing the admin)
- Modules (Ordered by dependencies)
Placing Fields¶
Fields have a custom differentiator as their shape is used in many places.
It is built using the Part it's contained in, and the name of the Field.
For instance, if a field named MyField would be added to an Article content type, its differentiator would be Article-MyField.
If a field named City was added to an Address part then its differentiator would be Address-City.
For example, to place a TextField named Subtitle attached directly to the Article content type:
{
"TextField": [{
"differentiator":"Article-Subtitle",
"place":"Content:2"
}]
}
For a field named City attached to an Address part:
{
"TextField": [{
"differentiator":"Address-City",
"place":"Content:3"
}]
}
Field Display Modes¶
The placement rules are stricter for non-standard display modes.
- The shape type must include
_Display. For exampleTextField_Display. - You must use the full differentiator defined as
[PartType]-[FieldName]-[FieldType]_Display__[DisplayMode]. For a text field namedMyFieldon the Content TypeBlogthe differentiator isBlog-MyField-TextField_Display__Header.
{
"TextField_Display": [
{
"place": "Content:1",
"differentiator": "Blog-MyField-TextField_Display__Header"
}
]
}
Shape differentiators¶
You can find information about shape differentiators in the Templates documentation
Related Articles¶
Editor shape placement¶
Editor shapes support grouping placement, which allows you to group editor shapes, to create a variety of content editor layouts.
Supported groupings¶
- Tabs
- Cards
- Columns
Each grouping works by itself, or can be progressive, so Tabs can support Cards, and / or Columns, and Cards can support Columns.
Groupings are created by applying a modifier and a group name.
Modifiers¶
- The Tabs modifier is
# - The Cards modifier is
% - The Columns modifier is
|
Each of these modifiers support a position modifier for the group, in the format ; and Columns support an additional modifier for the column width, of _.
To apply a position modifier, or column width modifier, apply the appropriate value to every group name.
Fields or Parts which do not have a grouping will fall into the default Content group when other fields apply a grouping.
Shape position vs. group position
The position (the number after :) controls the order of shapes within a zone. The group position (the number after ;) controls the order of the groups themselves (e.g., which tab appears first). These are two different things.
For example, in Parts:2#Settings;1:
:2→ The shape renders at position 2 within the zone (after shapes at position 1).#Settings;1→ The Settings tab is ordered at group position 1 among other tabs.
If multiple shapes share the same position value (e.g., all use :1), their order will depend on module registration order, which may not be predictable. Always use distinct positions for shapes that need a specific order within a zone.
Examples¶
In the following example we place the MediaField_Edit shape in a tab called Media, and position the Media tab first, and the Content tab second.
{
"MediaField_Edit" : [
{
"place" : "Parts:0#Media;0",
"contentType": [
"Article"
]
}
],
"HtmlField_Edit" : [
{
"place": "Parts:0#Content;1",
"contentType": [
"Article"
]
}
]
}
In the following example we place the MediaField_Edit shape in a card called Media, and position the Media card first, and the Content card second.
{
"MediaField_Edit" : [
{
"place" : "Parts:0%Media;0",
"contentType": [
"Article"
]
}
],
"HtmlField_Edit" : [
{
"place": "Parts:0%Content;1",
"contentType": [
"Article"
]
}
]
}
In the following example we place the MediaField_Edit shape in a column called Media, and position the Media column first, and the Content column second.
We also specify that the Content column will take 9 columns, of the default 12 column grid.
{
"MediaField_Edit" : [
{
"place" : "Parts:0|Media;0",
"contentType": [
"Article"
]
}
],
"HtmlField_Edit" : [
{
"place": "Parts:0|Content_9;1",
"contentType": [
"Article"
]
}
]
}
Note
By default the columns will break responsively at the md breakpoint, and a modifier will be parsed to col-md-9.
If you want to change the breakpoint, you could also specify Content_lg-9, which is parsed to col-lg-9.
Column layout behavior¶
Each shape with a column modifier (|ColumnName) gets its own column wrapper inside a Bootstrap row (<div class="row">). The column name is an arbitrary label used for the CSS class — it has no special meaning (e.g., |MyCol_4 is the same as |Sidebar_4 — neither implies left or right positioning).
The column width (the number after _) maps to Bootstrap's 12-column grid. For example, _4 means col-md-4 (4/12 = ⅓ width). If the total widths exceed 12, columns wrap to the next line automatically (standard Bootstrap behavior).
The column position (the number after ;) determines the order of columns within the row.
Shapes without a column modifier are rendered as full-width content outside the row. Their vertical position relative to the column row is determined by their order in the placement sequence:
- Shapes positioned before the first column-specified shape render above the row.
- Shapes positioned after the first column-specified shape render below the row.
Example: Three equal-width columns with a full-width field below
{
"TextField_Edit" : [
{
"place" : "Parts:1%Details;1|Col_4;1",
"differentiator": "MyPart-FieldA"
},
{
"place" : "Parts:1%Details;1|Col_4;2",
"differentiator": "MyPart-FieldB"
},
{
"place" : "Parts:1%Details;1|Col_4;3",
"differentiator": "MyPart-FieldC"
},
{
"place": "Parts:2%Details;1",
"differentiator": "MyPart-FieldD"
}
]
}
This produces:
- A row with three columns, each 4/12 width (⅓): FieldA, FieldB, FieldC side by side.
- Below the row: FieldD rendered at full width (no column wrapper).
Example: Two columns with different widths
{
"TextField_Edit" : [
{
"place" : "Parts:1|Sidebar_3;1",
"differentiator": "MyPart-FieldA"
},
{
"place" : "Parts:1|Main_9;2",
"differentiator": "MyPart-FieldB"
}
]
}
This produces a row with a 3/12 width column (FieldA) and a 9/12 width column (FieldB).
Note
The column name (e.g., Col, Sidebar, Main) is just an identifier used for CSS targeting via the generated class column-{name}. You can use any name — it does not affect positioning. To put multiple fields in separate columns, give each field a column modifier; the position (;N) controls their order in the row.
Warning
All column-specified shapes within the same grouping level (same tab/card) share a single row. If you need multiple separate column rows, place them in different cards.
Dynamic part placement¶
In the following example we place a dynamic part (part without driver, i.e. created in json) GalleryPart in a zone called MyGalleryZone. When displaying that part inside Content template we would execute:
=== Content-Product.Detail.html
@await DisplayAsync(Model.MyGalleryZone)
Dynamic parts use ContentPart shape for detail display with differentiator of Part name, so the placement file would look like this:
{
"ContentPart": [
{
"place": "MyGalleryZone",
"differentiator": "GalleryPart"
}
]
}
This setup would then show your template (e.g. GalleryPart.cshtml or GalleryPart.Detail.cshtml) where DisplayAsync was called.
If we would like to show the same part in a summary display content template (or any other that isn't Detail display type):
=== Content-Product.Summary.html
@await DisplayAsync(Model.MyGalleryZone)
Our placement would look like this (note the _Summary suffix to ContentPart name; change your suffix accordingly):
{
"ContentPart_Summary": [
{
"place": "MyGalleryZone",
"differentiator": "GalleryPart"
}
]
}
This setup would then show your template (e.g. GalleryPart.cshtml or GalleryPart.Summary.cshtml) where DisplayAsync was called.
Fluent Location API¶
When setting placement locations in display drivers, you can use the PlacementLocationBuilder fluent API as an alternative to manually constructing location strings. This makes the placement intent more explicit and reduces mistakes.
The builder enforces the nesting hierarchy through the rendering order (Zone → Tab → Card → Column), using a single fluent PlacementLocationBuilder class where all methods return the same instance for easy chaining.
String syntax vs. Fluent API¶
// String syntax (traditional):
.Location("Parameters:5#Settings;1")
// Fluent API (equivalent):
.Location(l => l.Zone("Parameters", "5").Tab("Settings", "1"))
Available methods¶
The builder starts with .Zone() and all methods return the same PlacementLocationBuilder instance for fluent chaining:
| Method | Description | String equivalent |
|---|---|---|
.Zone("Content", "5") |
Sets the target zone and shape position (required) | Content:5 |
.AsLayoutZone() |
Targets a layout zone | / prefix |
.Tab("Settings", "1") |
Groups into a tab with optional group position | #Settings;1 |
.Card("Details", "2") |
Groups into a card with optional group position | %Details;2 |
.Column("Left", "1", "9") |
Groups into a column with position and width | \|Left_9;1 |
.Group("search") |
Assigns a group identifier for filtering | @search |
Note
The .Column() method parameters are: name, position, width. For example, .Column("Left", "1", "9") creates a column named "Left" at position 1 with width 9.
Methods can be called in any order after .Zone(). Levels can be skipped (e.g., .Zone().Card() without a .Tab() is valid).
Examples¶
Place a shape at position 5 in the Content zone:
.Location(l => l.Zone("Content", "5"))
// Produces: "Content:5"
Place a shape in a tab:
.Location(l => l.Zone("Parameters", "1").Tab("Settings", "1"))
// Produces: "Parameters:1#Settings;1"
Full nesting — zone → tab → card → column:
.Location(l => l
.Zone("Parameters", "5")
.Tab("Settings", "1")
.Card("Details", "2")
.Column("Left", "3", "9"))
// Produces: "Parameters:5#Settings;1%Details;2|Left_9;3"
Place a shape in a column with a width (skipping tab and card):
.Location(l => l.Zone("Parts", "0").Column("Content", "1", "9"))
// Produces: "Parts:0|Content_9;1"
Place a shape in a layout zone:
.Location(l => l.Zone("Content", "5").AsLayoutZone())
// Produces: "/Content:5"
Set location per display type:
.Location("Summary", l => l.Zone("Content", "1"))
// Produces: "Content:1" for the "Summary" display type