Technology - Software Development

5 easy steps for creating block display options in Episerver

Nov 10, 2016
Jonathan Guerra

One big concern for content editors is to get as much flexibility as possible from content blocks in the CMS. Sometimes it is a business requirement to present the same content with more than one layout, like an image gallery where blocks can be four different sizes. Episerver's display options let developers define multiple rendering templates for blocks, shown as "options" on edit view. The editor selection will determine how the item will be laid out on the content area.

Image 2: Display options preview

Adding display options to a project is easy, if you follow these steps:

1) First of all, an initialization class is needed to indicate that Episerver is going to use display options.

 
DisplayRegistryInitialization
 
namespace my.project.Business.Initialization
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class DisplayRegistryInitialization : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            if (context.HostType == HostType.WebApplication)
            {
                // Register Display Options
                var options = ServiceLocator.Current.GetInstance<DisplayOptions>();
                foreach (var optionId in options.Select(x => x.Id).ToArray())
                {
                    options.Remove(optionId);
                }
                options
                    .Add("full""/displayoptions/full", GlobalSettings.ContentAreaTags.FullWidth, string.Empty, "epi-icon__layout--full")
                    .Add("wide""/displayoptions/wide", GlobalSettings.ContentAreaTags.TwoThirdsWidth, string.Empty, "epi-icon__layout--two-thirds")
                    .Add("half""/displayoptions/half", GlobalSettings.ContentAreaTags.HalfWidth, string.Empty, "epi-icon__layout--half")
                    .Add("narrow""/displayoptions/narrow", GlobalSettings.ContentAreaTags.OneThirdWidth, string.Empty, "epi-icon__layout--one-third");
                 
                AreaRegistration.RegisterAllAreas();
 
            }
        }
 
        public void Preload(string[] parameters) { }
 
        public void Uninitialize(InitializationEngine context) { }
    }
 
}

 

ContentAreaTags
/// <summary>
/// Tags to use for the main widths used in the Bootstrap HTML framework
/// </summary>
publicstaticclassContentAreaTags
{
publicconststringFullWidth ="Full";
publicconststringTwoThirdsWidth ="Wide";
publicconststringHalfWidth ="Half";
publicconststringOneThirdWidth ="Narrow";
}

2) With this code, we are telling Episerver to add these options to a block's toggle menu. Now under the package "Business/Rendering" there is a class called "TemplateCoordinator". Change this class to following:


TemplateCoordinator
namespace my.project.Business.Rendering
{
    [ServiceConfiguration(typeof(IViewTemplateModelRegistrator))]
    public class TemplateCoordinator : IViewTemplateModelRegistrator
    {
        //locate the folder for templates
        public const string BlockFolder = "~/Views/Shared";
 
        public static void OnTemplateResolved(object sender, TemplateResolverEventArgs args)
        {
            //Disable DefaultPageController for page types that shouldn't have any renderer as pages
            if (args.ItemToRender is IContainerPage && args.SelectedTemplate != null && args.SelectedTemplate.TemplateType == typeof(DefaultPageController))
            {
                args.SelectedTemplate = null;
            }
        }
 
