Extending the Hierarchical Viewer

20 MINUTE READ / BY KILE LINDGREN

Kentico's hierarchical viewer is a very flexible control. I have used it to create everything from simple listing pages to complicated mega menus. So when I needed to create another parent/child listing page, my first thought was to use the hierarchical viewer. In this instance, however, I quickly ran into an unexpected limitation.

The Problem

The design for this page called for the parent page to be listed as a category header and child items to be listed in a three-column grid below it. The page could list multiple categories with two or more children items showing underneath the header. Getting the parent pages to show up with their children beneath them was straightforward. The problem came when I tried to break the children pages into a three-column grid.

At first, I tried using the DataItemIndex property like I would for a normal repeater transformation, hoping it would reset each time the hierarchical viewer rendered each level. I added the following line of code to the bottom of the child-item transformation:

<%# IfTrue(((DataItemIndex+1) % 3 == 0 && !IsLast()),"</div><div class=\"row\">") %>

This approach seemed to work at first but then I added a few more child items and the layout broke. Upon further inspection, the DataItemIndex and DataItemCount properties were returning the index position and count for the ENTIRE dataset and not just the level currently being rendered.

After an hour of searching and reading Kentico’s documentation, I couldn’t find any built-in methods or attributes that would get the current items’ position relative to its level, even though this information had to exist for the first, last, single, and alternate hierarchical transformations to work. So I opened the CMS.Controls.dll in the excellent (and free) Telerik JustDecompile and navigated to the CMSUniView control to see how it worked. My search turned up the GenerateInnerHierarchicalContent method in the UniView class and, after a little trial and error, I had a workable solution.

Adding Custom Methods to Transformations

In this solution, the first thing we needed to do is extend the CMSTransformation class to add our additional methods and properties and make them accessible in our hierarchical transformation. Kentico provides documentation on how to do exactly this. To start, create a new C# class under your App_Code (or CMSApp_AppCode -> Old_App_Code folder if you installed the project as a web application) and name it CMSTransformation.cs. Remove the default content of the class file and replace it with the code below.

