5

I'm trying to programmatically get the HTML from an item, including its presentation details (i.e. placeholder items inserted via Experience Editor).

I'm using the top solution from this post but it ONLY renders the item itself and does not include any of the renderings I've added to the page: Is it possible to get page rendering result programmatically ?

3
  • What are you trying to do, get the HTML of the whole page or just part of it? Is the page accessible to anonymous users or only to logged in users?
    – jammykam
    Commented Apr 5, 2018 at 17:58
  • I have one page (lets call it the master page) that 'renders' it's child items (small pages), stacked atop each other, inside of its inner content area. I do this because these master pages are quite large and the content editor will be able to edit these smaller pages/sections individually (using experience editor) and then they will be 'rolled up' to the master pages. I don't necessarly need access to the HTML, I just need the pages to render on the master page in some sort of loop. Commented Apr 5, 2018 at 18:05
  • 1
    Sounds like you need true item rendering. See also: sitecoreart.martinrayenglish.com/2016/04/…
    – maz
    Commented Apr 5, 2018 at 18:19

2 Answers 2

4

You can get the HTML of the page using Sitecore.Web.WebUtil.ExecuteWebPage("provide url of item")

3

This appears to be working for me: https://jeffdarchuk.com/2015/10/18/render-item-to-string-mvc/

Create a new context stack that you can push new PageRenderItemDefinitionContext items to, then override the render pipeline to use the PageRenderItemDefinitionContext stack if it exists.

//<summary>
/// A context stack for the current page defintion.  By default many of the MVC pipeline
/// processes use a static singular page context PageContext.  This is problematic if
/// you want to render from a completely seperate context.  This is set up as a context
/// stack to be used on top of the already used PageContext.
/// </summary> 
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);
    }
}

The second part of this is to override the render method in the PerformRendering processor of the RenderPlaceholder pipeline to use our new PageRenderItemDefinitionContext class.

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());
            }
        }
    }
}

Create a new class to gather what we need and run the needed pipelines

/// <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.Site.SetDisplayMode(DisplayMode.Preview, 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);
       }
   }
}

At which point we can add an extension method to the Item object

/// <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 RenderToString(this Item item)
{
   return new ItemRenderer(item).Render();
}

I'm calling this method in a loop and appending the result to a string

private static string GetHtmlFromItem(Item item)
{
    return item.RenderToString();
}

EDIT: I didn't think I needed the config override as I thought I was calling the method directly from the code. I had it in the configs in my local environment but it wasn't working on another.

2
  • hey Michael, can you try expand your answer as link only answers are not encouraged.
    – josedbaez
    Commented Apr 6, 2018 at 8:48
  • Done. Note, I am using VS 2013 and did have some compiling issues with lack of C# 6 support. I made some minor tweaks in my own code. Commented Apr 6, 2018 at 17:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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