Allow Single Instance of the page type in Episerver CMS

When we are working with the Episerver website there are few page types that we want Content editors to create only one instance of the page. Such as the “Search result page”. We only want one search result page on the whole website. Few similar examples are;

  • Home Page (Start Page)
  • Checkout Page
  • Basket Page
  • Blog Listing page
  • Site Setting Page

In order to fulfill this requirement, first of all, I have created a custom attribute called “SingleInstancesAttribute”

using System;

namespace Foundation.Cms.Attributes
{
    [AttributeUsage(AttributeTargets.Class)]
    public class SingleInstancesAttribute : Attribute
    {
        public enum InstanceScope
        {
            Site,
            SameContentTree,
        }
        public InstanceScope Scope { get; set; }
    }
}

As you can see I’m using AttributeUsage attribute from System class. This is because I want to control the manner in which it is been used. For example, the indicated attribute class must derive from Attribute, either directly or indirectly. You can check detailed documentation and other options on the Microsoft website.

I have also defined the scope element of this attribute.

Site Scope: The instance of page type can not be created more than once on whole website

SameContentTree Scope: The instance of page type can not be created more than once on the same content tree. Such as their ParentLink can not be the same.

Now I can add this attribute on those page types that I want only one instance. In the below example, I have applied this attribute on “SearchResultPage” page type of the Episerver Foundation example site.

using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Foundation.Cms.Attributes;

namespace Foundation.Cms.Pages
{
    [ContentType(DisplayName = "Search Results Page",
        GUID = "6e0c84de-bd17-43ee-9019-04f08c7fcf8d",
        Description = "Page to allow customer to search the site",
        GroupName = CmsGroupNames.Content)]
    [ImageUrl("~/assets/icons/cms/pages/CMS-icon-page-03.png")]
    
    [SingleInstances(Scope = SingleInstancesAttribute.InstanceScope.Site)]
    
    public class SearchResultPage : FoundationPageData
    {
        [CultureSpecific]
        [Display(Name = "Top content area", Order = 210)]
        public virtual ContentArea TopContentArea { get; set; }

        [CultureSpecific]
        [Display(
Name = "Show recommendations", 
Description = "This will determine whether or not to show recommendations", Order = 220)]
        public virtual bool ShowRecommendations { get; set; }

        public override void SetDefaultValues(ContentType contentType) => ShowRecommendations = true;
    }

    
}

The next step is to create a Validator. The validator will tell Episerver that something needs validation before publishing a page. You can consider it a Pre-Publish event.

In the below validator example, I’m using Episerver find to get all instances of The page type and checking it against Scope of Attribute. You can use Content Loader to do the same (I find Episerver Find is more efficient in such queries)

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using EPiServer.Core;
using EPiServer.Validation;
using Foundation.Cms.Attributes;
using Foundation.Cms.Pages;
using Foundation.Find.Cms;

namespace Foundation.Demo.Validation
{
    public class SingleInstancesValidator : IValidate<PageData>
    {
        private readonly ICmsSearchService _searchService;
        public SingleInstancesValidator(ICmsSearchService seaechService)
        {
            _searchService = seaechService;
        }
        public IEnumerable<ValidationError> Validate(PageData instance)
        {
            var singleInstanceAttribute = instance.GetType().GetCustomAttribute<SingleInstancesAttribute>(true);

            if (singleInstanceAttribute == null)
            {
                return Enumerable.Empty<ValidationError>();
            }

            // call search service to get all existing instances of page type

            var existingInstances = _searchService.SearchByPageType<SearchResultPage>().ToList();

            if (existingInstances.Any())
            {
                if (existingInstances.Count > 0)
                {
                    // if we already have a instance of this page in find then check scope of instance
                    if (singleInstanceAttribute.Scope == SingleInstancesAttribute.InstanceScope.Site)
                    {
                        // Error
                        return new[]
                        {
                            new ValidationError
                            {
                                ErrorMessage =
                                    $"Only one instances of this page type can exist.",
                                PropertyName = "PageType",
                                Severity = ValidationErrorSeverity.Error,
                                ValidationType = ValidationErrorType.StorageValidation
                            }
                        };
                    }
                    else if (singleInstanceAttribute.Scope == SingleInstancesAttribute.InstanceScope.SameContentTree)
                    {
                        if (existingInstances.Any(x => x.ParentLink == instance.ParentLink))
                        {
                            //Error
                            return new[]
                            {
                                new ValidationError
                                {
                                    ErrorMessage =
                                        $"Only one instances of this page type can exist at this level",
                                    PropertyName = "PageName",
                                    Severity = ValidationErrorSeverity.Error,
                                    ValidationType = ValidationErrorType.StorageValidation
                                }
                            };
                        }
                    }
                }
            }
            
            return Enumerable.Empty<ValidationError>();
        }
    }
}

Now if your find index has already crawled all pages of the website and if you start to create another instance of SearchResult page it gives you an error “Only one instance of this page type can exist.” and don’t let you publish the page.

Below is the code of new method I have created in ICmsSearchService to give me all instances of a page type.

public IEnumerable<T> SearchByPageType<T>() where T : PageData
        {
            var productSearch = _findClient.Search<T>();
            productSearch = productSearch.FilterForVisitor();
            return productSearch.GetContentResult();
        }

I have implemented this example on the Episerver Foundation example site. You can find code by visiting following Git repo

https://github.com/nulhaq/EpiserverSingleInstanceValidator

About the author

Naveed Ul-Haq

I'm Naveed. I am a UK based technical architect. I love working with .NET based CMS, eCommerce solutions, .NET Core, DevOps, and Cloud computing. I am a Certified Episerver CMS developer, MCSD (Microsoft Certified Solution Developer) and MCP in Azure application development. I spend my free time with my family and reading books. You can contact me on [email protected]

View all posts

3 Comments

Leave a Reply to Per Nergård Cancel reply

Your email address will not be published. Required fields are marked *