Parts in this series:
– Part 1: Registration and Discovery
– Part 2: Template Selection
– Part 3: Extensibility
Overview
Following parts and pieces can be customizable, changeable or replaced by other code completely:
- Change renderer - It's possible to change they guy who is responsible for whole process in the first place. You can swap content area renderer completely and replace with your own code. You would need to inherit from built-in
ContentAreaRenderer
, swap it out in IoC container and do your stuff; - Change display template - Change view (
ContentArea.ascx
) for the content area type; - Customize rendering - It's possible to add and specify various settings while rendering then content area (
Html.PropertyFor(..., new { }
); - Handle events - As we got to know in Part 2
TemplateResolver
fires few events. We are able to hook on those events and customize templates selected as we need.
Replace Renderer
If something does not work as expected, you need some customization that is not achievable by other methods, want to add some extra stuff on top of existing content area renderer? It's possible to inherit from built-inContentAreaRenderer
, replace methods with your own and register new implementation in IoC container.
Inherit from Built-in Renderer
Inheritance is simple:
public class MyOwnContentAreaRenderer : ContentAreaRenderer
{
}
These are all methods that make sense to override and change behavior (most probably there is no point to override method that checks if currently CMS context is set to Edit
mode):
void Render(HtmlHelper htmlHelper, ContentArea contentArea)
void RenderContentAreaItems(
HtmlHelper htmlHelper,
IEnumerable<ContentAreaItem> contentAreaItems)
void RenderContentAreaItem(
HtmlHelper htmlHelper,
ContentAreaItem contentAreaItem,
string templateTag,
string htmlTag,
string cssClass)
void BeforeRenderContentAreaItemStartTag(
TagBuilder tagBuilder,
ContentAreaItem contentAreaItem)
TemplateModel ResolveTemplate(
HtmlHelper htmlHelper,
IContent content,
string templateTag)
string GetContentAreaTemplateTag(HtmlHelper htmlHelper)
string GetContentAreaItemTemplateTag(
HtmlHelper htmlHelper,
ContentAreaItem contentAreaItem)
string GetContentAreaHtmlTag(
HtmlHelper htmlHelper,
ContentArea contentArea)
string GetContentAreaItemHtmlTag(
HtmlHelper htmlHelper,
ContentAreaItem contentAreaItem)
string GetContentAreaItemCssClass(
HtmlHelper htmlHelper,
ContentAreaItem contentAreaItem)
Let's go through the call sequence of each of these methods.
Eventual impact of each method in resulting markup is shown in diagram below:
If you are looking for an easy way to customize resulting markup of ContentArea
, without overriding almost everything in ContentAreaRenderer
, head to "Customize Rendering" section.
Few observations:
- Why CSS
class=""
for content area itself can't be controlled viavirtual
method in the same way as other customization forHtml
tags andCss class
for area item? - Why there is no
AfterRenderContentAreaItemEndTag
method? Where you could hook in and add some necessary stuff after content area item has closed its tag element?
Swap Default Renderer
So once you are ready with your own content area renderer you will need to set it in action. In order to swap out built-in renderer and replace it with your own, all you need to do is to replace one in IoC container:
[ModuleDependency(typeof(ServiceContainerInitialization))]
[InitializableModule]
public class SwapRendererInitModule : IConfigurableModule
{
public void ConfigureContainer(ServiceConfigurationContext context)
{
context.Container.Configure(container =>
container.For<ContentAreaRenderer>()
.Use<MyOwnContentAreaRenderer>());
}
public void Initialize(InitializationEngine context)
{
}
public void Uninitialize(InitializationEngine context)
{
}
}
Now everything and anywhere where ContentArea
property is rendered - you will get full access and control of workflow and can customize it according to site's requirements.
Change Display Template
If you want to become real hacker and apply your logic to every single content area and also do some black magic with markup and the way how content areas are rendered, you may need to swap out existing built-in template forContentArea
by just creating "more specific" display template in your Shared/DisplayTemplates
folder (or anywhere where Asp.Net can find it):
I've seen projects where this was implemented. Fun times to try to find it out.. Custom display template for EPiServer built-in type was the last idea that came to mind.
Customize Rendering
There are also some extension points on how you can customize content area resulting markup.
Basically you can pass in few known to EPiServer keys in ViewData
when executing @Html.PropertyFor()
. For instance:
...
@Html.PropertyFor(m => m.ContentArea, new { tag = "some-tag" })
...
Following are known keys to EPiServer you can pass in:
Tag
- I think one of the most importantViewData
key while rendering content area. This key regulates what templates may be selected for the content area and potentially for its items. For more info on template selection - please read Part 2;HasContainer
- key indicates whether to generate wrapping element for the content area at all;CustomTag
- wrapping element name for the content area (defaults to "div
") ifhascontainer = true
. Small observation - a bit confusing and could be mismatched withtag
;CssClass
- CSS class(-es) to add to content area wrapping elementChildrenCustomTagName
- element name for every content area item (defaults to "div
"). Small observation - naming for area element name and child element name should be consisten. Let's say: eithercustomtagname
for content area itself orchilderencustomtag
for item;ChildrenCssClass
- CSS class(-es) name to add to every content area item
So this are list of keys you can pass in:
@Html.PropertyFor(m => m.ContentArea, new {
Tag = "some-tag",
HasContainer = true,
CustomTag = "ul",
CssClass = "this-is-a-list-class",
ChildrenCustomTagName = "li",
ChildrenCssClass = "this-is-list-item-class"
})
In the results you will get something like this assuming that there are few items added to the content area:
<ul class="this-is-a-list-class">
<li class="this-is-list-item-class">...</li>
<li class="this-is-list-item-class">...</li>
<li class="this-is-list-item-class">...</li>
</ul>
Which gives you pretty much flexibility to render content area as you want and need. This reminds me of Per Magne's approach to build menu driven by content area.
Handle Resolving Events
There 2 type of events that could be handled during content area rendering:
TemplateResolving
- from the name of the event it's clear that this event is raised before template is resolved;TemplateResolved
- however this event on the other hand is raised after EPiServer has finished template resolving process;
Both of these events are exposed from EPiServer API in order for us developers to hook in, handle and customize selection of the template.
Event handling for the template selection is pretty straight forward:
[ModuleDependency(typeof(InitializationModule))]
public class CustomizedRenderingInitialization : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
context.Locate.TemplateResolver().TemplateResolving +=
TemplateCoordinator.OnTemplateResolving;
context.Locate.TemplateResolver().TemplateResolved +=
TemplateCoordinator.OnTemplateResolved;
}
public void Uninitialize(InitializationEngine context)
{
ServiceLocator.Current.GetInstance<TemplateResolver>().TemplateResolving -=
TemplateCoordinator.OnTemplateResolving;
ServiceLocator.Current.GetInstance<TemplateResolver>().TemplateResolved -=
TemplateCoordinator.OnTemplateResolved;
}
}
public class TemplateCoordinator
{
public static void OnTemplateResolved(object sender, TemplateResolverEventArgs args)
{
// handle event - you may change selected template by:
// args.SelectedTemplate = ....;
}
public static void OnTemplateResolving(object sender, TemplateResolverEventArgs args)
{
// handle event - you may change selected template by:
// args.SelectedTemplate = ....;
}
}
This code fragment is taken from our old friend AlloyTech sample site. However I haven't dig into on why duringInitialize
service locator is access as context.Locate
, but in Uninitialize
we are referring to service locator asServiceLocator.Current
. I'm sure there is a reason behind this.
For more info about template model selection and it's internals - please read Part 2 of these series.
Summary
Overall ContentArea
was one of the most interesting improvement over EPiServer's 6.x ComposerBlock
concept. It's really powerful and you may achieve high flexibility and utilize it's potential in various scenarios.
However - I guess that ContentArea
needs a bit more love when it comes to discovery of magic behind the scene - that was main goal for these series to spread knowledge I gain while research EPiServer libraries.
The most challenging and interesting for me was Part 2 where I recapped how template model is working and how models are selected based on environmental settings. This part I find most unclear for developers (Usually questions are - "I do have a view for the block and I do have a controller for the block. Why my controller is not invoked, but instead view is rendered directly?").
Hope that these series will give you some more insightful look at what's going on when EPiServer scans your template models, when you execute @Html.PorpertyFor(m => m.ContentArea)
, which template model will be chosen and how to customize other stuff for the content area rendering pipeline.
Happy coding!