Content area rendering in Episerver
Learn from our experiences getting to grips with content area rendering in Episerver CMS, with code examples available via GitHub.
In a recent project, we were set the challenge of building a new public website for a client in Episerver CMS. The advantages of doing this were around how configurable content types were in Episerver compared to some of the alternatives we might have been asked to use. We thought it might be useful to describe some of the lessons we learned around content area rendering in this blog post.
What is Episerver CMS?
Episerver CMS is a content management system created using Microsoft technology stack ASP.NET. It is used by a multitude of companies to create, edit and publish content for their websites and intranets.
Episerver differs from other CMSes, like WordPress, in its complexity. Instead of providing users with preprogrammed units and ready-to-use features, Episerver is a framework that allows for innumerable extensions and modifications. This framework is one of the very best features in Episerver. Its ability to have virtually every bit of it extended and modified enables development in Episerver to tailor websites to user needs more accurately.
Add to this Episerver's ability to integrate with other systems because it is specifically designed to handle such integrations, and Episerver proves itself to be the best content management system for many website development projects.
Developing in Episerver
Episerver's development flexibility turned out to be one of the greatest assets in a recent project. We had some rather complex requirements. A set of content blocks that could:
- be used on any page
- have a certain behaviour if they were the first item in a content area of one page-type but not others
There are numerous ways to deal with this. However, we decided that the best solution would be to create something that suited our generic, reusable convention-oriented approach. Episerver was the best content management system for the job to ensure a similar experience for both visitors and editors, as well as maintaining consistency between views.
We had the following two primary requirements:
- Pass in custom attributes to both content area as well as its child items from the view that is rendering the property
- Allow each item in the content area to have its own attributes depending on varying criteria, such as the item's content type or its position in the content area.
We implemented these requirements by updating the default Alloy template and creating a tag builder convention which replicates the behaviour of the Alloy renderer performing these actions:
- Determine the rendering tag provided for that specific item or its container and provide relevant class
- Fetch and add a custom CSS class if the item implements `CustomCssInContentArea`
Content area renderer
Let’s start by clearing out 'AlloyContentAreaRenderer' and add the following methods and stubs:
public class AlloyContentAreaRenderer : ContentAreaRenderer { public override void Render(HtmlHelper htmlHelper, ContentArea contentArea) { if (contentArea == null || contentArea.IsEmpty) { return; } var viewContext = htmlHelper.ViewContext; TagBuilder contentAreaTagBuilder = null; if (!IsInEditMode(htmlHelper) && ShouldRenderWrappingElement(htmlHelper)) { contentAreaTagBuilder = new TagBuilder(GetContentAreaHtmlTag(htmlHelper, contentArea)); AddNonEmptyCssClass(contentAreaTagBuilder, viewContext.ViewData["cssclass"] as string); viewContext.Writer .Write(contentAreaTagBuilder.ToString(TagRenderMode.StartTag)); } RenderContentAreaItems(htmlHelper, contentArea.FilteredItems); if (contentAreaTagBuilder == null) { return; } viewContext.Writer.Write(contentAreaTagBuilder.ToString(TagRenderMode.EndTag)); } protected override void RenderContentAreaItem(HtmlHelper htmlHelper, ContentAreaItem contentAreaItem, string templateTag, string htmlTag, string cssClass) { base.RenderContentAreaItem(htmlHelper, contentAreaItem, templateTag, htmlTag, cssClass); } protected override void BeforeRenderContentAreaItemStartTag(TagBuilder tagBuilder, ContentAreaItem contentAreaItem) { } }
The 'Render(HtmlHelper, ContentArea)' method is pretty much the original implementation. Unfortunately, we don’t have another method we could use to access the content area’s own 'TagBuilder' instance before being written into the response stream.
Storing content area data
In order to carry around information between content area items as they’re being rendered, we need a context within which we can store those details. We’re able to make this easier for ourselves by understanding the dependency resolution lifecycle rules for deciding when to create a new instance – the original 'ContentAreaRenderer' is registered with a unique lifecycle, so every time its resolution is requested, we get a new instance.
This means that every time 'PropertyFor()' or 'DisplayFor()' is called in the view for a content area, eventually there’s a request to resolve a 'ContentAreaRenderer' and we know each 'ContentArea' instance rendered that way will have its own 'ContentAreaRenderer' instance.
While this also means we could store all our data publicly in renderer and just pass its instance around, we’d ultimately have a serious violation of the Single Responsibility Principle (SRP).
To carry our data, well create this rendering context:
public class ContentAreaRenderingContext { // ContentAreaItem-specific properties public int CurrentItemIndex { get; protected set; } public ContentAreaItem CurrentItem { get; protected set; } public IContent CurrentItemContent { get; protected set; } public IContent PreviousItemContent { get; protected set; } public DisplayOption CurrentItemDisplayOption { get; protected set; } // ContentArea-specific properties public ViewDataDictionary ViewData { get; } public ContentArea ContentArea { get; } public int TotalItems { get; } public ContentAreaRenderingContext(ViewDataDictionary viewData, ContentArea contentArea) { ViewData = viewData; ContentArea = contentArea; TotalItems = contentArea?.FilteredItems?.Count() ?? 0;; } public void BeginRenderingItem(ContentAreaItem contentAreaItem, IContent content, DisplayOption displayOption) { CurrentItem = contentAreaItem; CurrentItemContent = content; CurrentItemDisplayOption = displayOption; } public void FinishRenderingItem() { PreviousItemContent = CurrentItemIndex < TotalItems - 1 ? CurrentItemContent : null; CurrentItemIndex++; CurrentItem = null; CurrentItemContent = null; CurrentItemDisplayOption = null; } public bool IsRenderingContentArea() { return CurrentItem == null && CurrentItemContent == null && ContentArea != null; } public bool IsRenderingContentAreaItem() { return CurrentItem != null && CurrentItemContent != null && ContentArea != null; } }
Next, we’ll add this context to the renderer as a private field, but will skip initialisation. However, a dependency on 'IContentAreaLoader' is still required to populate the renderer later on:
public class AlloyContentAreaRenderer : ContentAreaRenderer { private ContentAreaRenderingContext _renderingContext; private readonly IContentAreaLoader _contentAreaLoader; public AlloyContentAreaRenderer(IContentAreaLoader contentAreaLoader) { if (contentAreaLoader == null) { throw new ArgumentNullException(nameof(contentAreaLoader)); } _contentAreaLoader = contentAreaLoader; } ...
Since we didn’t initialise the context in the constructor, we’ll both initialize and dereference it in the 'Render(HtmlHelper, ContentArea)' method override:
public override void Render(HtmlHelper htmlHelper, ContentArea contentArea) { if (contentArea == null || contentArea.IsEmpty) { return; } var viewContext = htmlHelper.ViewContext; TagBuilder contentAreaTagBuilder = null; _renderingContext = new ContentAreaRenderingContext(viewContext.ViewData, contentArea); // New line if (!IsInEditMode(htmlHelper) && ShouldRenderWrappingElement(htmlHelper)) { contentAreaTagBuilder = new TagBuilder(GetContentAreaHtmlTag(htmlHelper, contentArea)); AddNonEmptyCssClass(contentAreaTagBuilder, viewContext.ViewData["cssclass"] as string); viewContext.Writer.Write(contentAreaTagBuilder.ToString(TagRenderMode.StartTag)); } RenderContentAreaItems(htmlHelper, contentArea.FilteredItems); _renderingContext = null; // New line if (contentAreaTagBuilder == null) { return; } viewContext.Writer.Write(contentAreaTagBuilder.ToString(TagRenderMode.EndTag)); }
Avoiding external dependencies
Since we want to keep the context as clean as possible from external dependencies, we’ll ensure that all items are populated by the time they’re needed. To do this, we’ll also wrap the call to properly render the content area item in between calls to 'BeginRenderingItem()' and 'FinishRenderingItem()' methods:
protected override void RenderContentAreaItem(HtmlHelper htmlHelper, ContentAreaItem contentAreaItem, string templateTag, string htmlTag, string cssClass) { _renderingContext.BeginRenderingItem(contentAreaItem, _contentAreaLoader.Get(contentAreaItem), _contentAreaLoader.LoadDisplayOption(contentAreaItem)); base.RenderContentAreaItem(htmlHelper, contentAreaItem, templateTag, htmlTag, cssClass); _renderingContext.FinishRenderingItem(); }
Using patterns
Continuing to adhere to SRP, we’re going to make use of the Registry and Composition patterns to produce the required elements for our approach and create two interfaces:
public interface ITagBuilderConventionComposer { void Compose(ContentAreaRenderingContext context, TagBuilder tagBuilder); } public interface ITagBuilderConvention { void Apply(ContentAreaRenderingContext context, TagBuilder tagBuilder); }
The first interface is to hold our registry of conventions. This may be required to be generated in a different manner, or may obtain its conventions from a different source, or perhaps order them by an attribute. Neither the conventions nor consuming renderer will need to care about where one or the other comes from - that is the responsibility of the dependency inversion container.
The first interface has an easy enough implementation, it’ll just take an enumerable of types of the second interface as dependencies and loop through them to apply when requested:
public class TagBuilderConventionComposer : ITagBuilderConventionComposer { private readonly ITagBuilderConvention[] _registry; public TagBuilderConventionComposer(IEnumerable<ITagBuilderConvention> registry) { _registry = registry.ToArray(); } public void Compose(ContentAreaRenderingContext context, TagBuilder tagBuilder) { foreach(var item in _registry) item.Apply(context, tagBuilder); } }
We’ll add the composer interface as an additional dependency to the 'AlloyContentAreaRenderer' class. We'll make use of it in 'Render(HtmlHelper, ContentArea)' method override’s if-statement and as the action for 'BeforeRenderContentAreaItemStartTag(TagBuilder, ContentAreaItem)' method:
public class AlloyContentAreaRenderer : ContentAreaRenderer { ... // Other fields private readonly ITagBuilderConventionComposer _composer; public AlloyContentAreaRenderer(..., ITagBuilderConventionComposer composer) { ... // Other field assignments if (composer == null) { throw new ArgumentNullException(nameof(composer)); } _composer = composer; } public override void Render(HtmlHelper htmlHelper, ContentArea contentArea) { ... // Null/empty checks & local variable initializations if (!IsInEditMode(htmlHelper) && ShouldRenderWrappingElement(htmlHelper)) { contentAreaTagBuilder = new TagBuilder(GetContentAreaHtmlTag(htmlHelper, contentArea)); AddNonEmptyCssClass(contentAreaTagBuilder, viewContext.ViewData["cssclass"] as string); _composer.Compose(_renderingContext, contentAreaTagBuilder); // New line viewContext.Writer.Write(contentAreaTagBuilder.ToString(TagRenderMode.StartTag)); } ... // Render items, dereference context, and write end tag } protected override void BeforeRenderContentAreaItemStartTag(TagBuilder tagBuilder, ContentAreaItem contentAreaItem) { _composer.Compose(_renderingContext, tagBuilder); }
While the current approach assumes the responsibility to determine whether the 'TagBuilder' instance passed to it belongs to the container or to a child item to the convention implementation, it would be possible to either create and implement two more interfaces. An example of this is 'IContentAreaConvention' and 'IContentAreaConventionComposer' and using those for appropriate calls. In the interests of brevity, we won’t carry out those additional steps in this post.
Replicating the original Alloy renderer behaviour
That leaves only two more things left to do - first, the convention which replicates the original Alloy renderer behaviour:
public class AlloyTagBuilderConvention : ITagBuilderConvention { public void Apply(ContentAreaRenderingContext context, TagBuilder tagBuilder) { if(context.IsRenderingContentAreaItem()) { ApplyCore(context, tagBuilder); } } protected virtual void ApplyCore(ContentAreaRenderingContext context, TagBuilder tagBuilder) { var tag = GetContentAreaItemTemplateTag(context.ViewData, context.CurrentItemDisplayOption); tagBuilder.AddCssClass(string.Format($"block {GetTypeSpecificClasses(context.CurrentItemContent)} {GetCssClassForTag(tag)} {tag}")); } protected virtual string GetCssClassForTag(string tagName) { if (string.IsNullOrEmpty(tagName)) { return ""; } switch (tagName.ToLower()) { case "span12": return "full"; case "span8": return "wide"; case "span6": return "half"; default: return string.Empty; } } protected virtual string GetTypeSpecificClasses(IContent content) { var cssClass = content?.GetOriginalType().Name.ToLowerInvariant() ?? string.Empty; var customClassContent = content as ICustomCssInContentArea; if (!string.IsNullOrWhiteSpace(customClassContent?.ContentAreaCssClass)) { cssClass += $" {customClassContent.ContentAreaCssClass}"; } return cssClass; } protected virtual string GetContentAreaItemTemplateTag(ViewDataDictionary viewData, DisplayOption displayOption) { if (displayOption != null) { return displayOption.Tag; } return GetContentAreaTemplateTag(viewData); } protected virtual string GetContentAreaTemplateTag(ViewDataDictionary viewData) { return viewData["tag"] as string; } }
Second, and before any of this will work, we need to register the required classes in an implementation of 'IConfigurableModule'. With Alloy, there is already a 'DependencyResolverInitialization' which will work. The initialization code might look something like this (includes a helper class at the end):
[InitializableModule] [ModuleDependency(typeof(Episerver.Web.InitializationModule))] public class DependencyResolverInitialization : IConfigurableModule { public void ConfigureContainer(ServiceConfigurationContext context) { context.Container.Configure(ConfigureContainer); DependencyResolver.SetResolver(new StructureMapDependencyResolver(context.Container)); } private static void ConfigureContainer(ConfigurationExpression container) { //Swap out the default ContentRenderer for our custom container.For<IContentRenderer>().Use<ErrorHandlingContentRenderer>(); container.For<ContentAreaRenderer>().Use<AlloyContentAreaRenderer>(); //Implementations for custom interfaces can be registered here. // Nothing different for code above this line // New code below this line container .ForSingletonOf<ITagBuilderConventionComposer>() .Use<TagBuilderConventionComposer>(); // Register implementations of ITagBuilderConvention as singletons container.Scan(scan => { scan.AssemblyContainingType<ITagBuilderConvention>(); scan.Convention<SingletonConvention<ITagBuilderConvention>>(); }); } public void Initialize(InitializationEngine context) { } public void Uninitialize(InitializationEngine context) { } public void Preload(string[] parameters) { } } // This class is needed to allow configuring registrations of multiple types as transient public class SingletonConvention<TPluginFamily> : IRegistrationConvention { public void Process(Type type, Registry registry) { if (type.IsAbstract || !type.CanBeCreated() || !type.AllInterfaces().Contains(typeof(TPluginFamily))) { return; } registry.ForSingletonOf(typeof(TPluginFamily)).Use(type); } }
The project can now be run and assuming Alloy base, the site should look exactly the same as the unmodified Alloy sample site.
The singleton registration can be changed in the initialisation module if there is a need for dependency injection in the conventions themselves. In the interest of performance, we recommend the rules for class names to be decided in such a way which doesn’t require dynamic access for individual content area items other than viewdata or what is already in the rendering context.
The convention pay off
To examine other ways of using the convention here are a couple of example implementations:
/// <summary> /// Provides a way to add custom attributes to both container and child items from calling view /// </summary> public class CustomAttributesTagBuilderConvention : ITagBuilderConvention { public virtual void Apply(ContentAreaRenderingContext context, TagBuilder tagBuilder) { if (context.IsRenderingContentArea()) { ApplyCore(context, tagBuilder, "customattributes"); } else if(context.IsRenderingContentAreaItem()) { ApplyCore(context, tagBuilder, "childrencustomattributes"); } } protected virtual void ApplyCore(ContentAreaRenderingContext context, TagBuilder tagBuilder, string viewDataKey) { var attributes = context.ViewData[viewDataKey]; if (attributes == null) { return; } tagBuilder.MergeAttributes(new RouteValueDictionary(attributes)); } }
With the presented convention, It’s possible to do calls such as this from views:
@Html.PropertyFor(x => x.ContentArea, new { CustomAttributes = new { my_container_attribute = "this is my container" }, ChildrenCustomAttributes = new { my_child_attribute = "this is a child" } })
Which would generate output as HTML similar to this:
<div my-container-attribute="this is my container"> <div my_child_attribute="this is a child"> Content Area Item 1 </div> <div my_child_attribute="this is a child"> Content Area Item 2 </div> </div>
Convention to add a class on the basis of several criteria with a cache for storing of blocks as they are first requested:
/// <summary> /// Adds push-double--top class to the first content area item /// if the additionalviewdata passed to content area has PushFirstElement = true, /// unless the content area item is of specified types or has dark background /// </summary> public class PushFirstItemTagBuilderConvention : ITagBuilderConvention { public virtual void Apply(ContentAreaRenderingContext context, TagBuilder tagBuilder) { if(ShouldApply(context)) { tagBuilder.AddCssClass("push-double--top"); } } public const string RenderSettingsKey = "pushfirstelement"; protected virtual bool ShouldApply(ContentAreaRenderingContext context) { return IsFirstContentAreaItem(context) && ShouldPushFirstItem(context) && ShouldBeApplied(context.ContentData); } private static bool ShouldBeApplied(IContentData content) { var originalType = content.GetOriginalType(); var typeIsNotIgnored = IgnoreTypeLookups.GetOrAdd(originalType, type => IgnoreThese.All(t => !t.IsAssignableFrom(type))); var darkBackgroundCandidate = content as IHasDarkBackground; return darkBackgroundCandidate?.HasDarkBackground != true && typeIsNotIgnored; } private static bool IsFirstContentAreaItem(ContentAreaRenderingContext context) { return context.CurrentItemIndex == 0 || context.CurrentItemIndex == 1 && context.PreviousItem is HeroBlockBase; } private static bool ShouldPushFirstItem(ContentAreaRenderingContext context) { return context.ViewData[RenderSettingsKey] as bool? ?? false; } private static readonly ConcurrentDictionary<Type, bool> IgnoreTypeLookups = new ConcurrentDictionary<Type, bool>(); private static readonly Type[] IgnoreThese = { typeof(NotForThisBlock), typeof(AndNotForThisBlock) }; }
We found Episerver to be extremely powerful and it met our needs really well. However, given its complexity, it can be a steep learning curve. If you are trying to settle on a good way to configure the rendering of blocks in Episerver, hopefully, you have found our experiences and examples helpful.
All of the code used in this article can be found in our GitHub repository.