Blog about tips & tricks for CMS enhancement

eric.petersson

Custom tool to List pages by their page type


Summary

Today's blog post will be demonstrating to build a practical use of the tools section in Episerver.

Quite often your client or you would like to get an overview of how many pages of each page type there has been created. Or simply what page that belong to their page type. This may be achieved by creating a custom tool and looping through the Episerver content repository for the given page types and select each page created of that type.

The result of this blog post will look something like this:

Overview of List pages by page type

All available page types (except SysRoot and SysRecycleBin) will be available to choose from given in a dropdown control. When the user selects a page type of his/her choice we will update the view result with the help of ajax and populate the given data of the pages whom are to be displayed as well as creating a pagination for usability of stepping between the result.

Files and requirements

Files at GitHub: click here

CMS compatibility: 9.x and 10.x

Requirements: MVC, jQuery

Steps to follow

To hook into the tools section of Episerver, the following controller will be created and decorated with the GuiPlugIn attribute. This way we may set the Area to live under the admin tools section as well as giving our mvc path a custom url to follow with Url property.

ListPagesByPageTypesController.cs

[GuiPlugIn(
        DisplayName = "[Custom] List Pages by Page Types",
        Description = "Lists all pages by filtering their belonging page type.",
        SortIndex = 100,
        Area = PlugInArea.AdminMenu,
        Url = "/custom-plugins/list-pages-by-page-types")]
    [Authorize(Roles = "CmsAdmins")]
    public class ListPagesByPageTypesController : Controller
    {

    }

To fully let Episerver hook up with our given url we shall also create a custom route. For the sake of easiness, we will create a seperate .cs-file and inject the path with the InitializableModule attribute. This way we may map the routes to our newly created /custom-plugins/list-pages-by-page-types path. We will also prepare the parameters of id and page since we will later on be retreiving the pages by the page type id given, and filter pages by the given page number for the pagination.

It should look like the following:

CustomRouteInitialization.cs

[InitializableModule]
    public class CustomRouteInitialization : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            RouteTable.Routes.MapRoute("Default",
                "custom-plugins/list-pages-by-page-types/{action}/{id}/{page}",
                new { controller = "ListPagesByPageTypes", action = "Index", id = UrlParameter.Optional, page = UrlParameter.Optional });
        }

        public void Uninitialize(InitializationEngine context)
        {
        
        }
    }

In the Views folder in the project create an Index.cshtml file and place the following:

Index.cshtml

@inherits System.Web.Mvc.WebViewPage<Episerver_Playground.Business.Plugins.Admin.ListPagesByPageTypes.ListPagesViewModel>
@using System.Web.Mvc.Html
@using EPiServer.DataAbstraction

@{
    Layout = null;
}

@Html.Partial("~/Views/Shared/EpiResources/_SystemStyles.cshtml")
@Html.Partial("~/Business/Plugins/Admin/ListPagesByPageTypes/Views/_MainScript.cshtml")

<div class="epi-contentContainer epi-padding">
    <label for="pagetype-selector">Choose a page type:</label>
    <select id="pagetype-selector">
        @foreach (PageType pageType in Model.PageTypesList)
        {
            <option id="@pageType.ID">
                @(!string.IsNullOrEmpty(pageType.DisplayName) ? pageType.DisplayName : pageType.Name)
            </option>
        }
    </select>
    <div id="page-result"></div>
</div>

The _SystemStyles.cshtml contains all of the necessary Episerver styles in order to achieve Episerver's included styles. However, we will not have all of the admin features in this includement, which we will see later on.

On to the model! We will have the following in our ViewModel called

ListPagesViewModel.cs

 public class ListPagesViewModel : Pagination
    {
        public IEnumerable PagesList { get; set; }
        public IEnumerable PageTypesList { get; set; }
    }

    public class Pagination
    {
        public int CurrentPage { get; set; }
        public int StartPage { get; set; }
        public int EndPage { get; set; }
        public int TotalPages { get; set; }
        public int NumberOfPages { get; set; }
        public int PageSize { get; set; }
    }

Go back to the controller and insert the following methods and logic:

ListPagesByPageTypesController.cs

