loading...
Cover image for Kentico EMS: MVC Widget Experiments Part 4 - Shared Widget Pages

Kentico EMS: MVC Widget Experiments Part 4 - Shared Widget Pages

seangwright profile image Sean G. Wright ・12 min read

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.

Screenshot of a website with a sidebar of content on the left

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.

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

Blog Page Types listed in the CMS

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.

Container Page Type listed in the CMS

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

Kentico CMS Page Type field definition screen

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:

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.

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:

Blog and Shared Widget Page content structure 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):

Shared Widget Page form tab

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 👏🏽:

Shared Widget Page preview with customizable widgets

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:

Alt Text

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:

Blog Post Page tab preview with no editable components

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 🙏!


Photo by Seyedeh Hamideh Kazemi on Unsplash

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:

Discussion

pic
Editor guide