Kentico Xperience - Reusing Widget content with the Page Builder

ynze

Ynze Nunnink

Posted on September 3, 2020

Kentico Xperience - Reusing Widget content with the Page Builder

During the development of a Kentico Xperience website the idea came up to have a section of widget content on every page. The content in that section would be the same for every page so it only needs to be managed in one centralized place. It's a reasonable idea because we can reuse existing widgets and the content can be personalized.

There are multiple ways to store content, but widgets can currently only be used on pages. Widgets are part of the page builder feature in Kentico and the content is stored in the document table. Which makes it not flexible enough to reuse widget content across multiple different pages. In this article I will explain how the page builder works and how we can make it more flexible so that widget content can easily be reused.

The page builder

Kentico Xperience comes with a set of features that can be enabled at application startup. The page builder is one of those features and it allows us to use sections and widgets on pages. Multiple steps are required to use the page builder and it's best to understand what happens at each step.

Enabling the page builder feature

To enable the page builder as a feature you can call the UsePageBuilder method at application startup. If you have installed Kentico using the installer there is a file called ApplicationConfig.cs where this can be done instead.

public class ApplicationConfig
{
    public static void RegisterFeatures(IApplicationBuilder builder)
    {
        // Enables the page builder feature
        builder.UsePageBuilder();
    }
}
Enter fullscreen mode Exit fullscreen mode

Behind the scenes it will register all the required services, routes and bundles.

Initializing the page builder

Now that the page builder feature has been enabled we can start using it on pages. The page builder can be accessed via the Kentico() extension of the HttpContext. The first thing to do is initialize the page builder in the controller. The initialize method has a page identifier argument which later on will be used to retrieve and store required information about the page.

HttpContext.Kentico().PageBuilder().Initialize(int pageIdentifier);
Enter fullscreen mode Exit fullscreen mode

Editable areas

After initialization we can set up an editable area in the view. This area will act as a container for sections and widgets.

@Html.Kentico().EditableArea("main")
Enter fullscreen mode Exit fullscreen mode

The content created in editable areas is stored in the database table dbo.CMS_Document as a JSON string. Specifically is stored in the [DocumentPageBuilderWidgets] column. Here is an example of what is stored in that there, mind you I've removed some clutter:

{
   "editableAreas": [
      {
         "identifier": "main",
         "sections": [
            {
               "zones": [
                  {
                     "widgets": [
                        {
                           "type": "Kentico.Widget.RichText",
                           "variants": [
                              {
                                 "properties": {}
                              }
                           ]
                        }
                     ]
                  }
               ]
            }
         ]
      }
   ]
}
Enter fullscreen mode Exit fullscreen mode

The data context

The page builder uses a DataContext object to contain context specific information of the initialized page. This is also where the configurations for sections and widgets are stored. When the DataContext is initialized the PageIdentifier is used to retrieve those configurations, which is set during initialization of the page builder. To be specific it's set when calling this method:

HttpContext.Kentico().PageBuilder().Initialize(int pageIdentifier);
Enter fullscreen mode Exit fullscreen mode

When is the data context initialized? The first time it's used. And very specifically only the first time. The EditableArea helper method is an example of a method that uses the data context. It invokes the data context and that triggers the initialization.

In the following code snippet example the page builder is initialized 3 times with different page identifiers. Every initialization will override the previous one and set the new page identifier. At this point the information about the widgets is not yet being retrieved, that will actually be done when the DataContext is used.

HttpContext.Kentico().PageBuilder().Initialize(111);
HttpContext.Kentico().PageBuilder().Initialize(222);
HttpContext.Kentico().PageBuilder().Initialize(333);
//Page builder is now set to use page with ID 333.
Enter fullscreen mode Exit fullscreen mode

What we want achieve is to render the widgets of multiple pages within the same context. In the snippet below, I try to render widgets of multiple pages by re-initializing the page builder after rendering the widgets.

//Initialize page with identifier 111
@{ Request.RequestContext.HttpContext.Kentico().PageBuilder().Initialize(111); }

