6

I have a Sitecore item that has presentation details ("MainPage"). Within those details is a component that also has presentation details ("PageSlide"). I cannot get Sitecore to display the presentation details from PageSlide.

The idea is that the page can have multiple slides. Each of the slides has its own unique presentation details, and some items are part of a placeholder inside of PageSlide but not in MainPage.

Here are the details for MainPage. (The layout is set, as are the necessary placeholders. I will eventually add more slides.)

enter image description here

And here are the details for PageSlide. FlexColumn is a DIV with a placeholder inside.

enter image description here

I have tried:

  1. Assigning each slide to a singular placeholder in MainPage. This only renders the slide, but not the slide's presentation details.
  2. Assigning each slide to it's own placeholder in MainPage.
  3. Using Item Renderings, add code to loop through the renderings in PageSlide and render them manually. (Note, this works to display the items as long as they are independent and consecutive. FlexColumn is completely rendered, and the RichTextContent items are placed on the page after the FlexColumn's div has closed. )

    @foreach (RenderingReference rendering in context.GetItem<Item>(Model.Id).Visualization.GetRenderings(Sitecore.Context.Device, true)
    {
        if (!string.IsNullOrWhiteSpace(rendering.Settings.DataSource))
        {
            @Html.Sitecore().ItemRendering(rendering.Settings.DataSource);
        }
        else
        {
            @Html.Sitecore().Rendering(rendering.RenderingID.ToString(), new { DataSource = rendering.Settings.DataSource });
        }
    }
    

    (Note: I am using Glass Mapper, but it's not a requirement.)

  4. Looping through the presentation details of MainPage and assigning the slides to a partial view with a content item as the model. (This does not work, because the presentation details are not pulled in.)

    @{
        IEnumerable<RenderingReference> renderings = Sitecore.Context.Item.Visualization.GetRenderings(Sitecore.Context.Device, true).Where(x => x.RenderingItem.Name == IPageSlideConstants.TemplateName);
        foreach (RenderingReference reference in renderings) 
        {
            PageSlide confSlide = context.GetItem<PageSlide>(reference.Settings.DataSource);
            @Html.Partial("~/Views/renderings/Components/PageSlide.cshtml", confSlide)
        }
    }
    

Two things I haven't looked into yet:

  1. Altering the pipelines.
  2. AJAX to display each slide as it's needed, into it's own DIV.

Is there another way to do this?

Edit: It appears there is a Html.Sitecore().ChildRenderings() method in the developer's reference guide, but I can't find any information on how to use it. I have added it to the view for the main page as well as in the slide, but nothing has changed.

2

Jeff Darchuk wrote a blog post describing how to render a Sitecore item to a string. The blog post provides all relevant copy-paste ready code. With this approach, you can retrieve the PageSlide item from the rendering data source, and render the item directly in your parent view:

@Html.Raw(PageSlideItem.Render())

His solution is to create a new temporary item context, extract the layout details, and utilize Sitecores standard pipelines to render the item into a string.

First step is to create a new temporary ItemDefinitionContext:

public class PageRenderItemDefinitionContext
{
    public static PageRenderItemDefinitionContext Current => ContextService.Get().GetCurrent<PageRenderItemDefinitionContext>();

    public static PageRenderItemDefinitionContext CurrentOrNull => ContextService.Get().GetCurrentOrDefault<PageRenderItemDefinitionContext>();

    public PageDefinition Definition { get; private set; }
    public Item Item { get; private set; }
    public DisplayMode PageMode { get; set; }

    public PageRenderItemDefinitionContext(PageDefinition pageDefinition, Item item, DisplayMode exteriorDisplayMode)
    {
        Assert.ArgumentNotNull(pageDefinition, nameof(pageDefinition));
        Assert.ArgumentNotNull(item, nameof(item));

        Definition = pageDefinition;
        Item = item;
        PageMode = exteriorDisplayMode;
    }

    public static IDisposable Enter(PageRenderItemDefinitionContext context)
    {
        Assert.ArgumentNotNull(context, "context");
        return ContextService.Get().Push(context);
    }

}

Next, override the PerformRendering pipeline processor to use the new PageRenderItemDefinitionContext:

    public class PerformItemRendering : PerformRendering
{
    public static readonly string ItemRenderingKey = Guid.NewGuid().ToString();

    /// <summary>
    /// Render step, except it temporarily abandons the placeholder context to render a seperate item, after which it puts the context back
    /// </summary>
    /// <param name="placeholderName">Placeholder to render</param>
    /// <param name="writer">writer to render to</param>
    /// <param name="args"></param>
    protected override void Render(string placeholderName, TextWriter writer, RenderPlaceholderArgs args)
    {
        if (PageRenderItemDefinitionContext.CurrentOrNull != null)
            args.PageContext.PageDefinition = PageRenderItemDefinitionContext.Current.Definition;

        if (placeholderName != ItemRenderingKey)
        {
            base.Render(placeholderName, writer, args);
            return;
        }

        Stack<PlaceholderContext> previousContext = new Stack<PlaceholderContext>();
        while (PlaceholderContext.CurrentOrNull != null)
        {
            previousContext.Push(PlaceholderContext.Current);
            PlaceholderContext.Exit();
        }

        try
        {
            PipelineService.Get().RunPipeline("mvc.renderRendering", new RenderRenderingArgs(args.PageContext.PageDefinition.Renderings.First(x => x.Placeholder.IsWhiteSpaceOrNull()), writer));
        }
        finally
        {
            while (PlaceholderContext.CurrentOrNull != null)
                PlaceholderContext.Exit();

            while (previousContext.Any())
            {
                PlaceholderContext.Enter(previousContext.Pop());
            }
        }
    }
}

Patch in the pipeline processor:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.renderPlaceholder>
        <processor patch:instead="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderPlaceholder.PerformRendering, Sitecore.Mvc']" type="Namespace.Pipelines.Mvc.RenderPlaceholder.PerformRendering, Assembly" />
      </mvc.renderPlaceholder>
    </pipelines>
  </sitecore>
</configuration>

Finally, wire it all up as a new ItemRenderer:

/// <summary>
/// Renders an item's layout to a string or TextWriter.
/// </summary>
public class ItemRenderer
{
    public Item Item { get; set; }

    public ItemRenderer(Item item)
    {
        Item = item;
    }

    /// <summary>
    /// Renders an item with a layout defined to a string for MVC
    /// </summary>
    /// <returns>HTML of item</returns>
    public virtual string Render()
    {
        using (TextWriter tw = new StringWriter())
        {
            Render(tw);

            return tw.ToString();
        }
    }

    /// <summary>
    /// Renders an item with a layout defined to a string for MVC
    /// </summary>
    /// <returns>HTML of item</returns>
    public virtual void Render(TextWriter writer)
    {
        var originalDisplayMode = Context.Site.DisplayMode;

        // keep a copy of the renderings we start with.
        // running the renderPlaceholder pipeline (which runs renderRendering) will overwrite these
        // and we need to set them back how they were when we're done rendering the xBlock
        var originalRenderingDefinitionContext = RenderingContext.CurrentOrNull?.PageContext?.PageDefinition;

        try
        {
            // prevents editing the snippet in context, so you cannot mistakenly change something shared by mistake
            if (Context.PageMode.IsExperienceEditorEditing || Context.PageMode.IsPreview)
            {
                Context.Site.SetDisplayMode(DisplayMode.Normal, DisplayModeDuration.Temporary);
            }

            var pageDef = new PageDefinition
            {
                Renderings = new List<Rendering>()
            };

            //Extracts the item's layout XML, then parses all of the renderings out of it.
            pageDef.Renderings.AddRange(GetRenderings(GetLayoutFromItem()));

            // Uncovers the main layout rendering
            var pageRenderingArgs = new GetPageRenderingArgs(pageDef);
            PipelineService.Get().RunPipeline("mvc.getPageRendering", pageRenderingArgs);

            //Renders all placeholders for the layout rendering, which would be the entire page
            var renderPlaceholderArgs = new RenderPlaceholderArgs(PerformItemRendering.ItemRenderingKey, writer, pageRenderingArgs.Result)
            {
                PageContext = new PageContext
                {
                    PageDefinition = pageDef
                }
            };

            using (PageRenderItemDefinitionContext.Enter(new PageRenderItemDefinitionContext(pageDef, Item, originalDisplayMode)))
            {
                PipelineService.Get().RunPipeline("mvc.renderPlaceholder", renderPlaceholderArgs);
            }
        }
        catch (Exception e)
        {
            Log.Error("There was a problem rendering an item to string", e, this);
            if (originalDisplayMode == DisplayMode.Edit || originalDisplayMode == DisplayMode.Preview)
            {
                writer.Write($"<p class=\"edit-only\">Error occurred while rendering {Item.Paths.FullPath}: {e.Message}<br>For error details, <a href=\"{LinkManager.GetItemUrl(Item)}\" onclick=\"window.open(this.href); return false;\">visit the target page</a></p>");
            }
        }
        finally
        {
            // replace the renderings in the current context with the ones that existed before we ran our sideline renderPlaceholder
            // because they have been overwritten with the xBlock's renderings at this point
            if (originalRenderingDefinitionContext != null)
            {
                RenderingContext.CurrentOrNull.PageContext.PageDefinition = originalRenderingDefinitionContext;
            }

            Context.Site.SetDisplayMode(originalDisplayMode, DisplayModeDuration.Temporary);
        }
    }

    /// <summary>
    /// Gets the layout XML from the item
    /// </summary>
    /// <returns>xml of the layout definition</returns>
    protected virtual XElement GetLayoutFromItem()
    {
        Field innerField = new LayoutField(Item).InnerField;

        if (innerField == null)
            return null;

        string fieldValue = LayoutField.GetFieldValue(innerField);

        if (fieldValue.IsWhiteSpaceOrNull())
            return null;

        return XDocument.Parse(fieldValue).Root;
    }

    /// <summary>
    /// Gets the rendering out of the xml node and injects some values in
    /// </summary>
    /// <param name="renderingNode"></param>
    /// <param name="deviceId"></param>
    /// <param name="layoutId"></param>
    /// <param name="renderingType"></param>
    /// <param name="parser"></param>
    /// <returns>MVC rendering</returns>
    protected virtual Rendering GetRendering(XElement renderingNode, Guid deviceId, Guid layoutId, string renderingType, XmlBasedRenderingParser parser)
    {
        Rendering rendering = parser.Parse(renderingNode, false);
        rendering.DeviceId = deviceId;
        rendering.LayoutId = layoutId;
        if (renderingType != null)
            rendering.RenderingType = renderingType;

        // if the xBlock is rendering in the context of another page, renderings with no data source should be repointed to the xBlock page item
        // as opposed to the context page item
        if (string.IsNullOrWhiteSpace(rendering.DataSource)) rendering.DataSource = Item.ID.ToString();

        return rendering;
    }

    /// <summary>
    /// Get all renderings out of the layout definition
    /// </summary>
    /// <param name="layoutDefinition">xml of the layout definition</param>
    /// <returns>list of renderings</returns>
    protected virtual IEnumerable<Rendering> GetRenderings(XElement layoutDefinition)
    {
        XmlBasedRenderingParser parser = MvcSettings.GetRegisteredObject<XmlBasedRenderingParser>();
        foreach (XElement xelement in layoutDefinition.Elements("d"))
        {
            Guid deviceId = xelement.GetAttributeValueOrEmpty("id").ToGuid();
            Guid layoutId = xelement.GetAttributeValueOrEmpty("l").ToGuid();

            yield return GetRendering(xelement, deviceId, layoutId, "Layout", parser);

            foreach (XElement renderingNode in xelement.Elements("r"))
                yield return GetRendering(renderingNode, deviceId, layoutId, renderingNode.Name.LocalName, parser);
        }
    }

}

Now, using the ItemRenderer, you can render the item to a string. To ease the usage, you can add an extension method to Item

/// <summary>
/// Renders an item with a layout definition to a string
/// </summary>
/// <param name="item"></param>
/// <returns>Rendered output for the item</returns>
public static string Render(this Item item)
{
    return new ItemRenderer(item).Render();
}

Note that this approach requires the rendered item to have a stripped down layout assigned, so that only the actual content is rendered (i.e. without html, head, body tags).

@Html.Sitecore().Placeholder("content")

To add Experience Editor support, switch on PageMode to add containing HTML-tags:

@if (Sitecore.Context.PageMode.IsExperienceEditor || Sitecore.Context.PageMode.IsPreview)
{
    <!DOCTYPE html>
    <html lang="da">
    <head>
        <!-- Header content here -->
    </head>
    <body>
        @Html.Sitecore().Placeholder("content")
    </body>
</html>
}
else
{
    @Html.Sitecore().Placeholder("content")
}
1

I believe a lot of the heavy lifting for what you're trying to do has already been done by the composite component module.

There's information about the module on the author's blog.

Composite components lets you design components in the Sitecore Experience Editor and have those components be placed as a whole on a page.

You may be able to use this module in it's native form to do what you want. If not, it's open source so you should be able to see how they're tackling the issue.

  • Thanks for the link, Owen. I looked at it, and it's close to what I'm looking for. (To be honest, I haven't even considered the impact on the Experience Editor, yet. I'm just trying to get the components to show.) I think the take-away is that I'm going to have to modify the rendering pipeline. – Nick Hamlin Jan 10 '17 at 21:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.