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.