using System;using System.Web.UI;using System.Collections;using CMS.Base;using CMS.Helpers;namespace CMS.Controls{    /// <summary>    /// Extends the CMSTransformation partial class.    /// </summary>    public partial class CMSTransformation    {   //Add custom code here    }}

Accessing the Datasource Property

The next thing we need to do is access the hierarchical viewer’s Datasource property. To do so, we need to recursively navigate the parent property until we find the UniView control. Once we have the UniView control, we can access the GroupedDataSource assigned to its Datasource property.

/// <summary>/// Returns the GroupedDataSource from a UniView control/// </summary>private GroupedDataSource GetUniViewDataSource(){    try    {        var uniView = FindUniViewControl(this.Parent.Parent);        if (uniView == null)            return null;        return (GroupedDataSource)uniView.DataSource;    }    catch { }    return null;}/// <summary>/// Finds the UniView control by crawling the control hierarchy/// </summary>/// <param name="control">The control</param>private UniView FindUniViewControl(Control control){    if (control != null)    {        if (control is UniView)            return (UniView)control;        if (control.Parent != null)            return FindUniViewControl(control.Parent);    }    return null;}

After we get the GroupedDataSource, we can call its GetItems method passing in a parent id to retrieve a list of child items under that parent. In order to figure out what our parent id is, we will first try and use the GroupedDataSources’ ColumnName property. If that fails, we will revert back to ParentNodeId, which is the default for GroupedDataSource when interacting with the content tree. (I am also providing an overload here in case you want to provide your own parent id.)

/// <summary>/// Returns list of children items from the current levels parent/// </summary>public IList GetLevelDataList(){    try    {        var dataSource = GetUniViewDataSource();                if (dataSource == null)            return null;        var parentid = -1;        try {            parentid = ValidationHelper.GetInteger(Eval(dataSource.ColumnName), -1);        }        catch {            parentid = ValidationHelper.GetInteger(Eval("NodeParentId"), -1);        }        return dataSource.GetItems(parentid);    }    catch { }    return null;}/// <summary>/// Returns list of children items from the provided parent node id/// </summary>/// <param name="parentid">Parents Node Id</param>public IList GetLevelDataList(int parentid){    try    {        var dataSource = GetUniViewDataSource();        if (dataSource == null)            return null;        return dataSource.GetItems(parentid);    }    catch { }    return null;}

Putting It All Together

Now that we can get a list of child items for a parent we needed to create two properties to fill in our missing functionality. I called these DataItemLevelIndex and DataItemLevelCount after the existing DataItemIndex and DataItemCount properties. Getting the DataItemLevelIndex involves a simple call to the IndexOf() method on the returned list. To get the DataItemLevelCount, we just needed to call the Count property on the returned list. Notice that I set both the index and count at the same time since we had to do a bunch of extra processing to get the list of child items in the first place and often use these two properties together.

private int _DataItemLevelIndex = -1;private int _DataItemLevelCount = -1;/// <summary>/// Returns index position for its level/// </summary>public int DataItemLevelIndex{    get    {        if(_DataItemLevelIndex < 0)        {            //get the list of items for this level            var list = GetLevelDataList();            if (list == null) {                 _DataItemLevelIndex = 0;                _DataItemLevelCount = 0;            }            else            {                _DataItemLevelIndex = list.IndexOf(this.DataItem);                _DataItemLevelCount = list.Count;            }        }        return _DataItemLevelIndex;    }}/// <summary>/// Returns count of items in its level/// </summary>public int DataItemLevelCount{    get    {        if (_DataItemLevelCount < 0)        {            //get the list of items for this level            var list = GetLevelDataList();            if (list == null)            {                _DataItemLevelCount = 0;                _DataItemLevelIndex = 0;            }            else            {                _DataItemLevelIndex = list.IndexOf(this.DataItem);                _DataItemLevelCount = list.Count;            }        }        return _DataItemLevelCount;    }}

Rounding It Out

In Version 8, Kentico added the incredibly useful utility methods IsFirst() and IsLast(). To get similar functionality, I added some additional utility methods that build on the work above.

  • IsLevelFirst() – Returns true if it is the first sibling item.

  • IsLevelLast() – Returns true if it is the last sibling item.

  • IsLevelModulus (int divisor, int remainder = 0, bool ignoreIfFirst = false, bool ignoreIfLast = false) – Returns true if (DataItemLevelIndex +1) % divisor == remainder. The last two Boolean parameters tell the function to ignore the modulus comparison (return false) if the item is first or last. This is useful to avoid actions involving the first or last item.

    /// <summary>/// Returns true if control is the first item in its level/// </summary>public bool IsLevelFirst(){ return (DataItemLevelIndex == 0);}/// <summary>/// Returns true if control is the last item in its level/// </summary>public bool IsLevelLast(){ return (DataItemLevelIndex + 1 == DataItemLevelCount);}/// <summary>/// Returns true if control is the first item in its level/// </summary>/// <param name="divisor">Number to take the modulo of the index (DataItemLevelIndex + 1) % divisor == remainder</param>/// <param name="remainder">Number to compare the remainder too (DataItemLevelIndex + 1) % divisor == remainder</param>/// <param name="ignoreIfFirst">If true, return false if the item is first (default is false)</param>/// <param name="ignoreIfLast">If true, return false if the item is last(default is false)</param>public bool IsLevelModulus(int divisor, int remainder = 0, bool ignoreIfFirst = false, bool ignoreIfLast = false){ if (ignoreIfFirst && DataItemLevelIndex == 0) { return false; } else if (ignoreIfLast && (DataItemLevelIndex + 1) == DataItemLevelCount) { return false; } return ((DataItemLevelIndex + 1) % divisor == remainder);}

The Final Product

using System;using System.Web.UI;using System.Collections;using CMS.Base;using CMS.Helpers;namespace CMS.Controls{    /// <summary>    /// Extends the CMSTransformation partial class.    /// </summary>    public partial class CMSTransformation    {    private int _DataItemLevelIndex = -1;        private int _DataItemLevelCount = -1;        /// <summary>        /// Returns index position for its level        /// </summary>        public int DataItemLevelIndex        {            get            {                if(_DataItemLevelIndex < 0)                {                    //get the list of items for this level                    var list = GetLevelDataList();                            if (list == null) {                         _DataItemLevelIndex = 0;                        _DataItemLevelCount = 0;                    }                    else                    {                        _DataItemLevelIndex = list.IndexOf(this.DataItem);                        _DataItemLevelCount = list.Count;                    }                }                return _DataItemLevelIndex;            }        }                /// <summary>        /// Returns count of items in its level        /// </summary>        public int DataItemLevelCount        {            get            {                if (_DataItemLevelCount < 0)                {                    //get the list of items for this level                    var list = GetLevelDataList();                            if (list == null)                    {                        _DataItemLevelCount = 0;                        _DataItemLevelIndex = 0;                    }                    else                    {                        _DataItemLevelIndex = list.IndexOf(this.DataItem);                        _DataItemLevelCount = list.Count;                    }                }                return _DataItemLevelCount;            }        }        /// <summary>        /// Returns true if control is the first item in its level        /// </summary>        public bool IsLevelFirst()        {            return (DataItemLevelIndex == 0);        }        /// <summary>        /// Returns true if control is the last item in its level        /// </summary>        public bool IsLevelLast()        {            return (DataItemLevelIndex + 1 == DataItemLevelCount);        }        /// <summary>        /// Returns true if control is the first item in its level        /// </summary>        /// <param name="divisor">Number to take the modulo of the index (DataItemLevelIndex + 1) % divisor == remainder</param>        /// <param name="remainder">Number to compare the remainder too (DataItemLevelIndex + 1) % divisor == remainder</param>        /// <param name="ignoreIfFirst">If true, return false if the item is first (default is false)</param>        /// <param name="ignoreIfLast">If true, return false if the item is last(default is false)</param>        public bool IsLevelModulus(int divisor, int remainder = 0, bool ignoreIfFirst = false, bool ignoreIfLast = false)        {            if (ignoreIfFirst && DataItemLevelIndex == 0)            {                return false;            }            else if (ignoreIfLast && (DataItemLevelIndex + 1) == DataItemLevelCount)            {                return false;            }            return ((DataItemLevelIndex + 1) % divisor == remainder);        }/// <summary>        /// Returns list of children items from the current levels parent        /// </summary>        public IList GetLevelDataList()        {            try            {                var dataSource = GetUniViewDataSource();                                if (dataSource == null)                    return null;                var parentid = -1;                try {                    parentid = ValidationHelper.GetInteger(Eval(dataSource.ColumnName), -1);                }                catch {                    parentid = ValidationHelper.GetInteger(Eval("NodeParentId"), -1);                }                return dataSource.GetItems(parentid);            }            catch { }            return null;        }        /// <summary>        /// Returns list of children items from the provided parent node id        /// </summary>        /// <param name="parentnodeid">Parents Node Id</param>        public IList GetLevelDataList(int parentid)        {            try            {                var dataSource = GetUniViewDataSource();                if (dataSource == null)                    return null;                return dataSource.GetItems(parentid);            }            catch { }            return null;        }/// <summary>        /// Returns the GroupedDataSource from a UniView control        /// </summary>        private GroupedDataSource GetUniViewDataSource()        {            try            {                var uniView = FindUniViewControl(this.Parent.Parent);                if (uniView == null)                    return null;                return (GroupedDataSource)uniView.DataSource;            }            catch { }            return null;        }        /// <summary>        /// Finds the UniView control by crawling the control hierarchy        /// </summary>        /// <param name="control">The control</param>        private UniView FindUniViewControl(Control control)        {            if(control != null) {                 if (control is UniView)                    return (UniView)control;                if (control.Parent != null)                    return FindUniViewControl(control.Parent);            }            return null;        }    }}

Putting It to Use

Once I had the extra transformation methods, I adjusted the hierarchical transformation to use the new IsLevelModulus method to figure out when to break the columns to the next row. The image below shows the results of the transformation, the output of the built transformation methods, and our extended transformation methods.

You can download the final CMSTransformation.cs file here. This has been tested on Kentico 8.2 and 9, but would probably work on earlier version of Kentico as well.

If you find this information useful and would like to see Kentico include it by default, please vote on my uservoice ticket.


The Future of Kentico

Kentico 2020 - and the MVC model - are bringing big changes.

Kentico recommends upgrading now to MVC, to be ready for Kentico 2020. But is that big investment really necessary? Find out from our Kentico experts.

Download "The Future of Kentico" today.