Content area renderer on steroids

Mattias Olsson 12.07.2016 12:01:11

A common thing to do when you are developing an Episerver page with a content area is to wrap the content area inside a container div with a maximum width, so the blocks fits nicely into the grid. But what if the designer designs a page with a hero block in the middle of the page going all the way from the left to the right of the browser window? You could of course have three content areas on the page: one in the top, one hero content area and one in the bottom. You can then wrap the top and the bottom content areas inside a container. Not so dynamic if you ask me. And not the best solution, both from a technical and user experience perspective. No, let's stick with just one and put the magic inside a custom content area renderer.

The goals with my content area renderer

  • Let the editor select from a set of display options (sizes) for blocks to design the page as they want.
  • I want normal blocks to automatically be wrapped inside a container div and also balanced in rows based on the selected size of the blocks.
  • I want to be able to place a hero block wherever I want in the content area. These blocks should not be wrapped in a container and row.

Display options

For this example I'm using these display options:

public static class SiteDisplayOptions
{
    public const string Hero = "Hero";
    public const string FullWidth = "FullWidth";
    public const string TwoThirdsWidth = "TwoThirdsWidth";
    public const string HalfWidth = "HalfWidth";
    public const string OneThirdWidth = "OneThirdWidth";
}


Notice the Hero display option. I will use this in my content area renderer to determine if the block should be rendered as a hero block.

Let's get down to business

To make your custom content area renderer you need to inherit from EPiServer.Web.Mvc.Html.ContentAreaRenderer.

public class ContainerAndRowContentAreaRenderer : ContentAreaRenderer
{
    ...
}


I have defined the following properties to keep track of things during rendering:

// Number of columns in site grid
protected const int NumberOfGridColumns = 12;

// TagBuilder for the current container div.
protected TagBuilder CurrentContainer;

// TagBuilder for the current row div.
protected TagBuilder CurrentRow;

// To keep track of the current container.
protected bool IsContainerOpen;

// To keep track of the current row.
protected bool IsRowOpen;

// Current column counter for each row
protected int CurrentColumn;


The key method we need to override is RenderContentAreaItems.

protected override void RenderContentAreaItems(HtmlHelper html, IEnumerable contentAreaItems)
{
    // Begin by resetting properties
    CurrentColumn = 0;
    IsContainerOpen = false;
    IsRowOpen = false;

    foreach (ContentAreaItem contentAreaItem in contentAreaItems)
    {
        // Get template tag (selected display option).
        string templateTag = this.GetContentAreaItemTemplateTag(html, contentAreaItem);

	// Check if the selected display option is Hero. 
        bool isHeroContentAreaItem = this.IsHeroTag(templateTag);

        if (isHeroContentAreaItem)
            this.BeforeRenderHeroContentAreaItem(html, contentAreaItem);
        else
            this.BeforeRenderNormalContentAreaItem(html, contentAreaItem, templateTag);

        // Call the base class implementation of RenderContentAreaItem.
        this.RenderContentAreaItem(html, contentAreaItem, templateTag, this.GetContentAreaItemHtmlTag(html, contentAreaItem), this.GetContentAreaItemCssClass(html, contentAreaItem, templateTag));
    }

    // Make sure to close last row if open.
    if (IsRowOpen)
        EndRow(html);

    // Make sure to close last container if open.
    if (IsContainerOpen)
        EndContainer(html);
}


Override of GetContentAreaItemTemplateTag with fallback to FullWidth tag.

protected override string GetContentAreaItemTemplateTag(HtmlHelper html, ContentAreaItem contentAreaItem)
{
    var templateTag = base.GetContentAreaItemTemplateTag(html, contentAreaItem);

    if (string.IsNullOrWhiteSpace(templateTag))
    {
        // Default to full width.
        return SiteDisplayOptions.FullWidth;
    }

    return templateTag;
}


Override of GetContentAreaItemCssClass to return class for selected display option.

protected virtual string GetContentAreaItemCssClass(HtmlHelper html, ContentAreaItem contentAreaItem, string templateTag)
{
    var baseClass = base.GetContentAreaItemCssClass(html, contentAreaItem);

    // Allow developer to override by passing ChildrenCssClass in ViewData.
    if (string.IsNullOrEmpty(baseClass) == false)
    {
        return baseClass;
    }

    string displayOptionCssClass;

    switch (templateTag)
    {
        case SiteDisplayOptions.FullWidth:
            displayOptionCssClass = "full";
            break;
        case SiteDisplayOptions.TwoThirdsWidth:
            displayOptionCssClass = "two-thirds";
            break;
        case SiteDisplayOptions.HalfWidth:
            displayOptionCssClass = "one-half";
            break;
        case SiteDisplayOptions.OneThirdWidth:
            displayOptionCssClass = "one-third";
            break;
        case SiteDisplayOptions.Hero:
            displayOptionCssClass = "hero";
            break;
        default:
            displayOptionCssClass = templateTag;
            break;
    }

    return string.Format("block {0}", displayOptionCssClass);
}