        public void Register(TemplateModelCollection viewTemplateModelRegistrator)
        {
            viewTemplateModelRegistrator.Add(typeof(BlockWithDisplayOptions), new TemplateModel
            {
                Name = "BlockWithDisplayOptions",
                Description = "Displays a block with different display options, all available",
                Tags = new[] { GlobalSettings.ContentAreaTags.OneFourthWidth,
                    GlobalSettings.ContentAreaTags.TwoThirdsWidth,
                    GlobalSettings.ContentAreaTags.FullWidth,
                    GlobalSettings.ContentAreaTags.HalfWidth},
                AvailableWithoutTag = false,
                Path = BlockPath("BlockWithDisplayOptions""Index.cshtml")
            });
 
            viewTemplateModelRegistrator.Add(typeof(TeaserBlock), new TemplateModel
            {
                Name = "TeaserBlock",
                Description = "Displays a teaser of a page.",
                Tags = new[] { GlobalSettings.ContentAreaTags.FullWidth},
                AvailableWithoutTag = false,
                Path = BlockPath("TeaserBlock""TeaserFull.cshtml")
            });
 
            viewTemplateModelRegistrator.Add(typeof(TeaserBlock), new TemplateModel
            {
                Name = "TeaserBlockNarrow",
                Description = "Displays a teaser of a page.",
                Tags = new[] { GlobalSettings.ContentAreaTags.OneThirdWidth},
                AvailableWithoutTag = false,
                Path = BlockPath("TeaserBlock""TeaserNarrow.cshtml")
            });
             
        }
        //Build the path to the template of each block registered
        private static string BlockPath(string folder, string fileName)
        {
            return string.Format("{0}/{1}/{2}", BlockFolder, folder,fileName);
        }
 
    }
}

 

3) Then there is a class needed which is going to process the tag (display option selected) in order to apply a CSS class on the divs that contain the block using Bootstrap. This class is called MyContentAreaRenderer that is going to inherited from ContentAreaRenderer Episerver class.

ContentAreaRenderer
namespace my.project.Business.Rendering
{
    /// <summary>
    /// Extends the default <see cref="ContentAreaRenderer"/> to apply custom CSS classes to each <see cref="ContentFragment"/>.
    /// </summary>
    public class AlloyContentAreaRenderer : ContentAreaRenderer
    {
        protected override string GetContentAreaItemCssClass(HtmlHelper htmlHelper, ContentAreaItem contentAreaItem)
        {
            var tag = GetContentAreaItemTemplateTag(htmlHelper, contentAreaItem);
            return string.Format("block {0} {1}", GetCssClassesForContentItem(tag), tag);
        }
 
        /// <summary>
        /// Gets a CSS class used for styling based on a tag name (ie a Bootstrap class name)
        /// </summary>
        /// <param name="tagName">Any tag name available, see <see cref="Global.ContentAreaTags"/></param>
        protected virtual string GetCssClassesForContentItem(ContentAreaItem contentAreaItem, string tagName)
        {
            if (string.IsNullOrWhiteSpace(tagName))
            {
                tagName = GlobalSettings.ContentAreaTags.FullWidth;
            }
 
            switch (tagName)
            {
                case GlobalSettings.ContentAreaTags.FullWidth:
                    return "col-md-12";
                case GlobalSettings.ContentAreaTags.TwoThirdsWidth:
                    return "col-md-9";
                case GlobalSettings.ContentAreaTags.HalfWidth:
                    return "col-md-6";
                case GlobalSettings.ContentAreaTags.OneThirdWidth:
                    return "col-md-4";
                case GlobalSettings.ContentAreaTags.OneFourthWidth:
                    return "col-md-3";               
                default:
                    return "";
            }
        }
 
    }
}

 

 

If there is needed to add a default display option when the block is created, there is a couple of things that we need to keep in mind. First, another method needs to be override on this class "GetContentAreaItemTemplateTag". In this example below the method evaluates if the content is an ISpecialRenderingContent and this interface is created and implemented on each block that we want to have a default display options when the block is created.

GetContentAreaItemTemplateTag
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 GlobalSettings.ContentAreaTags.FullWidth;
            }
 
            return templateTag;
        }
  
//interface
public interface ISpecialRenderingContent
    {
        string DefaultDisplayOption { get; }
    }
  
//the interface implementation
public class GalleryImageBlock : BlockData, ISpecialRenderingContent
{
    ///your code
  
    public string DefaultDisplayOption { get return GlobalSettings.ContentAreaTags.HalfWidth; } }
  
}
  

 

4) The registration of this new module is required to execute, so we register the new MyContentAreaRenderer as a dependency, adding the code below in the class "DependencyResolverInitialization," which is under Business/Initialization package.

