Customising rendering to reduce nested blocks
There have been a couple of posts about performance over the last few weeks from Stefan Holm Olsen then Khurram Khan, both of which suggest minimising the use of nested blocks. This is good advice not just for performance reasons but for improving the editing experience. As developers working in content management, it’s our role to make life easier for the editors who have to use what we implement on a daily basis so, if we can put in a little more work during development to save a lot of content management effort in the future I’d class that as a win.
One of the suggested methods to avoid nesting is to use list properties but, while these are certainly useful and represent the right option in some circumstances, there are drawbacks to be aware of. From an editor’s perspective, using list properties can cause problems in that the items in list properties aren’t reusable in different lists, they’re not well suited to large amounts of fields or large amounts of text in a field, they can’t have personalisation applied and, perhaps most importantly, they are very difficult to manage if you need to translate them between different language branches. From a developer’s perspective, you also need to consider the fact that these property types aren’t yet officially supported (as you can see from the great big warning at the top of the documentation).
One of the most common (and unnecessary) uses for nesting blocks I’ve seen in Episerver implementations is to create a “wrapper” block to group together “item” blocks. To give an example, you might be adding accordion functionality to a site and need to add markup like this:
<div class="accordion">
<div class="accordion__items">
<div class="accordion__item">
<h2 class="accordion__item-heading">Accordion 1</h2>
<div class="accordion__item-body">
<p>Accordion content goes here</p>
</div>
</div>
<div class="accordion__item">
<h2 class="accordion__item-heading">Accordion 2</h2>
<div class="accordion__item-body">
<p>Accordion content goes here</p>
</div>
</div>
</div>
</div>
One of my pet hates is to see this implemented by creating an accordion wrapper block like this.
[ContentType(DisplayName = "Accordion Wrapper", GUID = "C11E7439-DD80-488B-8CEB-23FA6D23508D", Description = "Wrapper for accordion items")]
public class AccordionWrapperBlock : SiteBlockData
{
[Display(
Name = "Accordion Items",
Description = "The items in the accordion",
GroupName = SystemTabNames.Content,
Order = 10)]
[AllowedTypes(typeof(AccordionItemBlock))]
public virtual ContentArea AccordionItems { get; set; }
}
In this instance, the wrapper block exists only to render a wrapping element around the accordion items, adding to the workload of the editors without really adding value. Instead, we should identify groups of similar items while we’re rendering the content area and add in the wrappers programmatically, allowing the editors to concentrate on their content without having to worry about what it should be wrapped in.
In order to address this scenario, first we need to be able to derive which items require which wrapping markup. To do this I’ve created a class which defines how a group should be rendered (ContentAreaWrapper) and an interface which my content types will implement which simply defines which ContentAreaWrapper we should use for that content.
//Define the markup wrapping a group of blocks
public class ContentAreaWrapper
{
public string StartHtml;
public string EndHtml;
}
//Interface to allow content types to define how they should be grouped
interface IWrapable
{
ContentAreaWrapper Wrapper { get; }
}
To ensure consistency and to allow different content types to sit together in the same group, I’ve created a class which contains static instances of my ContentAreaWrapper definitions.
public class ContentAreaWrappers
{
//Used if no other wrapper is defined
public static ContentAreaWrapper DefaultWrapper = new ContentAreaWrapper
{
StartHtml = "",
EndHtml = ""
};
//Wrapper for accordion blocks
public static ContentAreaWrapper AccordionWrapper = new ContentAreaWrapper
{
StartHtml = "<div class=\"accordion\"><div class=\"accordion__items\">",
EndHtml = "</div></div>"
};
}
Rendering the content areas is, unsurprisingly, handled by an instance of ContentAreaRenderer. Sites based on Alloy will already have a customised ContentAreaRenderer registered but, for sites which don’t, it’s just a matter of creating a class which inherits ContentAreaRenderer, overriding the necessary methods and using dependency injection to swap out the default instance of ContentAreaRenderer for your custom version.
In order to wire-in the wrapping of elements, we need to override the RenderContentAreaItems method which is responsible for iterating through the content area items and sending them to be rendered. In this method we’ll loop through the items in the ContentArea, grouping them by their wrapper then sending them to be rendered in those groups, wrapping them in the appropriate HTML.
public class WrappingContentAreaRenderer : ContentAreaRenderer
{
//Override RenderContentAreaItems to group items together and render in batches
protected override void RenderContentAreaItems(HtmlHelper htmlHelper, IEnumerable<ContentAreaItem> contentAreaItems)
{
if (contentAreaItems == null || !contentAreaItems.Any())
{
return;
}
var items = new List<ContentAreaItem>();
ContentAreaWrapper wrapper = ContentAreaWrappers.DefaultWrapper;
foreach (var item in contentAreaItems)
{
var content = item.GetContent();
var itemWrapper = (content as IWrapable)?.Wrapper ?? ContentAreaWrappers.DefaultWrapper;
if (itemWrapper != wrapper)
{
RenderItemGroup(htmlHelper, items, wrapper);
wrapper = itemWrapper;
}
items.Add(item);
}
RenderItemGroup(htmlHelper, items, wrapper);
}
//Render grouped items wrapped in appropriate markup
private void RenderItemGroup(HtmlHelper htmlHelper, List<ContentAreaItem> contentAreaItems, ContentAreaWrapper wrapper)
{
if (contentAreaItems.Any())
{
htmlHelper.Raw(wrapper.StartHtml);
base.RenderContentAreaItems(htmlHelper, contentAreaItems);
htmlHelper.Raw(wrapper.EndHtml);
contentAreaItems.Clear();
}
}
}
Finally, we can create an AccordionBlock implementing IWrapable to define how we should render a group of accordion blocks without requiring a wrapper.
[ContentType(DisplayName = "Accordion Item", GUID = "18d351e2-0046-4421-b405-e14f4ac56bd8", Description = "")]
public class AccordionItemBlock : SiteBlockData, IWrapable
{
#region Properties
[CultureSpecific]
[Display(
Name = "Heading",
Description = "The heading of the accordion item",
GroupName = SystemTabNames.Content,
Order = 10)]
public virtual string Heading { get; set; }
[CultureSpecific]
[Display(
Name = "Content",
Description = "The content of the accordion item",
GroupName = SystemTabNames.Content,
Order = 20)]
public virtual XhtmlString MainContent { get; set; }
#endregion
#region IWrapable
public ContentAreaWrapper Wrapper => ContentAreaWrappers.AccordionWrapper;
#endregion
}
Obviously this is a fairly simple example to demonstrate the technique but it's be pretty straightforward to extend it to handle more complex scenarios. It's also worth mentioning that, if you're currently adding in row blocks to wrap your inner blocks in a row container you should take a look at the EPiBootstrapArea project from Valdis Iljuconoks which uses a similar technique coupled with display options to dynamically wrap the appropriate number of items in row markup.
Comments