Giuliano Dore
May 8, 2021
  2034
(8 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

Sam Brooks
Sam Brooks May 10, 2021 04:53 PM

This is a really useful article, good code examples. I have previously had to build a complicated form for a client and we went with episerver forms using a multi-step form because they needed some more complicated form elements (file upload, date pickers etc) but for a simple multiple choice form this is much nicer solution, maintaining a big episerver form can be a massive headache.

Giuliano Dore
Giuliano Dore May 11, 2021 11:09 AM

Thank you Sam & Ram 😊

Please login to comment.
Latest blogs
Copy Optimizely SaaS CMS Settings to ENV Format Via Bookmarklet

Do you work with multiple Optimizely SaaS CMS instances? Use a bookmarklet to automatically copy them to your clipboard, ready to paste into your e...

Daniel Isaacs | Dec 22, 2024 | Syndicated blog

Increase timeout for long running SQL queries using SQL addon

Learn how to increase the timeout for long running SQL queries using the SQL addon.

Tomas Hensrud Gulla | Dec 20, 2024 | Syndicated blog

Overriding the help text for the Name property in Optimizely CMS

I recently received a question about how to override the Help text for the built-in Name property in Optimizely CMS, so I decided to document my...

Tomas Hensrud Gulla | Dec 20, 2024 | Syndicated blog

Resize Images on the Fly with Optimizely DXP's New CDN Feature

With the latest release, you can now resize images on demand using the Content Delivery Network (CDN). This means no more storing multiple versions...

Satata Satez | Dec 19, 2024