Kentico Xperience Xplorations: Determining a Request's Page Builder Mode

seangwright

Sean G. Wright

Posted on January 11, 2021

Kentico Xperience Xplorations: Determining a Request's Page Builder Mode

In this post we'll be exploring how to determine the mode of a request in a Kentico Xperience 13.0 ASP.NET Core application, and also how to create a new class and interface to help us use this information in our code.

You can skip the post and jump right into the GitHub repository and NuGet package I've already created, or read on to see how it's made and how to use it 🧐!

Starting Our Journey: The Modes of the Xperience Page Builder

In Kentico Xperience 13.0, the Page Builder has several 'modes'. Each of these modes represents a different context under which a request to the ASP.NET Core application was made 🤔.

If a request was made from a content manager updating a Widget or creating a new Section in an Editable Area, then the Page Builder will be configured in ✍ 'Edit' mode.

Similarly, if the request was made from a preview URL, where a user can see the unpublished updates to content or the Page Builder configuration for a Page but cannot edit the Page, then the Page Builder is in 👀 'Preview' mode.

If the request was made to a live site URL, not using the Page Builder UI in the Content Management application, and not using a preview URL, then the Page Builder can be considered to be in ⚡ 'Live' mode.

Digging Deeper: Using Xperience's APIs to Determine the Page Builder Mode

When making a request to our ASP.NET Core application to put the Page Builder into 'Edit' or 'Preview' mode, the URL has a distinct structure, which always includes a path starting with /cmsctx/.

If the request has an ?editmode=1 query string parameter, then the request will be in 'Edit' mode, otherwise 'Preview' mode.

However, we never see these URLs in our application code, because Xperience's integration with ASP.NET Core does some rewriting of the request URL so that we don't have to worry about parsing it 🤓. Instead there are some documented APIs for accessing the state of the Page Builder.

These APIs can be found as extensions off the IHttpContextAccessor, which is an ASP.NET Core type that Xperience ensures is registered with the ASP.NET Core Dependency Injection container.

The following shows how we can determine if a request is in 'Preview' mode:

public class HomeController : Controller
{
    private readonly IHttpContextAccessor accessor;

    public HomeController(IHttpContextAccessor accessor) =>
        this.accessor = accessor;

    public ActionResult Index()
    {
        // Found in the Kentico.Content.Web.Mvc namespace
        bool isPreviewMode = accessor.HttpContext
            .Kentico()
            .Preview()
            .Enabled;
    }
}
Enter fullscreen mode Exit fullscreen mode

Likewise, here is how we determine if a request is in 'Edit' mode:

public class HomeController : Controller
{
    private readonly IHttpContextAccessor accessor;

    public HomeController(IHttpContextAccessor accessor) =>
        this.accessor = accessor;

    public ActionResult Index()
    {
        // Found in the Kentico.PageBuilder.Web.Mvc namespace
        bool isEditMode = accessor.HttpContext
            .Kentico()
            .PageBuilder()
            .EditMode;
    }
}
Enter fullscreen mode Exit fullscreen mode

Into the Weeds: Clarifying Our Definition of Page Builder Modes

Technically, 'Preview' mode is independent of the Page Builder 😕, because the Preview feature is used to determine what content is displayed, while the Page Builder editing feature is used to determine whether or not to render the Page Builder Page Template, Section, Widget UI.

Note: Any time a content manager can view the "Page" tab in the Content Management application and also click the "Save" button, then the request to display the ASP.NET Core page, within the "Page" tab, will be in 'Edit' mode - it doesn't matter whether or not you are using Widgets, Page Templates, or any other Page Builder features on the Page 😮.

Take a look at the private bool TryGetUrl(out string url) method in CMS\CMSModules\Content\CMSDesk\MVC\Edit.aspx.cs to see how the Preview/Edit mode is set for a URL in the Pages module of the Content Management application 🤓.

Determining the current request mode is a bit trickier 😖 than just checking one of the above APIs - instead we will often need to check both.

Here are two tables showing what we can determine about a request by checking only one of the two APIs:

Request Type Preview().Enabled == true Preview().Enabled == false
Site Visitor false true
Readonly Content Manager ? false
Editing Content Manager ? false
Request Type PageBuilder().EditMode == true PageBuilder().EditMode == false
Site Visitor false ?
Readonly Content Manager false ?
Editing Content Manager true false

We can see that by checking only one API, we can have incomplete information.

Finding a Path: IPageBuilderContext

It can be easy to forget we need to check both APIs to really determine the mode a request is currently in, so let's create a new abstraction, IPageBuilderContext, to help alleviate this problem 💪🏽.

Below we can see what that might look like:

public interface IPageBuilderContext
{
    /// <summary>
    /// True if either IsLivePreviewMode or IsEditMode is true. 
    /// Also the opposite of IsLiveMode
    /// </summary>
    bool IsPreviewMode { get; }

    /// <summary>
    /// True if IsLivePreviewMode and IsEditMode is false.
    /// Also the opposite of IsPreviewMode
    /// </summary>
    bool IsLiveMode { get; }

