Module: Advanced content
5 of 11 Pages
Filter content with taxonomies
Now, we can implement filtering based on the Article category taxonomy.
Add new widget property
First, let’s add a tag selector to the widget UI so that editors can select the desired tags.
Find the definition of the Article list widget properties in the Training guides repository.
Add a new IEnumerable<TagReference> property called Tags between the existing ContentTreeSection and TopN. Decorate the property with TagSelectorComponent attribute.
Specify allowed taxonomy passing our taxonomy code name, ArticleCategory, as the first parameter.
To refresh your memory on defining widget properties revisit our Build a simple call-to-action widget guide.
// ... existing code ...
public class ArticleListWidgetProperties : IWidgetProperties
{
[ContentItemSelectorComponent(
[
ArticlePage.CONTENT_TYPE_NAME,
DownloadsPage.CONTENT_TYPE_NAME,
EmptyPage.CONTENT_TYPE_NAME,
LandingPage.CONTENT_TYPE_NAME,
ServicePage.CONTENT_TYPE_NAME
],
Label = "Select a parent page to pull articles from",
AllowContentItemCreation = false,
MaximumItems = 1,
Order = 10)]
public IEnumerable<ContentItemReference> ContentTreeSection { get; set; } = [];
// new property definition
[TagSelectorComponent(
"ArticleCategory",
Label = "Filter to categories",
ExplanationText = "Select 0, 1 or more Article Type tags. Shows all if none are selected",
Order = 15)]
public IEnumerable<TagReference> Tags { get; set; } = [];
// ... existing code ...
}
// ... existing code ...
Run the project and navigate to Page Builder mode of any page that contains Article list widget. In the widget configuration you should see the new tag selector control allowing you to pick Animals or Plants tags.
Adjust widget view component
Now, let’s add some logic to the widget view component. It needs to account for the tags the editor has chosen when retrieving content.
Take a look at the ArticleListWidgetViewComponent.cs file.
The InvokeAsync method calls RetrieveArticlePages to fetch the relevant article pages from the Content hub, passing in the editor’s content tree selection.
Then, it orders and further filters the articles before constructing a view model and returning the widget view.
// ... existing code ...
public async Task<ViewViewComponentResult> InvokeAsync(ArticleListWidgetProperties properties)
{
var model = new ArticleListWidgetViewModel();
if (properties.ContentTreeSection.Any())
{
var articlePages = await RetrieveArticlePages(properties.ContentTreeSection.First());
model.Articles = (properties.OrderBy.Equals("OldestFirst", StringComparison.OrdinalIgnoreCase)
? GetArticlePageViewModels(articlePages).OrderBy(article => article.CreatedOn)
: GetArticlePageViewModels(articlePages).OrderByDescending(article => article.CreatedOn))
.Take(properties.TopN)
.ToList();
model.CtaText = properties.CtaText;
}
return View("~/Features/Articles/Widgets/ArticleList/ArticleListWidget.cshtml", model);
}
// ... existing code ...
We’ll adjust the private method RetrieveArticlePages to consider the new Tags property in the following way:
// ... existing code ...
// Add IEnumerable of tag references as a new parameter
private async Task<IEnumerable<ArticlePage>> RetrieveArticlePages(ContentItemReference parentPageSelection, IEnumerable<TagReference> tags)
{
var selectedPageGuid = parentPageSelection.Identifier;
var selectedPage = selectedPageGuid != Guid.Empty
? await contentItemRetrieverService.RetrieveWebPageByContentItemGuid<ArticlePage>(selectedPageGuid)
: null;
string selectedPagePath = selectedPage?.SystemFields.WebPageItemTreePath ?? string.Empty;
if (string.IsNullOrEmpty(selectedPagePath))
{
return [];
}
// if no tags are specified, retrieve the Article pages in the same way as before
if (!tags.Any())
{
return await contentItemRetrieverService.RetrieveWebPageChildrenByPath<ArticlePage>(
selectedPagePath,
3);
}
// otherwise process the tags and retrieve the appropriate pages accordingly
else
{
// code to retrieve article pages based on tags will go here
}
}
// ... existing code ...
Now there is one issue we need to solve:
Our widget expects Article page content items to construct its view model. However, the property holding our taxonomy tags is on the reusable content item linked by the page.
Because of the current limitations of the ContentRetriever API, we will do this in two steps:
- Retrieve the IDs of all reusable content items that implement Article schema and contain the specified tags.
- Retrieve Article page content items on the specified path that link these reusable items.
Implement the service methods
Let’s go implement these service methods in our ContentItemRetrieverService.cs file.
Retrieve reusable content items by schema and tags
The ContentRetriever API has a RetrieveContentOfReusableSchemas extension method that is exactly suited for our scenario.
We will utilize it to retrieve the reusable content items that contain:
- Our Article schema, passing in the schema codename as a parameter (
schemaName) - GUIDs of specific tags, using the WhereContainsTags extension method.
The data we need to perform this query will become additional method parameters:
- the name of the database column where we expect to find the tags (
taxonomyColumnName) - the GUIDs of the required tags (
tagGuids) - the content’s language (
languageName)
Declare the method in the IContentItemRetrieverService interface and implement it in the ContentItemRetrieverService class.
using CMS.ContentEngine;
using Kentico.Content.Web.Mvc;
namespace TrainingGuides.Web.Features.Shared.Services;
public interface IContentItemRetrieverService
{
// ... existing code ...
Task<IEnumerable<IContentItemFieldsSource>> RetrieveContentItemsBySchemaAndTags(
string schemaName,
string taxonomyColumnName,
IEnumerable<Guid> tagGuids,
string? languageName = null);
// ... existing code ...
}
using CMS.ContentEngine;
using CMS.Websites.Routing;
using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;
namespace TrainingGuides.Web.Features.Shared.Services;
public class ContentItemRetrieverService : IContentItemRetrieverService
{
// ... existing code ...
/// <inheritdoc />
public async Task<IEnumerable<IContentItemFieldsSource>> RetrieveContentItemsBySchemaAndTags(
string schemaName,
string taxonomyColumnName,
IEnumerable<Guid> tagGuids,
string? languageName = null)
{
var parameters = new RetrieveContentOfReusableSchemasParameters
{
LanguageName = languageName ?? preferredLanguageRetriever.Get(),
IsForPreview = webSiteChannelContext.IsPreview,
};
return await contentRetriever.RetrieveContentOfReusableSchemas<IContentItemFieldsSource>(
[schemaName],
parameters,
query => query.Where(where => where.WhereContainsTags(taxonomyColumnName, tagGuids)),
RetrievalCacheSettings.CacheDisabled,
configureModel: null);
}
// ... existing code ...
}
Retrieve Article pages by path and reference
Next, we need a method that will fetch all child Article pages on a specific path which also link any of specified reusable content items.
Let’s implement a new public method in the ContentItemRetrieverService class that retrieves pages from a given path which link to the specified content items. This method needs two new parameters beyond the existing path and depth: the code name of the reference field and the IEnumerable of content item IDs.
Call RetrieveWebPageChildrenByPath method and pass in custom query configuration - an anonymous function calling the Linking extension method.
using CMS.ContentEngine;
using CMS.Websites.Routing;
using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;
namespace TrainingGuides.Web.Features.Shared.Services;
public class ContentItemRetrieverService : IContentItemRetrieverService
{
// ... existing code ...
public async Task<IEnumerable<T>> RetrieveWebPageChildrenByPathAndReference<T>(
string parentPagePath,
string referenceFieldName,
IEnumerable<int> referenceIds,
int depth = 1,
string? languageName = null)
where T : IWebPageFieldsSource, new()
=> await RetrieveWebPageChildrenByPath<T>(
path: parentPagePath,
depth: depth,
additionalQueryConfiguration: config => config.Linking(referenceFieldName, referenceIds),
languageName: languageName);
// ... existing code ...
}
// ... existing code ...
Page security
You may notice some methods in the content item retriever service, including RetrieveWebPageChildrenByPath, have a parameter called includeSecuredItems.
This parameter relates to registration and authentication, determining whether to filter out content items for which a visitor does not have permissions.
We’ve left it out of this example, but you can see security integrated into RetrieveWebPageChildrenByPathAndReference and the article list widget in the finished branch of the Training guides repo.
Our membership guides cover the supporting implementation that enables registration, authentication, and secured pages.
Remember to also declare the new method in the IContentItemRetrieverService interface.
using CMS.ContentEngine;
using Kentico.Content.Web.Mvc;
namespace TrainingGuides.Web.Features.Shared.Services;
public interface IContentItemRetrieverService
{
// ... existing code ...
Task<IEnumerable<T>> RetrieveWebPageChildrenByPathAndReference<T>(
string parentPagePath,
string referenceFieldName,
IEnumerable<int> referenceIds,
int depth = 1,
string? languageName = null)
where T : IWebPageFieldsSource, new();
// ... existing code ...
}
// ... existing code ...
See the full ContentItemRetrieverService.cs and IContentItemRetrieverService.cs files in the finished branch of our Training guides repo.
Put it all together
Our backend work is done. Let’s utilize the new methods in the ArticleListWidgetViewComponent.
Find the empty else statement inside the RetrieveArticlePages method. Add logic for the case when the editor did specify the tags to filter by:
// ... existing code ...
private async Task<IEnumerable<ArticlePage>> RetrieveArticlePages(ContentItemReference parentPageSelection, IEnumerable<TagReference> tags)
{
// ... existing code ...
else
{
// first extract the list of tag GUIDs
var tagGuids = tags.Select(tag => tag.Identifier).ToList();
// retrieve the IDs of Article schema reusable content items
// to specify the schema, use the REUSABLE_FIELD_SCHEMA_NAME constant from the generated IArticleSchema interface
// pass in the name of the ArticleSchemaCategory field
var taggedArticleIds = (
await contentItemRetrieverService.RetrieveContentItemsBySchemaAndTags(
IArticleSchema.REUSABLE_FIELD_SCHEMA_NAME,
nameof(IArticleSchema.ArticleSchemaCategory),
tagGuids)
).Select(article => article.SystemFields.ContentItemID);
// retrieve and return Article page content items on specified path that reference the reusable content items
// pass in the name of the ArticlePageArticleContent field, which we use to link reusable article item to Article page
return await contentItemRetrieverService.RetrieveWebPageChildrenByPathAndReference<ArticlePage>(
selectedPagePath,
nameof(ArticlePage.ArticlePageArticleContent),
taggedArticleIds,
depth: 3);
}
}
// ... existing code ...
Content querying approach
You’ll notice that this code first queries the IDs of reusable items associated with the tags, then queries pages that link to those items.
Depending on the number of tagged reusable items and pages in your project, this two-query approach may be faster or slower than retrieving all children of the provided page and filtering them after retrieval, for example, with LINQ.
The approach demonstrated here works better with paging functionality, allowing consistent page counts per batch, while a LINQ approach requires fewer database queries.
In either case, we strongly recommend that you implement caching in production scenarios.
RetrieveArticlePages method from InvokeAsync to retrieve the article pages.
// ... existing code ...
public async Task<ViewViewComponentResult> InvokeAsync(ArticleListWidgetProperties properties)
{
var model = new ArticleListWidgetViewModel();
if (properties.ContentTreeSection.Any())
{
// call the updated method passing in the Tags property
var articlePages = await RetrieveArticlePages(properties.ContentTreeSection.First(), properties.Tags);
model.Articles = (properties.OrderBy.Equals("OldestFirst", StringComparison.OrdinalIgnoreCase)
? GetArticlePageViewModels(articlePages).OrderBy(article => article.CreatedOn)
: GetArticlePageViewModels(articlePages).OrderByDescending(article => article.CreatedOn))
.Take(properties.TopN)
.ToList();
model.CtaText = properties.CtaText;
}
return View("~/Features/Articles/Widgets/ArticleList/ArticleListWidget.cshtml", model);
}
// ... existing code ...
See the results
Rebuild and run your project. Navigate to a page (e.g., Home) where you added the Article list widget. Configure the widget to show articles with different tags, and see the filtered results when you Apply the changes.
Find the complete and working implementation of this example in the finished branch of the Training guides repository. The repository version of the widget also incorporates membership functionality.