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;
}
}
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;
}
}
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 inCMS\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
}
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) ?? "";
}
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
>();
}
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;
}
}
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
- Enabling Xperience Preview Mode
- Using the Request mode when Querying for Pages
- Xperience Page Builder Mode Tag Helper repository
- Xperience Page Builder Mode Tag Helper NuGet package
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.
Or my Kentico Xperience blog series, like:
Top comments (0)