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 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…
Here is how we could architect our Sitecore content item structure:
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:
- How do we navigate through the desired choices?
- How do we switch between content that’s currently presented on-screen and newly loaded content?
- How do we ‘extract’ content added to the placeholder?
- 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:
- How the “slider-wrapper” div (main application div holder) is bound with our knockout decisionTreeViewModel;
- How the Navigation Data Structure DTO, URL for retrieving items, and Error messages are passed to the DecisionTree component’s JavaScript;
- 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.