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
1 | public JsonResult GetChoicePage( string id) |
3 | Database db = Sitecore.Context.ContentDatabase ?? Sitecore.Context.Database; |
4 | Item item = db.GetItem( new ID(id)); |
7 | PageContext.Current.Item = item; |
8 | StringWriter viewWriter = new StringWriter(); |
10 | ContextService.Get().Push<ViewContext>( new ViewContext( this .ControllerContext, PageContext.Current.PageView, this .ViewData, this .TempData, viewWriter)); |
12 | string temp = RenderPlaceholder( string .Format( "Page Content/{0}" , "NAME_OF_THE_NON_DYNAMIC_PLACEHOLDER_WITHIN_PAGE_CONTENT" )); |
14 | return new JsonResult() { Data = temp, JsonRequestBehavior = JsonRequestBehavior.AllowGet }; |
17 | return Json( string .Empty); |
21 | public static string RenderPlaceholder( string placeholderName) |
23 | StringBuilder sb = new StringBuilder(); |
24 | StringWriter argsWriter = new StringWriter(sb); |
26 | RenderPlaceholderArgs args = new RenderPlaceholderArgs(placeholderName, argsWriter); |
27 | args.PageContext = PageContext.Current; |
29 | CorePipeline.Run( "mvc.renderPlaceholder" , args); |
In SCORE 2.1, you can use Score.Custom.Utils.RendererUtil class like so:
1 | var rendererUtil = new Score.Custom.Utils.RendererUtil(); |
2 | 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:
1 | public ActionResult InitDecisionTree() |
3 | Database db = Sitecore.Context.ContentDatabase ?? Sitecore.Context.Database; |
5 | ChoiceItemDto dataStructure = new ChoiceItemDto(); |
7 | CreateNavigationStructure(ContextItem, ref dataStructure, ref level); |
9 | return View( "_ViewName" , new DecisionTreeRenderingModel() |
11 | DataStructure = dataStructure |
12 | ErrorContent = RenderingContext.Current.Rendering.Item.Fields[ "Error Content" ].ToStringOrEmpty() |
16 | private static void CreateNavigationStructure(Item currentItem, ref ChoiceItemDto startLevel, ref int level) |
18 | startLevel.Name = currentItem.Fields[ "Page Title" ].ToStringOrEmpty(); |
19 | startLevel.Id = currentItem.ID.ToString(); |
22 | currentItem.GetChildren().Where( |
23 | a => a.TemplateID.Equals( new ID( "CHOICE_PAGE_TEMPLATE" ))); |
25 | foreach (var item in children) |
28 | ChoiceItemDto nextLevel; |
29 | nextLevel = new ChoiceItemDto {Name = item.Name, Level = level, Id = item.ID.ToString()}; |
30 | startLevel.Children.Add(nextLevel); |
31 | CreateNavigationStructure(item, ref nextLevel, ref level); |
37 | public class ChoiceItemDto |
39 | public string Name { get ; set ; } |
40 | public string Id { get ; set ; } |
41 | public int Level { get ; set ; } |
43 | public List<ChoiceItemDto> Children = new List<ChoiceItemDto>(); |
50 | if (Children.Count == 0) |
57 | return Children.OfType<ChoiceItemDto>() |
58 | .Select(x => x.Depth) |
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.
1 | @ using Sitecore.Globalization |
2 | @model DecisionTreeRenderingModel |
4 | @ using (Html.BeginUXModule( "Components/DecisionTree" , |
7 | Data = Model.DataStructure, |
8 | Link = Url.Action( "GetChoicePage" , "DecisionTree" , new { id = string .Empty }), |
9 | Error = Model.ErrorContent |
13 | @ class = "decision-tree " + Model.RenderingWrapperClasses, |
14 | @style = Model.RenderingModelStyles |
17 | if (Sitecore.Context.PageMode.IsPageEditorEditing) |
19 | <!-- Handle Editor Experience Mode --> |
24 | <div class = "slider-wrapper" data-bind= "with: decisionTreeViewModel" > |
25 | <div class = "slider-inner" > |
26 | <div class = "slides intro-wrapper" > |
27 | <div class = "container" > |
29 | <div class = "cg-logo" > |
35 | <div class = "slides decision-tree-content-wrapper" > |
36 | <div class = "container" > |
37 | <div id= "decision-tree-content-header" > |
41 | <div class = "decision-tree-content-outer" > |
42 | <div class = "container" > |
43 | <div id= "decision-tree-content" > |
44 | <!-- dynamically inserting placeholder content--> |
49 | <!-- back/next buttons & progress bar --> |
50 | <div class = "slider-footer" > |
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.