4

One of client wants to send email using standard .NET SMTP client. I would like to know How I can generate HTML of my Sitecore item which has all layouts and renderings configured and assigned that HTML to MailMessage.Body property?

I dont want to use Sitecore EXM module.

3 Answers 3

6

While both answers so far will work. I believe it would be far simpler to setup the email as a "campaign" style page on the site. Most emails that contain html should have a "cannot view this page? click here to view in a browser" link anyway, so this could serve as that page.

Then it would be a simple WebClient to get the html and insert into the body of your email.

using(var client = new WebClient())
{
    var html = client.DownloadString(pageUrl);
    return html;
}

Nice and simple, and now you have a way of viewing the page in the browser for those email clients that don't play nice with complex markup or block markup in emails.

2
  • Well, the approach in my answer gives the same possibilities. It is essentially rendering the entire page, however using Sitecores pipelines instead of a new HTTP-request. This is a simple solution though.
    – Kasper
    Apr 11, 2017 at 16:51
  • What happens if we have enabled 'xDB', might this request give an incorrect report. ? I'm concerned on this when we had similar decision hence we opted out this option. :( Apr 13, 2017 at 8:56
2

Jeff Darchuk wrote a blog post describing how to render a Sitecore item to a string, including layout details, renderings etc. You can then add the string to the message body of your email.

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

You can use Sitecore Item Web API to render HTML of a Sitecore Item i.e. Page template.

Sample Http Request to render HTML:

http://<host_name>/-/item/v1/-
/actions/GetRenderingHtml?sc_database=master&language=en&renderingId={493B3A8
3-0FA7-4484-8FC9-4680991CF743}&sc_itemid={110D559F-DEA5-42EA-9C1C-
8A5DF7E70EF9}&a=1&b=2&c=3

Steps to enable Item Web API which would be disabled default.

  • Enable Sitecore.ItemWebApi.config
  • Web Item API setting for a site can be defined as below Site Settings

Note: We have used similar approach to retrieve HTML of sitecore item to perform print operation :)

Your Answer

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

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