Building a single-application component using Sitecore MVC, JS MVVM Framework and BrainJocks SCORE – PART 1

In this series of posts, I would like to show our readers how to build out a single-application component in Sitecore using one of the many popular MVVM JS frameworks on the market – in this case Knockout, together with BrainJocks SCORE. Even if you don’t use SCORE in your project, you can still follow along with this post as SCORE is only utilized for initializing our JavaScript files and coupling them to our Razor Views. Knockout is also not mandatory and can be replaced by any other JS MVVM Framework.

Let’s say that we want to build a decision tree component which navigates users through options on the page represented as buttons. Each button acts as a dynamic link that navigates the user to a different set of content without refreshing the page. Each of these transitions displays new content with new choices (button links), and drives the user deeper into the decision tree. Eventually the user ends up on a result page based on their navigational input. I think you get the point…
 

decision step one
 
Here is how we could architect our Sitecore content item structure:

Content Tree

 

In order to build this component with dynamic content loading functionality, there are a couple of things we have to solve and think about first:

  1. How do we navigate through the desired choices?
  2. How do we switch between content that’s currently presented on-screen and newly loaded content?
  3. How do we ‘extract’ content added to the placeholder?
  4. How do we know the current position in the decision tree and JSON navigational structure? This is important if we want to build previous/next navigation as a part of our component.

In this blog article, we will look at how to get components placed into non-dynamic placeholders and try to answer these questions. Let’s focus first on the controller action and how we can retrieve components inserted into non-dynamic placeholders on the page by using Sitecore’s render pipeline.

 

Controller

public JsonResult GetChoicePage(string id) //passing id of the item which contains content for displaying 
{
      Database db = Sitecore.Context.ContentDatabase ?? Sitecore.Context.Database;
      Item item = db.GetItem(new ID(id)); //get page item
      if (item != null)
      {
            PageContext.Current.Item = item; //set current page context to point to the page we retrieved
            StringWriter viewWriter = new StringWriter();
            //get ContextService object and push 'dummy' view context based on controllercontext
            ContextService.Get().Push<ViewContext>(new ViewContext(this.ControllerContext, PageContext.Current.PageView, this.ViewData, this.TempData, viewWriter)); 
            //here is where we render placeholder based on the full name path
            string temp = RenderPlaceholder(string.Format("Page Content/{0}", "NAME_OF_THE_NON_DYNAMIC_PLACEHOLDER_WITHIN_PAGE_CONTENT"));

            return new JsonResult() { Data = temp, JsonRequestBehavior = JsonRequestBehavior.AllowGet };
      }

      return Json(string.Empty);
}
 
// private method that renders content of the placeholder using "mvc.renderPlaceholder" pipeline
public static string RenderPlaceholder(string placeholderName)
{
      StringBuilder sb = new StringBuilder();
      StringWriter argsWriter = new StringWriter(sb);

      RenderPlaceholderArgs args = new RenderPlaceholderArgs(placeholderName, argsWriter);
      args.PageContext = PageContext.Current;
       
      CorePipeline.Run("mvc.renderPlaceholder", args);
      return sb.ToString();
}

 

In SCORE 2.1, you can use Score.Custom.Utils.RendererUtil class like so:

var rendererUtil = new Score.Custom.Utils.RendererUtil();
var html = rendererUtil.RenderPlaceholder(item, "placeholderKey");

As an added bonus, you can do this from outside of a Web context (such as in an agent, in a custom computed field, etc.).
 

Building the Front-End

Now that we know how to render placeholder content, the next step will be to create a non-complex JSON structure for our UI front-end that will contain all possible choice elements with just a few properties, like page ID, level, depth, title, etc.

I’ve used recursion to get items and their children from the Sitecore content tree:

