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")
}