Widget Experiments
This series dives into Kentico Xperience MVC Widgets and the related technologies that are part of Kentico's Page Builder technology - Widget Sections, Widgets, Form Components, Inline Editors, Dialogs, and Page Templates 🧐.
Join me 👋, as we explore the nooks and crannies of Kentico Xperience MVC Widgets and discover what might be possible with this powerful technology...
If you aren't yet familiar with Xperience MVC Widgets, check out the Youtube video on Building a Page with MVC Widgets in Xperience.
The previous version of this blog series is Kentico EMS - MVC Widget Experiments
Goals
This post is going to show how using Related Pages and MVC Widgets is a safe and easy way to help content editors reuse content on your site 😉.
Use Case - Featured Articles
Our site has many articles written by our content editors. They want to be able to feature some of those articles in various places around the site.
Notably, they want to show a list of articles in various parts of the layout of some of our pages, but they want to be able to easily choose which articles (and how many) are displayed on each page.
The content editors want to be able to use the MVC Page Builder to add a Widget into the page which will show Articles that are supposed to be displayed for that specific page, with no specific limit on the number of Articles displayed 👍🏾.
Problem - Multi-Select Widgets Are Difficult
Widgets in Xperience always have access to the page they were created on through the ComponentViewModel.Page
property (this applies to Basic View-only Widgets or View Component Widgets.
But, what about when we want to access other pages 🤔?
The Page Selector form component allows for a content editor to configure a Widget property to hold the NodeGuid
of another Page in the content tree - however this is currently restricted to selecting a single Page 🤨.
There's also the Path Selector. It works just like the Page Selector, returning the selected Page's NodeAliasPath
. It is also limited to 1 item.
Given the restriction both these selectors share, we could add multiple selectors to our Widget Properties class, 1 for each Page we wanted the Widget to have access to - but this obviously won't scale and requires a code change to update 😟.
There are more advanced options as well, like creating a modal dialog where we could build out a complex custom UI for selecting multiple Pages, but it seems like there should be an easier solution 😒!
Solution - Use Page Relationships
When we want a Widget (or a Page View) to display information from a select set of Pages, we are effectively creating a relationship between the current Page using the Widget and the Pages we want to be the source of content.
While the Page and Path Selector Form Components enable defining relationships like these, the process of defining relationships between content might better be thought of as part of the content management process 🤔.
Xperience already has many tools for performing content management outside of the Page Builder 😮.
⚠ Any time you find yourself doing content creation or content management in a Widget, pause and ask yourself if there is already a way to do this outside the Page Builder - it can often be a better choice.
Instead of doing our content management in the Widget, we can use the Page Relationships feature of Xperience.
When defining content or content relationships in a Widget, that work is isolated in the Widget. By defining Page Relationships on the Page itself, those relationships can be used by multiple Widgets or by any other part of Xperience. They are reusable 💪🏽!
💠 I'll be using the Kentico Xperience 13.0 Dancing Goat demo site running on ASP.NET Core for the example below. You can install a trial by downloading the Kentico Xperience Installation Manager.
Create a Relationship
To start down the path of using Related Pages with Widgets, we first need to create a Relationship by going to the Relationship Names module in Xperience.
I like to name my relationships with names that read like sentences. This helps me later, when I'm relating pages to each other. If the Relationship 'reads' correctly then I've set it up correctly 👍🏼.
Here, we name the Relationship HasArticle
because a given Page will 'have an article' (or more than 1) it is related to.
Add Related Pages
Now, we navigate to the Pages module and select the Page in the Content Tree that we want to use our Relationship with.
In this case we will select the Home Page and then select the "Related Pages" option from the menu.
Next, we create a couple Relationships between the Home Page and Article Pages.
Make sure that the Home Page is on the left side of the Relationship and each the Article Page is on the right. Again, the relationship should 'read' like a sentence - "Home has an Article On Roasts".
Query for Our Content
Now that we've performed all of our content management duties, we can get to coding 🥳! Fortunately, the Dancing Goat site already has most of the pieces that we need 😎.
First, we'll add a method to the ArticlesRepository
found under ~/Models/Articles/
.
public IEnumerable<Article> GetRelated(int count = 0)
{
var page = pageDataContextRetriever.Retrieve<TreeNode>().Page;
string key = $"{nameof(ArticleRepository)}|{nameof(GetRelated)}|{page.NodeGUID}|{count}";
string relDependency = $"nodeid|{page.NodeID}|relationships";
return pageRetriever.Retrieve<Article>(
query => query
.InRelationWith(
page.NodeGUID,
"HasArticle",
RelationshipSideEnum.Right)
.TopN(count)
.OrderByDescending("DocumentPublishFrom"),
cache => cache
.Key(key)
.Dependencies((_, builder) =>
builder
.Pages(new[] { page })
.Custom(relDependency)));
}
GetRelated()
looks very similar to the already existing GetArticles()
method, with the main difference being that it uses the pageDataContextRetriever
to get the current Page and then queries for Article
pages related to that Page.
Notice the .InRelationWith()
method we use to query for related pages 🧐.
public virtual TQuery InRelationWith(
Guid nodeGuid,
string relationshipName = null,
RelationshipSideEnum side = RelationshipSideEnum.Both);
One thing that often trips me up is not knowing which option of the RelationshipSideEnum
to use 😵.
So, just to clarify, the RelationshipSideEnum
value should be the side we expect the Page to be on that is supplying the nodeGuid
parameter, the results being returned by the query will be on the other side.
Our query can be read as "Find me all the Articles where the Page identified by the nodeGuid
is on the Left side of the HasArticle
relationship" 🤓.
Notice also that we define a custom cache dependency on all the relationships for the current page.
string relDependency = $"nodeid|{page.NodeID}|relationships";
Page Relationships are stored in a database table separate from the Page's primary content 😮, so we need to be explicit that this query's cache should be cleared if any of the Page's relationships change.
Reuse the Widget
Now that we've defined a new query for getting our content, let's add on to the existing "Latest articles" Widget, which can be found under ~/Components/Widgets/Articles/
.
First, we will add a new UseRelatedPages
property to the ArticlesWidgetProperties
class. It will be a property that is editable in the Widget's property dialog, so we'll use the EditingComponent
attribute to define the form element for editing this property.
Note: Adding
ExplanationText
is always a good idea. Although the purpose of a field might be clear to us developers (because we're building these components), it might not be clear at all to a content editor. Let's help them out with a nice description 🤗!
public class ArticlesWidgetProperties : IWidgetProperties
{
public int Count { get; set; } = 5;
[EditingComponent(
CheckBoxComponent.IDENTIFIER,
Order = 1,
Label = "Use Related Pages?",
ExplanationText = "Changes the Widget to display Articles related to the Current Page")]
public bool UseRelatedPages { get; set; } = false;
}
We want to pass the new UseRelatedPages
property to our View, so we'll also add it to the ArticlesWidgetViewModel
.
public class ArticlesWidgetViewModel
{
public IEnumerable<ArticleViewModel> Articles { get; set; }
public int Count { get; set; }
public bool UseRelatedPages { get; set; }
}
Finally, we can pull everything together in the ArticlesWidgetViewComponent
's Invoke()
method.
We will get our Article Pages from the GetRelated()
method if UseRelatedPages
is true
, otherwise we will get them from GetArticles()
.
public ViewViewComponentResult Invoke(
ComponentViewModel<ArticlesWidgetProperties> viewModel)
{
if (viewModel is null)
{
throw new ArgumentNullException(nameof(viewModel));
}
var articles = viewModel.Properties.UseRelatedPages
? repository.GetRelated(
viewModel.Properties.Count)
: repository.GetArticles(
ContentItemIdentifiers.ARTICLES,
viewModel.Properties.Count);
// ...
return View(
"~/Components/Widgets/Articles/_ArticlesWidget.cshtml",
new ArticlesWidgetViewModel
{
Articles = articlesModel,
Count = viewModel.Properties.Count,
UseRelatedPages = viewModel.Properties.UseRelatedPages
});
}
To wrap up our code changes, we will update the _ArticlesWidget.cshtml
View by making the Widget's title contextual, changing it based on the value of UseRelatedPages
.
@{
var i = 1;
var title = Model.UseRelatedPages
? HtmlLocalizer["Related article"]
: HtmlLocalizer["Latest article"];
}
<!-- ... -->
<div class="articles-section">
<div class="row">
<div class="title-wrapper">
<h1 class="title-tab">@title</h1>
</div>
<!-- ... -->
That's it 👍! Now we can test our Widget and toggle the source of the Articles that are displayed on the Page.
Conclusion
As you can see, we now have the option to source the Article Pages that our Widget displays either from Articles related to the current page, or by listing out the Articles from the Content Tree.
Using Related Pages allows us to unlock 🔓 content relationships.
We don't always have to focus on where content is located in the content tree, or what kind of Page we are currently adding Widgets to.
Related Pages can make our Widgets more flexible and our content 😁.
Where are you using Related Pages in your Kentico Xperience sites?
Let me know in the comments 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)