November Happy Hour will be moved to Thursday December 5th.

Paul Gruffydd
Jun 24, 2019
  5305
(7 votes)

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.

Jun 24, 2019

Comments

Jun 24, 2019 02:20 PM

Great in a simple example.

We commonly use a block for rows and rows with sidebar due to the fact our designs have different background colour sets, tails and highlights between blocks and other design features which need to be configured per block. 

What I've always wanted is a layout block with popout options, specifically where simple properties can be setup from the same menu that you set personalization on blocks for example and it's just used as a container. So nested blocks become less of and issues,

At least in our case we have a full page preview system for our blocks similar to Alloy so it's not such as issue.

K Khan
K Khan Jun 24, 2019 02:43 PM

Another great post!

Paul Gruffydd
Paul Gruffydd Jun 24, 2019 05:43 PM

Thanks Khurram.

Scott - Interesting point. I have to admit that, for quite some time, I tended to go for wrapper blocks of some kind in every project but, more recently we've managed to achieve the layouts we need through a combination of rendering tags and display options (and a few other tricks). I'd agree that sometimes row/wrapper blocks are the more practical option though, particularly if the wrapper has settings associated with it or contains multiple content areas. My frustration is more around developers creating wrapper blocks which serve no purpose beyond rendering a wrapping element or elements. I've reviewed sites (not ones we've built I hasten to add) where, to render a given block, it has to be nested multiple levels deep in the correct combination of wrappers. That's a terrible experience for the editors and one which is easily avoided.

Sounds like we've had similar thoughts on how to approach the problem in a more universal way. I've had the idea of "instance properties" which would be properties of a block (perhaps limited in number and type) which can be set per usage of the block in a content area rather than stored against the block itself. The idea would be that they're more configuration than content. Maybe that's an idea for a new feature/add-on perhaps coupled with this feature request?

KennyG
KennyG Jun 24, 2019 08:37 PM

Hey Paul, just curious. How would this work if you need two (or more) Accordions on a page. Doesn't this assume everything in the contentarea gets pulled into a single accordion? Thanks, this is really cool!

Deepa Puranik
Deepa Puranik Jun 25, 2019 06:50 AM

Hi Paul,

Great post.

Can we add maximum limit to accordion items added to this content area wrapper?

valdis
valdis Jun 25, 2019 07:10 AM

great post! simple approach.

be aware that sometimes more that one plugin looking at hot content area render seat and want to swap it out for its own glory. in those cases there might be conflicts and some of plugin might not work anymore (here last arrives - wins). I even ran into this issue on my own with 2 of my plugins. therefore - I had to wrap and compose them together.

valdis
valdis Jun 25, 2019 07:11 AM

@Deepa - this is possible via custom validation. similar solution could be found in BootstrapAreaRenderer project - https://github.com/valdisiljuconoks/EPiBootstrapArea/blob/master/src/EPiBootstrapArea/BootstrapRowValidationAttribute.cs

Paul Gruffydd
Paul Gruffydd Jun 25, 2019 10:14 AM

@KennyG - Yes, that's certainly possible. The code above works by looping through content area items until it sees a change in the wrapper the items require. At that point it renders those items in the relevent wrapper and moves on to the next group so, as long as there's something between the accordion groups (and I'd expect there to be at least a heading) they will render separately.

@Deepa - Yep. Depending on what you want to achieve there are a couple of things you could do. As Valdis mentioned, you could create a custom validator if you want to prevent more than x accordion items in a row from being added. If you wanted to split into a separate group after x items you could do that as well by just adding in a count then, rather than just checking for a change in wrapper, check the item count as well.

@Valdis - Yes, that's a good point. You can only have one ContentAreaRenderer which is why I tend to keep the code within the solution rather than referencing a compiled library (or libraries) for content area rendering.

James Wilkinson
James Wilkinson Jun 25, 2019 02:49 PM

Dynamic positioning of "child blocks" are always a pain in the butt.

I always try to avoid wrapper blocks by pushing for stuff like this to be fixed to the page template, and exposing content areas for different positions on the page. But depending on requirements, it's not always possible.

Thanks for the post Paul, I'll be certainly exploring this further. To manipulate the rendering like this is a great idea, and looks pretty simple to implement too.

Paul Gruffydd
Paul Gruffydd Jun 26, 2019 12:28 PM

Hi Jay. Thanks for the comment. There's always a trade-off between flexibility, editor experience and, to some degree, performance. Creating page types/templates with fixed layouts is certainly one approach to the problem which offers easy editing and can perform well but misses the mark somewhat on flexibility. Longer term it can cause problems with managing the site too as any changes to the layout of a given page would require a change in content type (which can't then be versioned/previewed) and you usually end up with lots of page templates covering all combinations/iterations of layouts. That said, it works really well in some circumstances and it's always worth considering as an option. There's no single best way to render content across all sites and content types - it's a matter of understanding the options available and being able to choose the optimal combination for a given scenario.

Glenn Lalas
Glenn Lalas Jun 28, 2019 01:39 PM

I love this concept.  We went hard down the route of container blocks for a bit and were looking for good alternatives, so I can't wait to try this out to see if it solves our editor's frustrations.  Great post, Paul!

Paul Gruffydd
Paul Gruffydd Jun 28, 2019 05:08 PM

Thanks Glen. It's a similar story here - I've built sites which make extensive use of container blocks but each time I've trained up editors it's made me question whether that's really the right option. It makes sense from a developers' perspective and sometimes can't be avoided but, for editors, it's an extra step and extra complexity which they shouldn't really have to worry about.

postjoe
postjoe Jul 16, 2019 02:52 PM

I love that you have coupled the back end developer POV with a CMS user experience issue.  

Robert Runge
Robert Runge Sep 5, 2019 11:59 AM

Hi Paul,

Great write and I have found the concept useful and have used most of it in a solution.

I did have to change the code a bit, since as original posted htmlHelper.Raw(..) is not outputting anything.

private void RenderItemGroup(HtmlHelper htmlHelper, ICollection<ContentAreaItem> contentAreaItems, ContentAreaWrapper wrapper)
{
  if (contentAreaItems.Any())
  {
    htmlHelper.ViewContext.Writer.Write(wrapper.StartHtml);
    base.RenderContentAreaItems(htmlHelper, contentAreaItems);
    htmlHelper.ViewContext.Writer.Write(wrapper.EndHtml);
    items.Clear();

  }
}

Please login to comment.
Latest blogs
Optimizely SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog