World is now on Opti ID! Learn more

Giuliano Dore
May 8, 2021
  55
(0 votes)

How to build a decision tree in EPiServer

During multiple meetings with different clients, we noticed lately that some of our clients were interested / looking for a solution involving a complex user journey with a decision tree for their digital platform.

It is worth noticing that our clients don’t necessary use the word “decision tree” as I believe, it is sometimes more relevant to technical people. We found out that this is the most relevant expression to use based on the needs.

But what is a decision tree?

Our clients were looking for a complex set of forms on their website(s) with pre-defined fields where, based on the answer, we would redirect the user to different questions and so on.

A very simple example of a scenario can be summarised like this:

  • Question 1: What kind of cuisine would you like to eat right now? Options are: Italian food or Chinese food
    • The user selects Italian food
      • Question 2: Would you prefer pizza or pasta?
        • The user selects pizza
          • Outcome: please find our selection of pizzerias in your area.
        • The user selects pasta
          • Outcome: please find our selection of Italian restaurants serving pasta in your area.
    • The user selects Chinese food
      • Question 2: Are you interested in rice dishes or dim sums?
        • The user selects rice dishes
          • Outcome: please find our selection of Chinese restaurants serving rice dishes in your area.
        • The user selects dim sums
          • Outcome: please find our selection of Chinese restaurants serving dim sums in your area.

As you can see, the user journey starts with a question as an entry point and each answer redirects the user to a different set of questions, forming a tree of questions and answers, offering the ability to provide different outcomes based on the answers.

For our clients (and potentially more requests in the future) we also need to built an engine that will support dozens of answers at the first level and possibly thousands of branches.

It is implied that decision trees only allow pre-defined answers. Open-ended answers would add a level of complexity as it can become complicated to "translate" plain text into an answer. Our requirements didn’t include open-ended answers.

There are a lot of games out there using a “decision tree engine” – some of them have been known for decades like the Choose Your Own Adventure games.

Choose Your Own Adventure - Wikipedia

We started doing some discovery about the capabilities in EPiServer to work with decision trees.

We found out that EPiServer Forms allows some kind of conditional logic where it is possible to display / hide some questions based on the answers of other questions, but unfortunately it is quite linear & wouldn’t scale well for scenarios with hundreds of branches & descendants.

But we also found out that we also have another tree we can use to “simulate” the decision tree: the content tree inside the CMS. The perks of going with that approach is that all the visual components are already there; no need to start using Dojo to build complex visual elements. It's also easy for the editors to group questions & answers using the built-in folders / nodes in the content tree.

Using references and the content properties inside EPiServer CMS, it is absolutely possible to store the data at the content tree level:

We can have one entry point (like a page) where we would set a reference to the first question / step.

As per requirement, the first question / step, needs to be a complex object possibly with a label and references to different answers.

Each answer would have a value and possibly an (optional) reference to the next question. If the property linking the answer to a possible next question is empty, we can safely assume that we are reaching one end of the tree.

The schema would look like:

Page properties

Question / step properties

Answer properties

 

Entry point: single reference to a question.

 

 

Question label: text

Answers: list of references to answer objects.

 

 

Answer label: text

Next question: (optional) reference to a question object.

Based on our analysis, we can use EPiServer pages or blocks for both questions and answers as pages and blocks are considered as complex customizable objects.

For our project we went with the following approach:

  • Each question is a page.
  • Each answer is a block, and the list is a ContentArea.

It's important to know that this is not the absolute truth, other data models can work just as well, we found this model to be a good fit based on our clients' ability to use the CMS.

We would add the following classes definitions:

public class DecisionTreePage : PageData
{
        [AllowedTypes(AllowedTypes = new[] { typeof(QuestionPage) })]
        [SelectOne(SelectionFactoryType = typeof(QuestionFactory))]
        public virtual PageReference StartingPoint { get; set; }
}

Public class QuestionPage: PageData
{
        public virtual string Label { get; set; }
        [AllowedTypes(AllowedTypes = new[] { typeof(AnswerBlock) })]
        public virtual ContentArea AnswersContainer { get; set; }
}

Public class AnswerBlock: BlockData
{
        [AllowedTypes(AllowedTypes = new[] { typeof(QuestionPage) })]
        public virtual PageReference NextQuestion { get; set; }
}

If you are paying attention, you will notice a selection factory decorating the starting point property. In order to avoid confusion regarding forms, questions and answers, we took the initiative to establish the following convention:

It is only possible to pick questions that are child nodes of the decision tree page. The code below shows a way to do it:

public class QuestionFactory : ISelectionFactory
{
        private Injected<IContentLoader> _contentLoader;

        public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
        {
            var currentPage = metadata.FindOwnerContent();

            if (currentPage == null)
                return Enumerable.Empty<ISelectItem>();

            if (!(currentPage is QuestionPage))
                return Enumerable.Empty<ISelectItem>();

            //triggered before the page is created - need to check contentlink as well
            if (ContentReference.IsNullOrEmpty(currentPage.ContentLink))
                return Enumerable.Empty<ISelectItem>();

            var children = _contentLoader.Service?.GetChildren<QuestionPage>(currentPage.ContentLink);
            return children.Select(x => new SelectItem
            {
                Text = x.Name,
                Value = x.ContentLink
            });
        }
}

And with this code, we make sure that only the child nodes will be selectable as the starting point.

Finally, we need some code to “build” our tree. It can be done using recursive functions available in the pseudo-code below:

public DecisionTreeViewModel GetTree(DecisionTreePage page)
{
        if (ContentReference.IsNullOrEmpty(page.StartingPoint))
                return null;

        var startingPoint = _contentLoader.Get<QuestionPage>(page.StartingPoint);
        var tree = GetRecursiveItems(startingPoint);
}

Public DecisionTreeViewModel GetRecursiveItems(QuestionPage question)
{
        If(question == null)
                return null;
        var toReturn = new DecisionTreeViewModel
        {
                questionLabel = question.Label;
                Answers = question.Answers.Select( x =>
                {
                        var nextQuestionPage = ContentReference.IsNullOrEmpty(x.NextQuestion) ?
                        null :
                        _contentLoader.Get<QuestionPage>(x.NextQuestion);

                       return new {
                                nextQuestion = nextQuestionPage == null ? null : GetRecursiveItems(nextQuestionPage);
                       }
                 }         
         }

         return toReturn;
}

And this is it! Once your tree view model is loaded, it is possible to move through the decision tree in a web page using the data that we collected before. It’s possible to use a SPA framework like Angular, React or Vue or even vanilla javascript to offer a very smooth user journey without reloading the page.

I thought of setting a plugin for decision trees for the content section of EPiServer, maybe on top of EPiServer Forms? if you are interested to join, please let me know in the comments 😊

May 08, 2021

Comments

Please login to comment.
Latest blogs
Make Global Assets Site- and Language-Aware at Indexing Time

I had a support case the other day with a question around search on global assets on a multisite. This is the result of that investigation. This co...

dada | Jun 26, 2025

The remote server returned an error: (400) Bad Request – when configuring Azure Storage for an older Optimizely CMS site

How to fix a strange issue that occurred when I moved editor-uploaded files for some old Optimizely CMS 11 solutions to Azure Storage.

Tomas Hensrud Gulla | Jun 26, 2025 |

Enable Opal AI for your Optimizely products

Learn how to enable Opal AI, and meet your infinite workforce.

Tomas Hensrud Gulla | Jun 25, 2025 |

Deploying to Optimizely Frontend Hosting: A Practical Guide

Optimizely Frontend Hosting is a cloud-based solution for deploying headless frontend applications - currently supporting only Next.js projects. It...

Szymon Uryga | Jun 25, 2025

World on Opti ID

We're excited to announce that world.optimizely.com is now integrated with Opti ID! What does this mean for you? New Users:  You can now log in wit...

Patrick Lam | Jun 22, 2025

Avoid Scandinavian Letters in File Names in Optimizely CMS

Discover how Scandinavian letters in file names can break media in Optimizely CMS—and learn a simple code fix to automatically sanitize uploads for...

Henning Sjørbotten | Jun 19, 2025 |