[GuiPlugIn(
        DisplayName = "[Custom] List Pages by Page Types",
        Description = "Lists all pages by filtering their belonging page type.",
        SortIndex = 100,
        Area = PlugInArea.AdminMenu,
        Url = "/custom-plugins/list-pages-by-page-types")]
    [Authorize(Roles = Constants.Authorization.CmsAdmins)]
    public class ListPagesByPageTypesController : Controller
    {
        private readonly int _pageSize;

        public ListPagesByPageTypesController() : base()
        {
            _pageSize = 20;
        }

        public ActionResult Index()
        {
            var pageTypes = GetAllPageTypes();

            var model = new ListPagesViewModel
            {
                PageTypesList = pageTypes
            };
            
            return View("~/Business/Plugins/Admin/ListPagesByPageTypes/Views/Index.cshtml", model);
        }

        /// <summary>
        /// Action result for viewing partial model of List pages by page types
        /// </summary>
        /// <param name="id">page type id</param>
        /// <param name="page">page id</param>
        /// <returns>Pages by their page type</returns>
        public PartialViewResult LoadPagesFromPageType(int id, int page)
        {
            var currentPage = page != 0 ? page : 1;
            var startPage = currentPage - 10;
            var endPage = currentPage + 11;

            var pages = GetAllPagesOfPageType(id).Skip(_pageSize * (page - 1)).Take(_pageSize).ToList();
            var totalCount = GetAllPagesOfPageType(id).Count();
            var numberOfPages = Convert.ToInt32(Math.Ceiling((double)totalCount / _pageSize));
           
            if (startPage <= 0)
            {
                endPage -= (startPage - 1);
                startPage = 1;
            }
            if (endPage > numberOfPages)
            {
                endPage = numberOfPages;
                if (endPage > 20)
                {
                    startPage = endPage - 21;
                }
            }

            ListPagesViewModel viewModel = new ListPagesViewModel
            {
                PageSize = _pageSize,
                TotalPages = totalCount,
                NumberOfPages = numberOfPages,
                CurrentPage = page,
                PagesList = pages,
                StartPage = startPage,
                EndPage = endPage,

            };

            return PartialView("~/Business/Plugins/Admin/ListPagesByPageTypes/Views/_Pages.cshtml", viewModel);
        }

        /// <summary>
        /// Method for getting all page types, excluded SysRoot and SysRecycleBin
        /// </summary>
        /// <returns>Gets all page types defined in Episerver</returns>
        private static IEnumerable<PageType> GetAllPageTypes()
        {
            var contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
            var sysRoot = contentTypeRepository.Load("SysRoot") as PageType;
            var sysRecycleBin = contentTypeRepository.Load("SysRecycleBin") as PageType;

            return contentTypeRepository.List().OfType<PageType>()
                .Where(t => t != sysRoot)
                .Where(t => t != sysRecycleBin)
                .OrderBy(t => t.DisplayName);
        }

        /// <summary>
        /// Method for retrieving all pages passed by a page type id
        /// </summary>
        /// <returns>Returns pages for each page type given by pagetypeid</returns>
        private static IEnumerable<PageData> GetAllPagesOfPageType(int id)
        {
            var pageTypeId = id;
            var repository = ServiceLocator.Current.GetInstance<IPageCriteriaQueryService>();

            var pageTypeCriteria = new PropertyCriteria
            {
                Name = "PageTypeID",
                Type = PropertyDataType.PageType,
                Value = pageTypeId.ToString(),
                Condition = CompareCondition.Equal,
                Required = true
            };

            var criteria = new PropertyCriteriaCollection
            {
                pageTypeCriteria
            };

            var currentCulture = ContentLanguage.PreferredCulture;
            var languageBranch = currentCulture.Name;

            var pageDataCollection = repository.
                FindAllPagesWithCriteria(ContentReference.RootPage, 
                criteria, 
                languageBranch, 
                LanguageSelector.AutoDetect(true));

            if (pageDataCollection != null)
            { 
                var sortedPageDataCollection = pageDataCollection.OrderByDescending(c => c.StartPublish);

                return sortedPageDataCollection;
            }

            return pageDataCollection;
        }
    }

The Action result of index will be a view result and model of the page types created within your Episerver solution. In the GetAllPageTypes method we will skip the SysRoot and SysRecycleBin since... well, you may enable them if you'd like to, but I won't. The result will be sorted by the DisplayName of the page types, if there are any given.

Eventually we will reach the LoadPagesFromPageType method, with jQuery ajax callbacks and retrieving the id and page we specified as optional parameters earlier. The _MainScript.cshtml contains the scripts for making the handshake to the MVC method and retrieving the data. It looks like this:

_MainScript.cshtl

@inherits System.Web.Mvc.WebViewPage

<script src="~/Business/Plugins/Admin/ListPagesByPageTypes/Assets/jquery-3.2.1.min.js" type="text/javascript"></script>

<script type="text/javascript">
    $(function () {
        let selector = $("#pagetype-selector");
        let result = $("#page-result");
        let pagination = $(".page-number");
        let initId = selector.children(":first").attr("id");
        let callback = '@Url.Action("LoadPagesFromPageType", "ListPagesByPageTypes")';

        showLoadingIcon();
        result.load(callback + "?id=" + initId + "&page=1");

        selector.change(function () {
            let optionId = $(this).children(":selected").attr("id");
            showLoadingIcon();
            result.load(callback + "?id=" + optionId + "&page=1");
        });

        function showLoadingIcon() {
            return result.html('<img src="@Url.Content("~/Business/Plugins/Admin/ListPagesByPageTypes/Assets/loading.gif")" /><div>Loading pages...</div>');
        }

    });
</script>

As you may see we refer to the jQuery library 3.2.1.min because of laziness. You should if you have the time write this in regular JavaScript, but for effortness this is what we will be using.