private static void ConfigureContainer(ConfigurationExpression container)
{
    //Swap out the default ContentRenderer for our custom
    container.For<ContentAreaRenderer>().Use<AlloyContentAreaRenderer>();
    //Implementations for custom interfaces can be registered here.
}

 

5) On the block's preview controller generic class, we make changes on the Index method in order to get the display options registered on the system, and apply them into a content area.

NOTE: The next code below is for preview template.

PreviewController
namespace My.project.Web.Controllers
{
    [TemplateDescriptor(Inherited = true,
        TemplateTypeCategory = TemplateTypeCategories.MvcController,
        Tags = new[] { RenderingTags.Preview, RenderingTags.Edit },
        AvailableWithoutTag = false)]
    public class PreviewController : ActionControllerBase, IRenderTemplate<BlockData>
    {
        private readonly TemplateResolver _templateResolver;
        private readonly DisplayOptions _displayOptions;
 
        public PreviewController(TemplateResolver templateResolver, DisplayOptions displayOptions)
        {
            _templateResolver = templateResolver;
            _displayOptions = displayOptions;
        }
 
        public ActionResult Index(IContent currentContent)
        {
            var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
            var startPage = contentRepository.Get<SitePageData>(ContentReference.StartPage);
            var model = new PreviewModel(startPage, currentContent);
 
            var supportedDisplayOptions = _displayOptions
                .Select(x => new { Tag = x.Tag, Name = x.Name, Supported = SupportsTag(currentContent, x.Tag) })
                .ToList();
            if (supportedDisplayOptions.Any(x => x.Supported))
            {
                foreach (var displayOption in supportedDisplayOptions)
                {
                    var contentArea = new ContentArea();
                    contentArea.Items.Add(new ContentAreaItem
                    {
                        ContentLink = currentContent.ContentLink
                    });
                    var areaModel = new PreviewModel.PreviewArea
                    {
                        Supported = displayOption.Supported,
                        AreaTag = displayOption.Tag,
                        AreaName = displayOption.Name,
                        ContentArea = contentArea
                    };
                    model.Areas.Add(areaModel);
                }
            }
            return View(model);
        }
 
        private bool SupportsTag(IContent content, string tag)
        {
            var templateModel = _templateResolver.Resolve(HttpContext,
                                      content.GetOriginalType(),
                                      content,
                                      TemplateTypeCategories.MvcPartial,
                                      tag);
 
            return templateModel != null;
        }
 
    }
}

 

5.1) On the HTML template we need to have something like this:

template html
@model PreviewModel
 
@foreach (var area in Model.Areas)
{
    if (area.Supported)
    {
        @Html.Partial("TemplateHint"string.Format(@Html.Translate("/preview/heading"), Model.PreviewContent.Name, area.AreaName))
        @*<div class="row">
            <div>
                @Html.PropertyFor(m => m.PreviewContent)
            </div>
        </div>*@
 
        <div class="row preview clearfix">
            @Html.DisplayFor(x => area.ContentArea, new { Tag = area.AreaTag })
        </div>
    }
    else
    {
        @Html.Partial("TemplateHint"string.Format(@Html.Translate("/preview/norenderer"), Model.PreviewContent.Name, area.AreaName))
    }
}
 
@if (!Model.Areas.Any())
{
    @Html.Partial("TemplateHint"string.Format(@Html.Translate("/preview/norendereratall"), Model.PreviewContent.Name))
}

In summary, following these steps will allow you to render a block with display options, as well as to see it accurately in the preview screen. Furthermore, in the above code there are validations that indicate whether you need to use display options or not in a block. This is because you can decide if a block has this feature or not. If not you don't have to add the block in TemplateCoordinator class and, of course, create your own controller to display the block on preview. Beyond these basic instructions, just make sure to remember to keep your code clean and easy to understand.

 

 


Related insights