DEV Community

Cover image for Kentico Xperience: MVC Widget Experiments Part 1 - Widgets And Related Pages
Sean G. Wright
Sean G. Wright

Posted on • Edited on

Kentico Xperience: MVC Widget Experiments Part 1 - Widgets And Related Pages

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.

Alt Text

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.

Alt Text

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.

Alt Text

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

Alt Text

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)));
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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"; 
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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
        });
}
Enter fullscreen mode Exit fullscreen mode

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>

<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

That's it ๐Ÿ‘! Now we can test our Widget and toggle the source of the Articles that are displayed on the Page.

Alt Text

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 ๐Ÿ™!


Photo by Kelly Sikkema 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:

#kentico

Or my Kentico Xperience blog series, like:

Top comments (0)