Widget Experiments
This series dives into Kentico 12 MVC Widgets and the related technologies that are part of Kentico's Page Builder technology - Widget Sections, Widgets, Form Components, Inline Editors, and Dialogs 🧐.
Join me 👋, as we explore the nooks and crannies of Kentico EMS MVC Widgets and discover what might be possible with this powerful technology...
If you aren't yet familiar with Kentico MVC Widgets, check out Kentico's Youtube video on Building a Page with MVC Widgets in Kentico.
Goals
This post explores a cool pattern that enables defining Page Builder configuration for multiple instances of a Page Type in 1 place, with any changes to those Widgets taking effect immediately for every page sharing that Page Builder configuration 👀.
I'm calling this pattern Shared Widget Pages.
Use Case - A Blog with a Sidebar
A common design pattern for websites is to have a column of content and a column of links and images, side by side.
Above, in a screenshot of a story on Aeon, we can see this pattern in use.
We can accomplish this same sort of pattern in Kentico 12 MVC by coding up our Razor View to have two columns, one of which is slimmer than the other.
<div class="row">
<section class="col-12 col-lg-9">
<!-- Main Content -->
</section>
<aside class="col-12 col-lg-3">
<!-- Sidebar Content -->
</aside>
</div>
This works great and gives us the layout we're looking for 😊.
For our use case we are going to have a Blog Post as the main content, which will have the following fields:
- Title
- Header Image
- Date
- Author Name
- Summary
- Content
The sidebar will be populated by a couple sets of content, depending on what the content editor chooses:
- List of the Most Recent Blog Posts
- A "Tag Cloud" (all the taxonomies for our Blog)
- An Author Bio
Populating the post content is easy - we get that from our View Model for the View and choose where each of the fields is displayed in the markup.
Populating the sidebar, however, requires some consideration 🤔...
We can either code the View to have a specific set and order of sidebar content, or allow for a more dynamic structure using MVC Widgets.
It seems like a reasonable request for content editors to be able to select, customize, or reorganize the content in a sidebar. For example, the site might begin offering a consulting service and it would be great to be able to add a call-to-action to the sidebar without needing a developer to code it up. With MVC Widgets, a "CTA Widget" could easily be dragged and dropped onto the page by a content editor.
With these requirements, MVC Widgets are the best solution 💪🏾!
⚠ A Problem - Updating the Page Builder Configuration
Before we jump into coding up our site and building the MVC Widgets we need, we should consider some scenarios:
Configuring Every Page Manually
- Each time a content editor creates a new post, they have to add each of the sidebar Widgets and ensure they configure them correctly to show the correct content, with the right design options, in the same order as the previous posts.
If each Page has several Widget Sections and Widgets, each with configurable properties, that's going to be a lot clicks and error prone data entry 😫.
MVC Page Templates could help solve this issue by allowing content editors to create a predefined set of configured Widgets and then use that set as a preset when creating a new Blog Post.
Unfortunately the connection between an MVC Page Template and a Page created with one is not live - instead it's "copy on create" 😟.
If the content editor goes back and updates the MVC Page Template, the pages previously created with it won't reflect those changes, only pages created after the change to the MVC Page Template will be affected.
Also, while Custom MVC Page Templates have a predefined set of Page Builder components (Widgets), normal MVC Page Templates only define Editable Areas.
📌 If you're not yet familiar with how MVC Page Templates work and what role they play in Kentico 12 MVC, check out my post Kentico 12: Design Patterns Part 25 - MVC Page Templates.
Kentico 12: Design Patterns Part 25 - MVC Page Templates
Sean G. Wright ・ Jul 1 '20
Making Changes to All Pages
- Our content editors continue to write Blog Posts and soon there are 300 😮 on the site. At this point they want to add a call-to-action to every Post sidebar, preferably using a Widget so they have control over design and position in the layout.
This problem is not solvable by any existing Page Builder functionality in Kentico 12 MVC 😕.
The Page Builder and Widgets are great at customizing the layout, design, and content of individual pages but there's no way for changes to one page apply to all others. This is because the Widget configuration is stored in the Page being edited (in the CMS_Document
database table).
The most common solution here is to avoid MVC Widgets and instead use fields on the pages to toggle content and design changes for each Page, setting up defaults for those Page fields.
Unfortunately, with this approach we loose all the wonderful dynamic layout, design, and content that MVC Widgets bring, and it still doesn't allow us to retroactively apply the same changes to all the pages 😰.
What we really want is to be able to store the Page Builder configuration for a group of pages in a single place, so that updating that configuration will be reflected in all the pages using it...
Solution - Shared Widget Pages
As I mentioned at the beginning of this post, I want to introduce something I'm calling Shared Widget Pages.
If you have a better idea for a name, I'm open to suggestions! Leave a comment below.
This is a set of patterns rather than any specific feature of Kentico 12 MVC or the Page Builder, and it requires a specific setup for the Page Type definitions, Content Tree organization, MVC application infrastructure, and Page Builder API calls.
What we'll end up with is the ability to use the Page Builder to define Widget Sections and Widgets for a Page Type in one place, and have that specific configuration applied to as many pages in the Content Tree as we want 😮.
When we update that Widget configuration, all associated pages will instantly update 🤯.
We'll also have the flexibility to switch between different Shared Widget pages for a given Page 🤩.
Creating Page Types
We're going to create 3 new Page Types for our Blog example:
- Blog Post List Page
- Shared Widget Page Container
- Blog Post Page
The Blog Post List Page is where we'd normally show a paged list of Blog Posts, but that feature isn't important for our goals. Instead we'll be using it as a parent Page Type to put all our Blog Post pages under.
The Shared Widget Page Container is similar to the Blog Post List Page, but it's only a Container and doesn't represent anything we can navigate to on the live site. This container holds the Blog Post pages that we will be using as the shared source of Page Builder configuration.
📌 Both of the above Page Types should have Blog Post Page as an allowed child Page Type.
The Blog Post Page is what we expect - a Page Type that holds the content for a Blog Post and will have fields matching what was listed at the beginning of this post (but these don't matter for the implementation).
An important part of this Page Type is a field we'll call BlogPostPageSharedWidgetPageNodeGuid
. This field is going to let us refer to a specific "Shared Widget Page" as the source of Page Builder configuration for each Blog Post Page we create 🧐.
Here are the settings for the Page Type field:
Data type: Unique Identifier (GUID)
Field Caption: Shared Widget Page
Form control: Drop-down list
Data source: SQL Query
SELECT NodeGuid, DocumentName
FROM View_CMS_Tree_Joined
WHERE NodeAliasPath LIKE '/shared-widget-pages/%'
AND ClassName = 'Sandbox.BlogPostPage'
Visibility Condition: EditedObject.Parent.NodeAliasPath != "/shared-widget-pages"
Ensure that "Use Page tab" is checked on the "General" tab and specify /blog/{% NodeAlias %}
as the "URL pattern".
Controllers and Widgets
Now that the content configuration is defined, we can create our MVC Controller.
We will use Attribute Routing to keep the setup simple, and a single Controller for all the Blog routes. We will also keep the data access directly in the Controller actions, for the sake of brevity.
📌 I would normally recommend to never put data access into a Controller and instead to keep Controller actions as thin as possible. For more on this, check out Tip #5 in my post Kentico 12: Design Patterns Part 3 - Tips and Tricks, Application Structure:
Kentico 12: Design Patterns Part 3 - Tips and Tricks, Application Structure
Sean G. Wright ・ Jun 1 '19
Let's look at the BlogController
code below:
[RoutePrefix("blog")]
public class BlogController : Controller
{
private readonly PageContext pageContext;
public BlogController(PageContext pageContext) =>
this.pageContext = pageContext;
[Route("{nodeAlias}")]
public ActionResult BlogPost(string nodeAlias)
{
var page = BlogPostPageProvider.GetBlogPostPages()
.Path($"/blog/{nodeAlias}", PathTypeEnum.Explicit)
.TopN(1)
.FirstOrDefault();
if (page is object)
{
pageContext.Id = page.DocumentID;
var sharedWidgetPage = BlogPostPageProvider
.GetBlogPostPages()
.WhereEquals(
nameof(BlogPostPage.NodeGUID),
page.Fields.SharedWidgetPageNodeGuid)
.FirstOrDefault();
HttpContext
.Kentico()
.PageBuilder()
.Initialize(sharedWidgetPage.DocumentID);
return View(page);
}
page = BlogPostPageProvider.GetBlogPostPages()
.Path($"/shared-widget-pages/{nodeAlias}")
.TopN(1)
.FirstOrDefault();
HttpContext
.Kentico()
.PageBuilder()
.Initialize(page.DocumentID);
return View(page);
}
}
That's a lot of code 😵, so let's break it down, piece by piece below.
The first thing we notice is the PageContext
that is being injected to the BlogController
as a dependency.
private readonly PageContext pageContext;
public BlogController(PageContext pageContext) =>
this.pageContext = pageContext;
We need this class to maintain information about the page we are currently trying to render based on the route. Normally this context would be populated wherever our central route handling is happening, but we're doing it here in the action method for simplicity.
📌 Check out my post Kentico 12: Design Patterns Part 10 - MVC Routing with NodeAliasPath to learn how to enable centralized dynamic routing in Kentico 12 MVC.
Kentico 12: Design Patterns Part 10 - MVC Routing with NodeAliasPath
Sean G. Wright ・ Aug 12 '19
The PageContext
is used in our Widgets that are Shared Widget Page "aware" so that we display the correct data when Widgets are rendered.
The second thing we can see in the above code is that we try to get the current page based on the nodeAlias
being associated with a page under /blog
. If the page being rendered is a normal Blog Post Page, then we set the PageContext.Id
to the Blog Post Page's Id:
[Route("{nodeAlias}")]
public ActionResult BlogPost(string nodeAlias)
{
var page = BlogPostPageProvider.GetBlogPostPages()
.Path($"/blog/{nodeAlias}", PathTypeEnum.Explicit)
.TopN(1)
.FirstOrDefault();
if (page is object)
{
// ✅
pageContext.Id = page.DocumentID;
// ...
}
// ...
}
We then query for the Shared Widget Page (which is also a Blog Post Page type) that the current Blog Post Page is associated to (via the SharedWidgetPageNodeGuid
field).
var sharedWidgetPage = BlogPostPageProvider
.GetBlogPostPages()
.WhereEquals(
nameof(BlogPostPage.NodeGUID),
page.Fields.SharedWidgetPageNodeGuid)
.FirstOrDefault();
Finally, we initialize Kentico's Page Builder context to the Shared Widget Page DocumentID
, not the Blog Post Page DocumentID
, and return the data to the View:
HttpContext
.Kentico()
.PageBuilder()
.Initialize(sharedWidgetPage.DocumentID);
return View(page);
This is ⚠ important ⚠! By telling the Page Builder infrastructure that the "current page" is the Shared Widget Page, we will get all the Widget Section and Widget configuration from our Shared Widget Page instead of our Blog Post Page!
If, on the other hand, the page we are currently rendering is itself a Shared Widget Page (because were customizing the shared Page Builder components), we won't find a document with a path matching /blog/{nodeAlias}
. Instead the path will look like /shared-widget-pages/{nodeAlias}
.
We still initialize the Page Builder context to the Shared Widget Page DocumentID
, but we don't set PageContext.Id
because we aren't rendering a normal site page:
page = BlogPostPageProvider.GetBlogPostPages()
.Path($"/shared-widget-pages/{nodeAlias}")
.TopN(1)
.FirstOrDefault();
HttpContext
.Kentico()
.PageBuilder()
.Initialize(page.DocumentID);
return View(page);
📌 There's a line in the Kentico Page Builder source code 🤓 that, conveniently, doesn't allow for Page Builder components to be edited in the Page view when the
DocumentID
the Page Builder is initialized with doesn't match the document that Kentico thinks should currently be rendered. However, Kentico will still render the Page preview.This is great for us, because it will force content editors to update the Widgets on the Shared Widget pages and not the normal site pages, which is exactly what we want.
Defining Shared Widget Page Widgets
All Widgets that work with Shared Widget pages need to be setup following a very specific pattern.
Simply put, Widgets need to source their data using the PageContext.Id
if the Page being rendered is a normal site page, and otherwise use the WidgetController.GetPage().DocumentID
for a Shared Widget Page.
Here's the code for a Latest Blog Posts Widget that is Shared Widget Page aware:
public class LatestBlogPostsWidgetController : WidgetController
{
private readonly IPageContext pageContext;
public LatestBlogPostsWidgetController(
IPageContext pageContext) =>
this.pageContext = pageContext;
public ActionResult Index()
{
// ✅
int documentId = pageContext.IsInitialized
? pageContext.Id
: GetPage().DocumentID;
var blogPosts = BlogPostPageProvider
.GetBlogPostPages()
.Path("/blog", PathTypeEnum.Children)
.WhereNotIn(
nameof(NewsPage.DocumentID),
new[] { documentId })
.ToList();
return PartialView(blogPosts);
}
}
Above we can see that conditional assignment that uses the IPageContext.IsInitialized
to determine where the data for the Widget should come from.
We can now define the IPageContext
and PageContext
types, which are pretty simple 😉:
public interface IPageContext
{
int Id { get; }
bool IsInitialized { get; }
}
public class PageContext : IPageContext
{
public int Id { get; set; }
public bool IsInitialized => Id != 0;
}
The PageContext
should be registered with the dependency injection container as a Per Request dependency (the PageContext
values should be shared between all consumers within a request, but not between requests).
Adding Pages to the Content Tree
Now that the code side is all set, let's add some content and see how this all works!
In the screenshot below we can see how the pages are structured in the Content Tree:
The "Blog" Page is our Blog Post List Page container for all Blog Post pages, and it has 2 child Blog Post pages.
The "Shared Widget Pages" is our container Page Type where we store Blog Post pages that are used only for sharing their Page Builder configuration, not for actual site content. In this screenshot above there are 2 Shared Widget pages for Blog Post pages to select from.
Creating Shared Widget Pages
We first create our Shared Widget pages, as we need at least 1 to reference when we create our normal Blog Post pages.
We can fill in any values we want here 🤔, but often it's helpful to use the name of the field or lorem ipsum text since the Shared Widget Page is meant to show how content and Widgets lay out, without being the actual content (it's like a model home 🏡, not the actual house you live in):
When we view the Page tab for a Shared Widget Page we can see that all the Page Builder functionality (Widgets and Widget Sections) are available to us 👏🏽:
Any changes we make to the Page Builder configuration in the Page tab for a Shared Widget Page will be reflected on all pages that are using this specific Shared Widget Page 😁.
Creating Normal Site Pages
Now we can create some Blog Post pages under the Blog
Page:
We can see the main difference between a Shared Widget Page and a normal Blog Post Page is the Blog Post Page has a Shared Widget Page:
field with a Dropdown showing all available Shared Widget Pages (highlighted in the screenshot above) 🧐.
It's also worth pointing out again, that we can't edit any of the Page Builder components from the Page tab of a normal Blog Post Page:
Instead, we need to navigate back to the Shared Widget Page to make Page Builder changes.
Conclusion
Hopefully your eyes 👀 and brain 🧠 haven't glazed over, because that was a lot to cover!
However, I think what we unlocked here was a really powerful way to use the already powerful 💪🏼 Kentico MVC Page Builder technology.
Shared Widget Pages enable all the dynamic functionality of Widgets with, predictable and scalable content, Widget, and layout management 😎.
We can use Widgets on thousands of pages while only needing to update 1 page to change the sites content or design 🤘.
We can now add this pattern to our tool set, along with MVC Page Templates (and others I've mentioned in my Kentico EMS - MVC Widget Experiments series), to create powerful and easy to use Kentico sites.
If you have any questions, thoughts, or suggestions, please leave a comment below.
...
As always, thanks for reading 🙏!
We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!
If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:
Or my Kentico Xperience blog series, like:
Top comments (0)