Content Area - Under the Hood, part III
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:
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-in ContentAreaRenderer , 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:
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 for ContentArea 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:
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:
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 during Initialize service locator is access as context.Locate , but in Uninitialize we are referring to service locator as ServiceLocator.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!
