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();
}
}
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);
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")
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": {}
}
]
}
]
}
]
}
]
}
]
}
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);
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.
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")
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);
}
}
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")
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")
}
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!
Top comments (0)