This method is called from RenderContentAreaItems before a Hero content area item is rendered.

protected virtual void BeforeRenderHeroContentAreaItem(HtmlHelper html, ContentAreaItem contentAreaItem)
{
    // Make sure to close row and container if open. We don't want to wrap hero blocks.
    if (IsRowOpen)
        EndRow(html);

    if (IsContainerOpen)
        EndContainer(html);
}


This method is called from RenderContentAreaItems before a normal (non Hero) content area item is rendered.

protected virtual void BeforeRenderNormalContentAreaItem(HtmlHelper html, ContentAreaItem contentAreaItem, string templateTag)
{
    int itemColumns = this.GetNumberOfGridColumns(templateTag);
    CurrentColumn += itemColumns;
    bool fitsInRow = CurrentColumn <= NumberOfGridColumns;

    // Open container if not already open.
    if (IsContainerOpen == false)
        StartContainer(html);

    if (fitsInRow == false || IsRowOpen == false)
    {
        // Make sure to close row if open.
        if (IsRowOpen)
            EndRow(html);

        // Start on a new row.
        StartRow(html);

        // Set current column
        CurrentColumn = itemColumns;
    }
}


Get number of grid columns that the current content area item needs based on the selected display option.

protected virtual int GetNumberOfGridColumns(string templateTag)
{
    switch (templateTag)
    {
        case SiteDisplayOptions.TwoThirdsWidth:
            return 8;
        case SiteDisplayOptions.HalfWidth:
            return 6;
        case SiteDisplayOptions.OneThirdWidth:
            return 4;
        default:
            return 12;
    }
}


Check if given template tag is Hero tag defined in display options.

protected virtual bool IsHeroTag(string templateTag)
{
    return templateTag == SiteDisplayOptions.Hero;
}


See full code at this Gist link: https://gist.github.com/MattisOlsson/4e617c1486f192550d37e1b8326bc14f

Some screens:

Block displayed in full width

Select Hero display option

Block displayed as Hero

Sample markup

This is some sample markup rendered with content area renderer:

<div>
    <div class="container">
        <div class="row">
            <div class="block full">...</div>
        </div>
        <div class="row">
            <div class="block one-half">...</div>
            <div class="block one-half">...</div>
        </div>
    </div>

    <div class="block hero">
        ...
    </div>

    <div class="container">
        <div class="row">
            <div class="block one-third">...</div>
            <div class="block one-third">...</div>
            <div class="block one-third">...</div>
        </div>
    </div>
</div>

 

Replace default renderer

Finally, to replace the default renderer and register your custom display options you need to create an InitializableModule:

[InitializableModule]
[ModuleDependency(typeof(ServiceContainerInitialization))]
[ModuleDependency(typeof(DataInitialization))]
public class InitializationModule : IConfigurableModule
{
    public void Initialize(InitializationEngine context)
    {
        RegisterDisplayOptions(context.Locate.Advanced.GetInstance());
    }

    public void Preload(string[] parameters) { }

    public void Uninitialize(InitializationEngine context)
    {
    }

    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Container.Configure(ConfigureContainer);
    }

    private static void ConfigureContainer(ConfigurationExpression container)
    {
        container.For<ContentAreaRenderer>().Use<ContainerAndRowContentAreaRenderer>();
    }

    private void RegisterDisplayOptions(DisplayOptions displayOptions)
    {
        displayOptions.Add(CreateDisplayOption(SiteDisplayOptions.FullWidth));
        displayOptions.Add(CreateDisplayOption(SiteDisplayOptions.TwoThirdsWidth));
        displayOptions.Add(CreateDisplayOption(SiteDisplayOptions.HalfWidth));
        displayOptions.Add(CreateDisplayOption(SiteDisplayOptions.OneThirdWidth));
        displayOptions.Add(CreateDisplayOption(SiteDisplayOptions.Hero));
    }

    private DisplayOption CreateDisplayOption(string tag)
    {
        return new DisplayOption
        {
            Id = tag,
            Name = string.Format("/displayoptions/{0}", tag),
            Tag = tag,
            IconClass = null // No icons in this example
        };
    }
}