//Render widgets by using the DataContext
@Html.Kentico().EditableArea("main")

//Initialize page with identifier 222
@{ Request.RequestContext.HttpContext.Kentico().PageBuilder().Initialize(222); }

//Render widgets by using the DataContext
@Html.Kentico().EditableArea("main")
Enter fullscreen mode Exit fullscreen mode

The problem is that both the first and second editable area will render the widgets of page 111. That's because the underlying data context is only initialized once at the point of rendering the first editable area. When the second editable area is called the data context has already been initialized and the new page identifier will not be used.

The solution: clearing the data context

The data context is only instantiated once which causes the problem of not being able to reuse widgets from other pages. Luckily with reflection we can overcome this problem by using it to clear the data context in between initializations. In this code snippet I'm using an extension method to retrieve the internal mDataContext variable and set its value to null:

public static class PageBuilderFeatureExtensions 
{
    public static void ClearDataContext(this IPageBuilderFeature feature)
    {
        var prop = feature.GetType().GetField("mDataContext", BindingFlags.NonPublic | BindingFlags.Instance);

        if (prop != null)
            prop.SetValue(feature, null);
    }
}
Enter fullscreen mode Exit fullscreen mode

To recap how this works, when the data context is null it automatically initializes itself when it is called, and it uses the PageIdentifier to do so. Using the extension method it is now possible to set the data context to null whenever needed. Which means we can render the widgets of one page, then set the data context to null and then render the widgets of another page.

The next code snippet is pretty much the same as the one we used before, except that now the data context is set to null after the widgets of the first page have been rendered.

//Initialize page with identifier 111
@{ Request.RequestContext.HttpContext.Kentico().PageBuilder().Initialize(111); }

//Render widgets using DataContext
//Widgets of page 111 are rendered
@Html.Kentico().EditableArea("main")

//Sets the DataContext to null
@{ Request.RequestContext.HttpContext.Kentico().PageBuilder().ClearDataContext(); }

//Initialize page with identifier 222
@{ Request.RequestContext.HttpContext.Kentico().PageBuilder().Initialize(222); }

//Render widgets using DataContext
//Widgets of page 222 are rendered
@Html.Kentico().EditableArea("main")
Enter fullscreen mode Exit fullscreen mode

Hooray🎉!! The widgets stored in two pages are now being rendered within a single page.

Beware of edit mode

The widgets of multiple pages are rendered correctly on the front facing site, however editing widgets of more than one page at the a time is not possible. Therefore it's best to check for edit mode when rendering widgets from pages other than the current page. For example if page 111 is the current page, and page 222 another page from the content tree, we want to only render the widgets of page 222 when the context is not in edit mode.

//Only render widgets of page 222 when not in edit mode
@if (!Request.RequestContext.HttpContext.Kentico().PageBuilder().EditMode)
{
    Request.RequestContext.HttpContext.Kentico().PageBuilder().ClearDataContext();

    Request.RequestContext.HttpContext.Kentico().PageBuilder().Initialize(222);

    @Html.Kentico().EditableArea("main")
}
Enter fullscreen mode Exit fullscreen mode

Recommendations before implementing

There are a lot of uses for rendering widget content by clearing and initializing the data context. However it can quickly become a messy and I recommend to keep it simple and not use it unnecessarily.

To ensure that the data context does not become a problem during the page rendering lifecycle I recommend re-initializing the data context with the current page identifier after rendering content from another page. If the data context is not set to the current page you may run into unexpected results when rendering content down the line.

There is a good chance that later versions of Kentico will introduce more flexibility in the page builder. Before you start using any of the techniques described in this article please ensure that the version of Kentico you are using does not already support similar functionality.

Conclusion

The page builder is a powerful feature but it is relatively new, which can make it tricky work with. Hopefully after reading this you've learned a bit more about how it works and how to use it effectively. Being able to render widgets from multiple pages is just another tool for the toolbox. I'll be happy to see it being used for building creative implementations!

💖 💪 🙅 🚩
ynze
Ynze Nunnink

Posted on September 3, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related