In this file we pick up the selector's content by the page type id which we were looping through earlier in the Index.cshtml and pass along the route with the optional parameters with the help of @Url.Action("methodName", "controllerName"). By the initial load of the index view we will send the &page=1 alongside with the initial page type id to give the user the preselected value in the dropdown control. The OnChange of the dropdown will be sending the given id and page to our LoadPagesByPageType method.

We will also display a loading icon if the initial result would be of the overkill size.

By passing the id and page parameters to the method, we will filter for the pages beloning to the page type with the help of the IPageCriteriaQueryService, property criterias, FindAllPagesWithCriteria and go through the root page of the site. We will also pass along the current culture to both define the languages of the source in Episerver and to retrieve all of the unpublished children as well.

Once we come back to the PartialViewResult of loading the pages from the paget type we will also setup the pagination logics and inherit the members and adding the corresponding data to our view model.

As a final action, we return the partial view _Pages.cshtml and updated that view with the pages and filter them by 20 pages at each page given. The _Pages.cshtml also contains its own script section _PartialScript.cshtml since we need to reuse more or less the same logics provided in the _MainScript.cshtml. The view result and its script look like this:

_Pages.cshtml

@inherits System.Web.Mvc.WebViewPage<Episerver_Playground.Business.Plugins.Admin.ListPagesByPageTypes.ListPagesViewModel>
@using System.Web.Mvc.Html
@using EPiServer.Core
@using EPiServer.Editor

@Html.Partial("~/Business/Plugins/Admin/ListPagesByPageTypes/Views/_PartialScript.cshtml")

<p>Found <strong>@Model.TotalPages page@(Model.TotalPages > 1 ? "s" : string.Empty)</strong></p>

<table class="epi-default">
    <tbody>
        <thead>
            <tr>
                <th class="epitableheading" scope="col">Status</th>
                <th class="epitableheading" scope="col">Page name</th>
                <th class="epitableheading" scope="col">Publish date</th>
                <th class="epitableheading" scope="col">Edit page</th>
            </tr>
        </thead>

        @foreach (PageData page in Model.PagesList)
        {
        <tr>
            <td>@page.Status</td>
            <td>@(page.Name.IsEmpty() ? "-" : page.Name)</td>
            <td>@(page.StartPublish.HasValue ? page.StartPublish.Value.ToString("yyyy-MM-dd") : "-")</td>
            <td><a href="@PageEditing.GetEditUrl(page.ContentLink)" target="_blank" class="epi-visibleLink">@(PageEditing.GetEditUrl(page.ContentLink) != null ? "Edit" : "")</a></td>
        </tr>
        }

        @if (Model.NumberOfPages > 1)
        {
            @Html.Partial("~/Business/Plugins/Admin/ListPagesByPageTypes/Views/_Pagination.cshtml")
        }
        
    </tbody>
</table>

<small>Viewing page @Model.CurrentPage of @Model.NumberOfPages</small>

_PartialScript.cshtml

@inherits System.Web.Mvc.WebViewPage

<script src="~/Business/Plugins/Admin/ListPagesByPageTypes/Assets/jquery-3.2.1.min.js" type="text/javascript"></script>

<script type="text/javascript">
    $(function () {
        let pagination = $(".page-number");
        let selector = $("#pagetype-selector");
        let result = $("#page-result");
        let optionId = selector.children(":selected").attr("id");
        let callback = '@Url.Action("LoadPagesFromPageType", "ListPagesByPageTypes")';

        pagination.click(function () {
            let page = parseInt($(this).attr("data-id"));
            result.load(callback + "?id=" + optionId + "&page=" + page);
        });
    });
</script>

_Pagination.cshtml

@inherits System.Web.Mvc.WebViewPage<Episerver_Playground.Business.Plugins.Admin.ListPagesByPageTypes.ListPagesViewModel>

<tr class="epipager">
    <td colspan="4" style="background-image: url('/Business/Plugins/Admin/ListPagesByPageTypes/Assets/epi-table-pagination.gif');background-color: #fff; background-repeat: repeat-x; padding: 0.1em 0.4em;">
        @if (Model.CurrentPage > 1)
        {
            <a class="page-number" style="font-weight:bold; padding: 0 0.1em; margin-left: 0.1em;" data-id="@Model.StartPage" href="#">&#171; First</a>
        }
        @for (int i = Model.StartPage; i <= Model.EndPage; i++)
        {
            if (i == Model.CurrentPage)
            {
                <span style="font-weight: bold; padding: 0 0.1em; margin-left: 0.1em;">@i</span>
            }
            else
            {
                <a style="font-weight:bold; padding: 0 0.1em; margin-left: 0.1em;" class="page-number" data-id="@i" href='#'>@i</a>
            }
        }

        @if (Model.CurrentPage < Model.NumberOfPages)
        {
            <a class="page-number" style="font-weight:bold; padding: 0 0.1em; margin-left: 0.1em;" data-id="@Model.EndPage" href="#">Last &#187;</a>
        }
    </td>
</tr>

In the pagination file I styled inline since there was just a background image and some smaller margins/padding and font size missing to achieve Episerver's default styles.

That should be it. You will now have the ability to list pages belonging to their page type!

Something not working the way it suppose to? Head over to GitHub and fork the repository!