How to limit rendering to specific display options and set default display option for a block

Mattias Olsson 2014-11-13 01:08:40

 

Specification

We want to accomplish the following:

  • Limit rendering of some of the blocks to specific display options.
  • If an unsupported display option is selected by the editor we want to show an error message instead of an ugly looking render of the block.
  • Set default display option for the block (when "Automatic" is selected).

In our case we have the following display options defined:

We also have the following rendering tags defined for above display options in our settings code:

public static class RenderingTags
{
    public const string FullWidth = "FullWidth";
    public const string HalfWidth = "HalfWidth";
    public const string OneThirdWidth = "OneThirdWidth";
}

 

Let's get down to business


ISpecialRenderingContent interface
The first thing we want to do is to create an interface that we can implement on our blocks so we're able to set supported rendering tags and default display option.

public interface ISpecialRenderingContent
{
    string[] SupportedRenderingTags { get; }
    string DefaultDisplayOption { get; }
}


Custom template resolver

Next we create our own TemplateResolver and override one of the Resolve methods. This is because we want to display an error message if the editor selects an unsupported display option. By default in EPiServer, if the template resolver returns null for a given content item and a given tag, an error message will be displayed in the content area. The message reads "The 'BlockName' can not be displayed". This is exactly what we want so we just return null if the current display option tag is not supported by our block.

[ServiceConfiguration(typeof(TemplateResolver), Lifecycle = ServiceInstanceScope.Singleton)]
public class MyTemplateResolver : TemplateResolver
{
    public MyTemplateResolver(IContentTypeRepository contentTypeRepository, TemplateModelRepository templateModelRepository, DisplayChannelService displayChannelService, TemplateModelSelector modelSelector) : base(contentTypeRepository, templateModelRepository, displayChannelService, modelSelector)
    {
    }
 
    public override TemplateModel Resolve(HttpContextBase httpContext, Type itemType, object itemToRender, TemplateTypeCategories templateTypeCategory, string tag)
    {
        var templateModel = base.Resolve(httpContext, itemType, itemToRender, templateTypeCategory, tag);
 
        var specialRenderingContent = itemToRender as ISpecialRenderingContent;
 
        if (specialRenderingContent != null && templateModel != null)
        {
            // Check if the current item to render supports the current tag (display option selected by the editor). If not, return null.
            return specialRenderingContent.SupportedRenderingTags.Contains(tag) ? templateModel : null;
        }
 
        return templateModel;
    }
}


Custom content area renderer
We need a custom content area renderer to be able to set the default display mode for blocks implementing the ISpecialRenderingContent interface. Our implementation looks like this. Pay extra close attention to the GetContentAreaItemTemplateTag method.

public class MyContentAreaRenderer : ContentAreaRenderer
{
    private IContent _currentContent;
 
    public MyContentAreaRenderer(IContentRenderer contentRenderer, TemplateResolver templateResolver, ContentFragmentAttributeAssembler attributeAssembler, IContentRepository contentRepository, DisplayOptions displayOptions)
        : base(contentRenderer, templateResolver, attributeAssembler, contentRepository, displayOptions)
    {
    }
 
    protected virtual IContent GetCurrentContent(ContentAreaItem contentAreaItem)
    {
        if (_currentContent == null || !_currentContent.ContentLink.CompareToIgnoreWorkID(contentAreaItem.ContentLink))
        {
            _currentContent = contentAreaItem.GetContent(ContentRepository);
        }
 
        return _currentContent;
    }
 
    protected override string GetContentAreaItemCssClass(HtmlHelper htmlHelper, ContentAreaItem contentAreaItem)
    {
        var baseClass = base.GetContentAreaItemCssClass(htmlHelper, contentAreaItem);
 
        // Allow developer to override by passing ChildrenCssClass in ViewData.
        if (!string.IsNullOrEmpty(baseClass))
        {
            return baseClass;
        }
 
        var tag = GetContentAreaItemTemplateTag(htmlHelper, contentAreaItem);
        return string.Format("block {0}", GetCssClassesForContentItem(contentAreaItem, tag));
    }
 
    protected override string GetContentAreaItemTemplateTag(HtmlHelper htmlHelper, ContentAreaItem contentAreaItem)
    {
        var templateTag = base.GetContentAreaItemTemplateTag(htmlHelper, contentAreaItem);
 
        if (string.IsNullOrWhiteSpace(templateTag))
        {
            var specialRenderingContent = GetCurrentContent(contentAreaItem) as ISpecialRenderingContent;
 
            if (specialRenderingContent != null)
            {
                return specialRenderingContent.DefaultDisplayOption;
            }
 
            return CommonSettings.RenderingTags.FullWidth;
        }
 
        return templateTag;
    }
  
    protected virtual string GetCssClassesForContentItem(ContentAreaItem contentAreaItem, string tagName)
    {
        if (string.IsNullOrWhiteSpace(tagName))
        {
            tagName = CommonSettings.RenderingTags.FullWidth;
        }
 
        switch (tagName)
        {
            case CommonSettings.RenderingTags.OneThirdWidth:
                return "one-third-width";
            case CommonSettings.RenderingTags.HalfWidth:
                return "half-width";
            default:
                return "full-width";
        }
    }
}


Implement the ISpecialRenderingContent interface on a given block content type

In this example we want to support "Full width" and "Half width" display options and the default display option should be "Half width". The implementation looks like this.

[ContentType(GUID = "F753A9D7-72D3-4599-8D40-7558F598537F", DisplayName = "Image, title and link", Description = "Image with title and alt text")]
public class MySpecialBlock : BlockData, ISpecialRenderingContent
{
    [Display(Name = "Text", Description = "Text for this feature", Order = 30)]
    public virtual XhtmlString Text { get; set; }
  
    [UIHint(UIHint.Image)]
    [Display(Name = "Image url", Order = 200)]
    public virtual ContentReference ImageUrl { get; set; }
 
    public string[] SupportedRenderingTags
    {
        get
        {
            return new[]
            {
                CommonSettings.RenderingTags.FullWidth,
                CommonSettings.RenderingTags.HalfWidth
            };
        }
    }
 
    public string DefaultDisplayOption { get { return CommonSettings.RenderingTags.HalfWidth; } }
}


Sass code for the blocks
Here are some basic sass code for the display options:

.block {
    height: 100%;
    @include clear; // Mixin for clearfix. Not included in this demo.
 
    &.full-width {
        clear: both;
    }
 
    &.half-width,
    &.one-third-width {
        float: left;
        display: inline;
    }
 
    &.half-width {
        width: 50%;
    }
 
    &.one-third-width {
        width: percentage(1/3);
    }
}


Wrapping things up

As a final step we're replacing the default template resolver and content area renderer with our custom implementations. In our case we do this in our structuremap registry class.

For<ContentAreaRenderer>().Use<MyContentAreaRenderer>();
For<TemplateResolver>().Use<MyTemplateResolver>();


Final thoughts

Is it possible to disable the display option select item in the context menu if no template is found to support it? Please let me know. It bugs me a lot.