public ActionResult InitDecisionTree()
{
       Database db = Sitecore.Context.ContentDatabase ?? Sitecore.Context.Database;

       ChoiceItemDto dataStructure = new ChoiceItemDto();
       int level = -1;
       CreateNavigationStructure(ContextItem, ref dataStructure, ref level);
            
       return View("_ViewName", new DecisionTreeRenderingModel()
       {
            DataStructure = dataStructure
            ErrorContent = RenderingContext.Current.Rendering.Item.Fields["Error Content"].ToStringOrEmpty()
       });
}
 
 
private static void CreateNavigationStructure(Item currentItem, ref ChoiceItemDto startLevel, ref int level)
{
        startLevel.Name = currentItem.Fields["Page Title"].ToStringOrEmpty();
        startLevel.Id = currentItem.ID.ToString();

        var children =
            currentItem.GetChildren().Where(
                a => a.TemplateID.Equals(new ID("CHOICE_PAGE_TEMPLATE")));

        foreach (var item in children)
        {
             level++;
             ChoiceItemDto nextLevel;
             nextLevel = new ChoiceItemDto {Name = item.Name, Level = level, Id = item.ID.ToString()};            
             startLevel.Children.Add(nextLevel);
             CreateNavigationStructure(item, ref nextLevel, ref level);

             level--;
        }
}
 
public class ChoiceItemDto
{
        public string Name { get; set; }
        public string Id { get; set; }
        public int Level { get; set; }

        public List<ChoiceItemDto> Children = new List<ChoiceItemDto>();

        public int Depth
        {
            get
            {
                // Completely empty menu (not even any straight items). 0 depth.
                if (Children.Count == 0)
                {
                    return 0;
                }
                // We've either got items (which would give us a depth of 1) or
                // items and groups, so find the maximum depth of any subgroups,
                // and add 1.
                return Children.OfType<ChoiceItemDto>()
                            .Select(x => x.Depth)
                            .DefaultIfEmpty() // 0 if we have no subgroups
                            .Max() + 1;
            }
        }
}

During Component Initialization, the navigation structure DTO is passed to the model; then it is passed by SCORE CCF (SCORE Component Communication Framework) to our JavaScript.
 

View

In the Razor MVC View example shown below, I have stripped additional knockout bindings in order to focus on 3 things:

  1. How the “slider-wrapper” div (main application div holder) is bound with our knockout decisionTreeViewModel;
  2. How the Navigation Data Structure DTO, URL for retrieving items, and Error messages are passed to the DecisionTree component’s JavaScript;
  3. How the div with id “decision-tree-content” is used for inserting dynamic content retrieved from the ‘GetChoicePage’ JsonResult Controller Action.
@using Sitecore.Globalization
@model DecisionTreeRenderingModel

@using (Html.BeginUXModule("Components/DecisionTree",
                            new
                            {
                                Data = Model.DataStructure,
                                Link = Url.Action("GetChoicePage", "DecisionTree", new { id = string.Empty }),
                                Error = Model.ErrorContent
                            },
                            new
                            {
                                @class = "decision-tree " + Model.RenderingWrapperClasses,
                                @style = Model.RenderingModelStyles
                            }))
{
    if (Sitecore.Context.PageMode.IsPageEditorEditing)
    {
        <!-- Handle Editor Experience Mode -->
    }
    else
    {

    <div class="slider-wrapper" data-bind="with: decisionTreeViewModel">
        <div class="slider-inner">
            <div class="slides intro-wrapper">
                <div class="container">

                    <div class="cg-logo">

                    </div>
                </div>
            </div>

            <div class="slides decision-tree-content-wrapper">
                <div class="container">
                    <div id="decision-tree-content-header">

                    </div>
                </div>
                <div class="decision-tree-content-outer">
                    <div class="container">
                        <div id="decision-tree-content">
                            <!-- dynamically inserting placeholder content-->

                        </div>
                    </div>
                </div>
                <!-- back/next buttons & progress bar -->
                <div class="slider-footer">

                </div>
            </div>
        </div>
    </div>
    }
}

 

In part 2 of this post series, I am going to cover the JavaScript pieces of this puzzle and how a knockout model can be used to leverage front-end functionality.

Ivan Omorac

I am a certified Sitecore developer with nearly 10 years of web development experience. I am also a Microsoft Certified Professional Developer (MCPD) in both ASP.NET Developer 3.5 and Web Developer 4. My development work is guided by a personal mission–to make it simple, efficient and easily maintainable–and I’m always looking for ways to improve. My skill set spans ASP.NET, C#, MVC.NET, Javascript and jQuery but, no matter what I’m programming in, my goal is to make my code bullet-proof and highly stable. I enjoy finding solutions to challenging problems and sharing knowledge with the team at BrainJocks, and see this blog as a natural extension of that collaboration to the broader Sitecore community. When I’m not in the Skybox you’ll probably find me playing guitar or hanging out at one of the local blues joints. Learn more about Ivan.

Add a Comment

Your email address will not be published. Required fields are marked *

Or request call back