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:

Code samples

You can find a project with completed, working versions of code smaples 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.

diagram of article content types

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

Deprecated article displaying its content

Schema-based article with no display

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 persepective, 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.

Reusable content 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.

C#ArticlePageService.cs


...
/// <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:

  1. Gets an Article from the provided ArticlePage’s ArticlePageContent property.
  2. Extracts the first Asset object from its ArticleTeaser property.
  3. Retrieves the URL of the provided ArticlePage
  4. 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.

C#ArticlePageService.cs


...
/// <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.

Schema-based article displaying its 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.

Widgets displaying the content of schema-based articles

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.

C#IsInContactGroupConditionType.cs

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 upcoming 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.