Create a list content item
Introduction
If we want to create a content item which can contains other content items.
We can create a new content type with ListPart and use it as a parent/container.
Then create a child content item with ContainedPart to associate it with its parent.
This list content item can be applied to many situation, e.g. a blog contains a list blog posts, an order contains a list of products.
Example content types
We want to create a number guessing game that allows a user to guess a number. Here are our simple content types:
Gamecontent type, it is a container or parent which contains multiple UserGame content items.UserGamecontent type, it is a contained type or child which is contained by Game content item.
classDiagram
class Game { TitlePart MarkdownBodyPart GamePart ListPart }
class UserGame { UserGamePart ContainedPart }Note GamePart and UserGamePart are custom content parts.
In Migrations.cs, we define Game and UserGame content types as the following code:
public int Create() { _contentDefinitionManager.AlterPartDefinition( nameof(GamePart), part => part.Attachable(true).WithDescription("Provide GamePart to a content item.") );
_contentDefinitionManager.AlterPartDefinition( nameof(UserGamePart), part => part.Attachable(true).WithDescription("Provide UserGame to a content item.") );
const string userGameTypeName = "UserGame"; _contentDefinitionManager.AlterTypeDefinition( userGameTypeName, type => type .WithPart(nameof(UserGamePart)) .Creatable() .Versionable(false) .Draftable(false) .Listable() );
_contentDefinitionManager.AlterTypeDefinition( "Game", type => type .WithPart(nameof(TitlePart), part => part.WithPosition("0").WithDisplayName("Game title")) .WithPart( nameof(MarkdownBodyPart), part => part.WithEditor("Wysiwyg").WithPosition("1").WithDisplayName("Game rule") ) .WithPart(nameof(GamePart).WithPosition("2")) .WithPart( nameof(ListPart), part => part.WithSettings(new ListPartSettings() { ContainedContentTypes = new[] { userGameTypeName }, PageSize = 50, }) ) .Creatable() .Versionable(false) .Draftable(false) .Listable(false) );
return 1; }Note if you have custom parts like GamePart and UserGame, you need to create a content part drivers and register them in Startup.cs file.
How to create a child content item programmatically.
- If we want to create a child content item (UserGame) on frontend with an alternate shape e.g.
Content-UserGame.cshtmlas the following HTML form. We need to passListPart.ContainerIdandListPart.ContentTypeas query string values to associate it with a container/parent content item (Game).@using OrchardCore.DisplayManagement.Zones<style asp-src="~/OrchardCore.Contents/Styles/Contents.min.css" debug-src="~/OrchardCore.Contents/Styles/Contents.css"></style><formmethod="post"asp-action="Create"asp-route-ListPart.ContainerId="@gameContentItemId"asp-route-ListPart.ContentType="UserGame"><div class="edit-container"><div class="edit-body"><div class="edit-item-parts">@if (Model.Parts != null){@await DisplayAsync(Model.Parts)}</div>@if (Model.Actions != null){<div class="edit-item-secondary group"><div class="edit-item-actions form-group">@await DisplayAsync(Model.Actions)</div></div>}</div></div></form> - Note you can also pass
ListPart.EnableOrderingquery string as true to enable ordering a list. - Then in a custom action method, we can create a UserGame content item as usual.
[HttpPost]public async Task<IActionResult> Create(){var contentItem = await _contentManager.NewAsync("UserGame");contentItem.Owner = User.FindFirstValue(ClaimTypes.NameIdentifier);contentItem.Author = User.FindFirstValue(ClaimTypes.Name);if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditOwnContent, contentItem)){return Forbid();}var typeDefinition = _contentDefinitionManager.GetTypeDefinition(contentItem.ContentType);try{var model = await _contentItemDisplayManager.UpdateEditorAsync(contentItem, _updateModelAccessor.ModelUpdater, true);if (ModelState.IsValid){await _contentManager.CreateAsync(contentItem, VersionOptions.Published);}await _notifier.SuccessAsync(H["{0} has been created.", typeDefinition.DisplayName]);}catch (Exception ex){await _session.CancelAsync();await _notifier.ErrorAsync(H["An error occurred while creating {0}.", typeDefinition.DisplayName]);_logger.LogError(ex, "An error occurred while creating {typeDisplayName}.", typeDefinition.DisplayName);}return View();}
Why do we need to set ListPart.ContainerId and ListPart.ContentType as query string values?
- ContainedPartDisplayDriver which is responsible to set values of ContainedPart’s properties requires us to pass these values to it:
ListPart.ContainerIdis a parent’s ContentItemId for associating a child content item to its parent.ListPart.ContentTypeis used by the driver to check if the current content item matches its value to make sure we don’t applyContainedPartto unintended type. This is because the driver is always called by any content item whenever we display/edit/update a content item.
Why we can’t attach ContainedPart when we defined a child type (UserGame) in Migrations.cs
- If we defined a new item with ContentedPart in its’ definition, we will get a new content item with ContainedPart which has ContainerId’s value equal to null.
- When ContainedPartDisplayDriver finds a content item has ContainedPart, it will render
ListPart_ContainerIdshape which set hidden input field with name toListPart.ContainerIdand value tonullin ListPart.ContainerId.cshtml template. - Even thought we send
ListPart.ContainerIdas query string, it can’t override value ofListPart.ContainerIdwhich is passed as hidden input field because ASP.NET Core MVC model binding will take a value of form field before query string as mentioned in this document. This results in a child item can’t associate to its parent.
Render a list content item
- To render a parent content item (UserGame) and its child content items (UserGame), we can create an alternate template in a
Viewsfolder of our customer module as the following code.@* Views/Game-ListPart.cshtml *@@model OrchardCore.Lists.ViewModels.ListPartViewModel@inject OrchardCore.ContentManagement.Display.IContentItemDisplayManager ContentItemDisplayManager@if (Model.ContentItems.Any()){<ul class="list-group">@foreach (var contentItem in Model.ContentItems){var contentItemSummary = await ContentItemDisplayManager.BuildDisplayAsync(contentItem,Model.Context.Updater,"Summary",Model.Context.GroupId);<li class="list-group-item">@await DisplayAsync(contentItemSummary)</li>}</ul>}else{<p class="alert alert-warning">@T["The list is empty"]</p>}@await DisplayAsync(Model.Pager)