Work with reusable field schemas
Reusable field schemas are collections of fields that multiple content types can share. When a schema is updated with a new field, for example, any content types that use that snippet will also have the new field.
Beyond this, you can restrict references between content items based on reusable field schemas, or even filter content item queries, retrieving items of multiple types that share a schema.
This guide will explore this functionality through the lens of a common scenario: content model evolution.
Before you start
This guide requires the following:
- Familiarity with C#, .NET Core, Dependency injection, and the MVC pattern.
- A running instance of Xperience by Kentico, preferably 29.6.1 or higher.Some features covered in the Training guides may not work in older versions.
Code samples
You can find a project with completed, working versions of code samples from this guide and others in the finished branch of the Training guides repository.
The main branch of the repository provides a starting point to code along with the guides.
The code samples in this guide are for .NET 8 only.
They come from a project that uses implicit using directives. You may need to add additional using
directives to your code if your project does not use this feature.
Consider the scenario
As businesses mature over time, they may need more advanced content models to meet their needs. As a developer on such projects, you’ll have to adapt the code to handle these changes.
In this example, the project is moving from a flat Article content type to a Reusable field schema called Article schema, which is shared between two content types: General article and Interview.
To save you time, the main branch already includes the two new schema-based article types, alongside the old article type. You can see them in the Content types application. Code files for the new types and the schemas have already been generated.
However, the code of the site does not yet handle the data from these new types. Interviews and General articles do not display properly on article pages, while the old type does.
Let’s dive into the code, and fix our article service to handle this new content type. Then, at the end, we can add personalization to further enhance the widgets that use it.
Examine the current implementation
In the content tree of the Training guides pages channel, you’ll find eight children under News and articles:
Four pages that display articles of the deprecated type:
- About reptiles
- About birds
- About dogs
- About cats
And four pages referencing schema-based articles that do not display:
- About frogs
- About conifers
- Bean plant cultivation: an interview with Jack Trott
- Sheep care: an interview with Little Bo-Peep
You’ll also notice that these new articles do not display if you use the Article list or Featured article widgets on a page with page builder.
Let’s take a look under the hood to see how article pages are rendered.
Check the controller
Visit ArticlePageController.cs in the TrainingGuides.Web/Features/Articles folder.
You can see that the controller retrieves an ArticlePage
object based on the current web page data context. To put this into perspective, open the Content types app in the Xperience administration. You’ll notice that the new content types, Article (general) and Article (interview) are both reusable types.
The Article page content type contains two fields for selecting content items:
ArticlePageContent
, labelled Article page content (DEPRECATED, DO NOT USE) in the admin UI, for selecting items of the deprecated Article content type.ArticlePageArticleContent
, labelled Article page content in the admin UI, for selecting items that use the new article schema.
Since the Article page controller is working with the Article page content type, rather than the reusable types it references, you can leave it alone.
However, notice that it uses an IArticlePageService
instance to extract article data into an ArticlePageViewModel
object. Let’s inspect this method, ArticlePageService.GetArticlePageViewModel
, in the next section.
Dig into the service
In the ArticlePageService.cs (from the Services folder that neighbors the controller), you’ll see code that converts the linked Article
item from an ArticlePage
object and uses it to construct an ArticlePageViewModel
.
If we make sure this method accounts for the schema-based articles as well, it allow the template and widgets to display them alongside the deprecated articles.
...
/// <summary>
/// Creates a new instance of <see cref="ArticlePageViewModel"/>, setting the properties using ArticlePage given as a parameter.
/// </summary>
/// <param name="articlePage">Corresponding Article page object.</param>
/// <returns>New instance of ArticlePageViewModel.</returns>
public async Task<ArticlePageViewModel> GetArticlePageViewModel(ArticlePage? articlePage)
{
if (articlePage == null)
{
return new ArticlePageViewModel();
}
var article = articlePage.ArticlePageContent.FirstOrDefault();
var articleTeaserImage = article?.ArticleTeaser.FirstOrDefault();
string articleUrl = (await webPageUrlRetriever.Retrieve(articlePage)).RelativePath;
return new ArticlePageViewModel
{
Title = article?.ArticleTitle ?? string.Empty,
Summary = new HtmlString(article?.ArticleSummary),
Text = new HtmlString(article?.ArticleText),
CreatedOn = articlePage.ArticlePagePublishDate,
TeaserImage = AssetViewModel.GetViewModel(articleTeaserImage!),
Url = articleUrl
};
}
...
In it’s current state, the GetArticlePageViewModel
method does the following:
- Gets an
Article
from the providedArticlePage
’sArticlePageContent
property. - Extracts the first
Asset
object from itsArticleTeaser
property. - Retrieves the URL of the provided
ArticlePage
- Assembles and returns an
ArticlePageViewModel
from all of these pieces.
Our goal is to have the project display new articles alongside old ones, prioritizing the new ones, so let’s try to get an IArticleSchema
from the provided ArticlePage
and fall back to the Article
only if none is found.
Update the code
Since we’ll need the URL of the page either way, move the line of code that retrieves it to the beginning.
Then, if the page has an IArticleSchema
in its ArticlePageArticleContent
collection, assemble and return an ArticlePageViewModel
using values from the IArticleSchema
instead of the Article
.
Outside of the if
statement, leave the previous steps to return an ArticlePageViewModel
based on the Article
type.
...
/// <summary>
/// Creates a new instance of <see cref="ArticlePageViewModel"/>, setting the properties using ArticlePage given as a parameter.
/// </summary>
/// <param name="articlePage">Corresponding Article page object.</param>
/// <returns>New instance of ArticlePageViewModel.</returns>
public async Task<ArticlePageViewModel> GetArticlePageViewModel(ArticlePage? articlePage)
{
if (articlePage == null)
{
return new ArticlePageViewModel();
}
// This line was moved up
string articleUrl = (await webPageUrlRetriever.Retrieve(articlePage)).RelativePath;
// Start new code
var articleSchema = articlePage.ArticlePageArticleContent.FirstOrDefault();
if (articleSchema != null)
{
var articleSchemaTeaserImage = articleSchema.ArticleSchemaTeaser.FirstOrDefault();
return new ArticlePageViewModel
{
Title = articleSchema.ArticleSchemaTitle,
Summary = new HtmlString(articleSchema?.ArticleSchemaSummary),
Text = new HtmlString(articleSchema?.ArticleSchemaText),
CreatedOn = articlePage.ArticlePagePublishDate,
TeaserImage = AssetViewModel.GetViewModel(articleSchemaTeaserImage!),
Url = articleUrl
};
}
//End new code
var article = articlePage.ArticlePageContent.FirstOrDefault();
var articleTeaserImage = article?.ArticleTeaser.FirstOrDefault();
return new ArticlePageViewModel
{
Title = article?.ArticleTitle ?? string.Empty,
Summary = new HtmlString(article?.ArticleSummary),
Text = new HtmlString(article?.ArticleText),
CreatedOn = articlePage.ArticlePagePublishDate,
TeaserImage = AssetViewModel.GetViewModel(articleTeaserImage!),
Url = articleUrl
};
}
...
See the results
Run the project and sign in to the Xperience administration. Now if you visit any of the new articles listed earlier, you should see the article content.
Go to the Page Builder tab of the Home page, and click Edit page
Add an instance of the Featured article widget. Configure the widget to reference one of the new articles.
Then, add an instance of the Article List widget. Set its content tree section to the News and articles page, and save your configuration.
Now you should see both widgets displaying new article content.
Add content personalization
After our changes the widgets are working again. However, we have the opportunity to make them even better.
If we implement content personalization, we can allow editors to show different featured articles to visitors who are members of certain contact groups, based on what is more relevant to them.
To give you an idea of what we are implementing, here is what the process of personalizing widgets looks like for editors:
Navigate to the Contact groups application in the Xperience admin, and define a new contact group with placeholder values for its name and description.
Now, define a new personalization condition type in the TrainingGuides.Web/Features/Personalization folder that allows editors to select a contact group and evaluates whether the current contact is in that group.
using CMS.ContactManagement;
using CMS.DataEngine;
using TrainingGuides.Web.Features.Personalization;
using Kentico.PageBuilder.Web.Mvc.Personalization;
using Kentico.Xperience.Admin.Base.FormAnnotations;
using Kentico.Xperience.Admin.Base.Forms;
[assembly: RegisterPersonalizationConditionType(
"TrainingGuides.Web.Features.Personalization",
typeof(IsInContactGroupConditionType),
"Is in contact group",
Description = "Evaluates if the current contact is in one of the contact groups.", IconClass = "icon-app-contact-groups", Hint = "Display to visitors who match at least one of the selected contact groups:")]
namespace TrainingGuides.Web.Features.Personalization;
/// <summary>
/// Personalization condition type based on contact group.
/// </summary>
public class IsInContactGroupConditionType : ConditionType
{
/// <summary>
/// Selected contact group code names.
/// </summary>
[ObjectSelectorComponent(PredefinedObjectType.CONTACTGROUP,
Label = "Contact groups",
Order = 0,
MaximumItems = 0)]
public IEnumerable<ObjectRelatedItem> SelectedContactGroups { get; set; } = Enumerable.Empty<ObjectRelatedItem>();
/// <summary>
/// Evaluate condition type.
/// </summary>
/// <returns>Returns <c>true</c> if implemented condition is met.</returns>
public override bool Evaluate()
{
var contact = ContactManagementContext.GetCurrentContact();
if (contact == null)
{
return false;
}
if (SelectedContactGroups == null || !SelectedContactGroups.Any())
{
return contact.ContactGroups.Count == 0;
}
return contact.IsInAnyContactGroup(SelectedContactGroups.Select(c => c.ObjectCodeName).ToArray());
}
}
Rebuild the solution and access the page builder tab. Now you can define a different featured article that displays for members of your contact group.
What’s next?
The next guide in this series will cover the process of querying content based on reusable field schemas and taxonomies in order to filter the article list widget by taxonomy term.