A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

Ravindra S. Rathore
Jun 24, 2019
  2610
(8 votes)

Render Episerver Page tree on View

Hi All,

I have a scenario on our website where I need to display the Episerver tree on our view.

Like-

Blog tree

So I thought that we can use the “GetDescendents” method and it will render the tree but I was wrong. It basically gives you all the page items in a single list so you need to build the tree by your own.

So to build the tree I wrote below code-

public class BlogItemTree
    {
        public int PageId { get; set; }

        public int? ParentPageId { get; set; }

        public PageData ContentPage { get; set; }

        public List<BlogItemTree> Children { get; set; }
    }
 
   public static class GenericExtensions
    {
        private static readonly IContentLoader _contentLoader;

        static GenericExtensions()
        {
            _contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
        }
        public static void FindDescendantsOfType<T>(PageData page, ICollection<T> descendants)
            where T : class
        {
            var children = _contentLoader.GetChildren<PageData>(page.PageLink);
            foreach (var child in children)
            {
                if (child is T)
                {
                    descendants.Add(child as T);
                }

                FindDescendantsOfType(child, descendants);
            }
        }

        public static List<BlogItemTree> BlogItemTrees(List<PageData> descendants)
        {
            var blogItemTree = new List<BlogItemTree>();
            foreach (var blogItem in descendants)
            {
                var item = new BlogItemTree() { PageId = blogItem.PageLink.ID, ParentPageId = blogItem.ParentLink.ID };
                blogItemTree.Add(item);
            }

            var tree = blogItemTree.BuildTree();
            return tree;
        }

        public static List<BlogItemTree> BuildTree(this IEnumerable<BlogItemTree> source)
        {
            var groups = source.GroupBy(i => i.ParentPageId);

            var roots = groups.FirstOrDefault().ToList();

            if (roots.Count > 0)
            {
                var dict = groups.Where(g => g.Key.HasValue).ToDictionary(g => g.Key.Value, g => g.ToList());
                for (int i = 0; i < roots.Count; i++)
                {
                    var pageRef = new PageReference(roots[i].PageId);

                    roots[i].ContentPage = _contentLoader.Get<PageData>(pageRef);
                    AddChildren(roots[i], dict);
                }
            }

            return roots;
        }

        private static void AddChildren(BlogItemTree node, IDictionary<int, List<BlogItemTree>> source)
        {
            if (source.ContainsKey(node.PageId))
            {
                node.Children = source[node.PageId];
                var pageRef = new PageReference(node.PageId);
                node.ContentPage = _contentLoader.Get<PageData>(pageRef);
                for (int i = 0; i < node.Children.Count; i++)
                {
                    AddChildren(node.Children[i], source);
                }
            }
            else
            {
                var pageRef = new PageReference(node.PageId);
                node.ContentPage = _contentLoader.Get<PageData>(pageRef);
                node.Children = new List<BlogItemTree>();
            }
        }
    }

And then created one more class to call these functions and get the proper output

public class BlogListByYearBlockService
    {
        public List<ContentPage> Descendants = new List<ContentPage>();

        private readonly IContentLoader _contentLoader;

        public BlogListByYearBlockService(IContentLoader contentLoader)
        {
            _contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
        }

        public List<BlogItemTree> GetTreeData(BlogListByYearBlock block)
        {
            GenericExtensions.FindDescendantsOfType(StartPage, Descendants);
            List<BlogItemTree> tree = GenericExtensions.BlogItemTrees(Descendants);
            return tree;
        }
    }

Now the tree object will give you the exact same tree that is in your Episerver you just need to render this.

Jun 24, 2019

Comments

Keshav Dave
Keshav Dave Jun 24, 2019 03:31 PM

Hi Ravindra,

 

Thanks, I just use this approach and it's saved my day.

valdis
valdis Jun 25, 2019 07:19 AM

sorry for being picky here, but to keep my ServiceLocator guy title, I need to comment (at least something) :)

why you are requesting `IContentLoader` as constructor argument and still retrieving it from ServiceLocator?

also in `GenericExtensions` class - how you can get rid of ServiceLocator? By creating extension methods on `IContentLoader`! this will move responsibility of retrieving instance of `IContentLoader` from your class to the "consumer" class. where and how "consumer" gets that instance - becomes "not my problem anymore" :)

Ravindra S. Rathore
Ravindra S. Rathore Jun 25, 2019 10:48 AM

Hey Valdis,

Thanks for laying out what's really needed to be done on the extension methods for 'IContentLoader'.

I'll cover this in the next blog and that's definitely going to be a good step for future users.

Thanks a ton , Appreciate!

RR

Antti Alasvuo
Antti Alasvuo Jun 26, 2019 08:10 AM

Hi Ravindra, adding to Valdis comments, your extension uses DataFactory.Instance.GetChildren(xxx). Don't use it, but use you IContentLoader instances GetChildren(xxx) method or one of the overloads. DataFactory should not be used, it is history.

Ravindra S. Rathore
Ravindra S. Rathore Jun 26, 2019 12:48 PM

Thanks for the suggestion Antti Alasvuo, I have updated the post with your suggestion.

Please login to comment.
Latest blogs
Looking back at Optimizely in 2025

Explore Optimizely's architectural shift in 2025, which removed coordination cost through a unified execution loop. Learn how agentic Opal AI and...

Andy Blyth | Dec 17, 2025 |

Cleaning Up Content Graph Webhooks in PaaS CMS: Scheduled Job

The Problem Bit of a niche issue, but we are building a headless solution where the presentation layer is hosted on Netlify, when in a regular...

Minesh Shah (Netcel) | Dec 17, 2025

A day in the life of an Optimizely OMVP - OptiGraphExtensions v2.0: Enhanced Search Control with Language Support and Synonym Slots

Supercharge your Optimizely Graph search experience with powerful new features for multilingual sites and fine-grained search tuning. As search...

Graham Carr | Dec 16, 2025

A day in the life of an Optimizely OMVP - Optimizely Opal: Specialized Agents, Workflows, and Tools Explained

The AI landscape in digital experience platforms has shifted dramatically. At Opticon 2025, Optimizely unveiled the next evolution of Optimizely Op...

Graham Carr | Dec 16, 2025

Optimizely CMS - Learning by Doing: EP09 - Create Hero, Breadcrumb's and Integrate SEO : Demo

  Episode 9  is Live!! The latest installment of my  Learning by Doing: Build Series  on  Optimizely Episode 9 CMS 12  is now available on YouTube!...

Ratish | Dec 15, 2025 |

Building simple Opal tools for product search and content creation

Optimizely Opal tools make it easy for AI agents to call your APIs – in this post we’ll build a small ASP.NET host that exposes two of them: one fo...

Pär Wissmark | Dec 13, 2025 |