    /// <summary>
    /// True if the current request is being made for
    /// a preview version of the Page with editing disabled
    /// </summary>
    bool IsLivePreviewMode { get; }

    /// <summary>
    /// True if the current request is being made for
    /// the Page Builder experience
    /// </summary>
    bool IsEditMode { get; }

    /// <summary>
    /// The current Mode as a PageBuilderMode value
    /// </summary>
    PageBuilderMode Mode { get; }

    /// <summary>
    /// The value of Mode as a string
    /// </summary>
    string ModeName();
}

/// <summary>
/// The various states that a request for a Page can be in,
/// in relation to the Page Builder
/// </summary>
public enum PageBuilderMode
{
    Live,
    LivePreview,
    Edit
}
Enter fullscreen mode Exit fullscreen mode

With our abstraction defined, let's look at the implementation:

public class PageBuilderContext : IPageBuilderContext
{
    private readonly IHttpContextAccessor accessor;

    public PageBuilderContext (IHttpContextAccessor accessor) =>
        this.accessor = accessor;

    public bool IsPreviewMode => 
        accessor.HttpContext
            .Kentico()
            .Preview()
            .Enabled;

    public bool IsLivePreviewMode => IsPreviewMode && !IsEditMode;

    public bool IsEditMode => 
        accessor.HttpContext
            .Kentico()
            .PageBuilder()
            .EditMode;

    public bool IsLiveMode => !IsPreviewMode;

    public PageBuilderMode Mode => 
        IsLiveMode
            ? PageBuilderMode.Live
            : IsLivePreviewMode
                ? PageBuilderMode.LivePreview
                : PageBuilderMode.Edit;

    public string ModeName() => 
       Enum.GetName(
           typeof(PageBuilderMode), 
           Mode) ?? "";
}
Enter fullscreen mode Exit fullscreen mode

Our Destination: Using IPageBuilderContext

Now that we understand the different modes of a request in our Xperience application, and we have a custom abstraction that neatly exposes that state, we can leverage it in our code 👍🏾.

First, we'll want to register our type with ASP.NET Core's dependency injection system:

// Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<
        IPageBuilderContext, 
        PageBuilderModeContext
    >();
}
Enter fullscreen mode Exit fullscreen mode

Now, let's return to our initial example, using the HomeController:

public class HomeController : Controller
{
    private readonly IPageBuilderContext context;
    private readonly IProgressiveCache cache;
    private readonly IEventLogService service;

    public HomeController(
        IPageBuilderContext context,
        IProgressiveCache cache,
        IEventLogService service)
    {
        this.context = context;
        this.cache = cache;
        this.service = service;
    }

    public ActionResult Index()
    {
        if (context.IsEditMode)
        {
            return View(Calculate());
        }

        string name = $"calc|mode:{context.Mode}";

        var cs = new CacheSettings(
            cacheMinutes: 10,
            cacheItemNameParts: name);

        int calculation = cache.Load(Calculate, cs);

        return View(calculation);
    }

    private int Calculate(CacheSettings cs)
    {
        if (context.IsLiveMode)
        {
            log.LogInformation("Calculation", "CACHE_MISS");
        }

        // Define the cache dependency keys

        // Do something that takes some time to compute

        return calculation;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example we are using the IPageBuilderContext to determine if the request is in 'Edit' mode, and if so, we skip caching completely and return an (expensively) calculated result.

Otherwise, we use the cache, ensuring that we have separate cache entries for "Live Preview" mode and "Live" mode by way of the cache item name.

The cache item name could look like "calc|mode:LivePreview" or "calc|mode:Live".

The reason for using the cache in LivePreview mode is that we want previewing the Page to be quick, especially if the preview URL is shared between viewers 😊.

But we also want to record all cache misses in Live mode to determine how effective our use of the cache is on the live site.

Since we don't want previews of the Page to effect the log, we have two different cached values 🧠.

...

If we find ourselves needing to access the mode of a request throughout our application, the usefulness of our IPageBuilderContext abstraction becomes apparent 👏!

Conclusion

There's a lot going on in the Kentico Xperience platform, especially when using the Page Builder!

It's worth diving into the details and figuring out 🧐 how and why things work the way they do, especially when we want our application's code to use the platform correctly.

While Xperience provides us with ways of determining the mode of a request to our ASP.NET Core application, creating a reusable abstraction, like IPageBuilderContext, can help add more meaning to the information that the platform exposes.

In a follow-up post I'll show how we use the IPageBuilderContext to create a custom ASP.NET Core Tag Helper that we can use in our Razor views in the same way we use the IPageBuilderContext in our C# code.

Until then, thanks for reading 🙏!

References


Photo by Jordan Madrid 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 or Xperience tags here on DEV.

#kentico

#xperience

Or my Kentico Xperience blog series, like:

💖 💪 🙅 🚩
seangwright
Sean G. Wright

Posted on January 11, 2021

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

Sign up to receive the latest update from our blog.

Related