Let the editor choose a rendering template for content in EPiServer

Mattias Olsson 2015-11-23 04:46:08

EPiServer supports having several rendering templates for one content type. The default behavior is that the TemplateResolver algorithm kicks in to try to choose the best suited from one or more supported templates. See more about that in this blog post by Johan Björnfot. In my scenario I want the editor to be able to select the template when they are creating/editing the page. This is how I chose to implement this functionality.

Overview

  • Create an interface to implement on selected content types.
  • Create an editor descriptor so the editor can select one of all supported templates.
  • Example: Creating two rendering templates for a content type.
  • Hook up to the TemplateResolved event of the TemplateResolver to dynamically set template.
  • Example: Implement the interface on a content type.

Create an interface to implement on selected content types

Pretty simple and straight forward. I will show an example implementation of this later.

public interface IDynamicTemplateContent
{
    void SetDynamicTemplate(TemplateResolverEventArgs args);
}

 

Supported template selection editor descriptor

I am using the TemplateModelRepository to list all supported templates for the current content type. To determine the content type being edited I use the ContainerType property on the ExtendedMetadata class. If the content type implements IRoutable, I presume the valid templates are MvcController or Web Forms. Otherwise I presume it's a partial rendering.

[EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "TemplateModel")]
public class TemplateModelEditorDescriptor : EditorDescriptor
{
    private readonly TemplateModelRepository _templateModelRepository;

    public TemplateModelEditorDescriptor() 
        : this(ServiceLocator.Current.GetInstance<TemplateModelRepository>())
    {
    }

    public TemplateModelEditorDescriptor(TemplateModelRepository templateModelRepository)
    {
        if (templateModelRepository == null)
            throw new ArgumentNullException("templateModelRepository");

        _templateModelRepository = templateModelRepository;
        ClientEditingClass = "epi-cms/contentediting/editors/SelectionEditor";
    }

    public override void ModifyMetadata(
        ExtendedMetadata metadata, 
        IEnumerable<Attribute> attributes
    )
    {
        base.ModifyMetadata(metadata, attributes);

        metadata.CustomEditorSettings["uiType"] = metadata.ClientEditingClass;
        metadata.CustomEditorSettings["uiWrapperType"] = UiWrapperType.Floating;
        var contentType = metadata.FindOwnerContent()?.GetOriginalType();
        TemplateTypeCategories[] validTemplateTypeCategories;

        if (typeof(IRoutable).IsAssignableFrom(contentType))
        {
            validTemplateTypeCategories = new[] {
                TemplateTypeCategories.MvcController, 
                TemplateTypeCategories.WebFormsPage, 
                TemplateTypeCategories.WebForms, 
                TemplateTypeCategories.MvcView, 
                TemplateTypeCategories.Page
            };
        }
        else
        {
            validTemplateTypeCategories = new[] {
                TemplateTypeCategories.MvcPartialController, 
                TemplateTypeCategories.MvcPartialView, 
                TemplateTypeCategories.WebFormsPartial, 
                TemplateTypeCategories.ServerControl, 
                TemplateTypeCategories.UserControl
            };
        }

        var templateModels = _templateModelRepository
            .List(contentType)
            .Where(x => Array.IndexOf(validTemplateTypeCategories, x.TemplateTypeCategory) > -1);

        metadata.EditorConfiguration["selections"] = templateModels.Select(x => new SelectItem
        {
            Text = x.Name ?? x.TemplateType.Name,
            Value = x.TemplateType.FullName // Value stored in the database
        });
    }
}

 

Example: Creating two rendering templates for a content type

For this example I have created two rendering templates for the start page.

First template

[TemplateDescriptor(Name = "My first start page template", Default = true)]
public class StartController : PageController<StartPage>
{
    public ActionResult Index(StartPage currentPage)
    {
        return View(currentPage);
    }
}

View for first template (~/Views/Start/Index.cshtml)

@model StartPage
<h1>This is my first template!</h1>

 

Second template

[TemplateDescriptor(Name = "My second start page template")]
public class OtherStartController : PageController<StartPage>
{
    public ActionResult Index(StartPage currentPage)
    {
        return View(currentPage);
    }
}

View for second template (~/Views/OtherStart/Index.cshtml)

@model StartPage
<h1>This is my second template!</h1>

 

Hook up to the TemplateResolved event

Yet another InitializableModule :) What I'm doing is hooking up to the TemplateResolved event of the TemplateResolver. If the item being rendered implements IDynamicTemplateContent interface, the SetDynamicTemplate method is called.

[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class DynamicTemplateInitialization : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        context.Locate.TemplateResolver().TemplateResolved += OnTemplateResolved;
    }

    public void Uninitialize(InitializationEngine context)
    {
        ServiceLocator.Current.GetInstance<TemplateResolver>().TemplateResolved -= OnTemplateResolved;
    }

    private static void OnTemplateResolved(object sender, TemplateResolverEventArgs args)
    {
        var content = args.ItemToRender as IDynamicTemplateContent;

        if (content != null)
        {
            content.SetDynamicTemplate(args);
        }
    }
}

  

Example: Implement IDynamicTemplateContent interface on a content type

I added a property called SelectedTemplateModel on my start page to be able to select the template model. As you can see it has the UIHint "TemplateModel" so the editor descriptor created earlier should be used. In the implementation for SetDynamicTemplate, I check if this property has a value. If it does, it is matched against args.SupportedTemplates that is passed in to the method.

[ContentType(DisplayName = "Start page")]
public class StartPage : PageData, IDynamicTemplateContent
{
    [Display(Name = "Template", Order = 90)]
    [UIHint("TemplateModel")]
    public virtual string SelectedTemplateModel { get; set; }

    public void SetDynamicTemplate(TemplateResolverEventArgs args)
    {
        if (string.IsNullOrWhiteSpace(SelectedTemplateModel) 
            || SelectedTemplateIsPreviewController(args.SelectedTemplate))
        {
            return;
        }

        TemplateModel selectedTemplate = args.SupportedTemplates
            .FirstOrDefault(
                tmpl => tmpl.TemplateType.FullName.Equals(
                    SelectedTemplateModel, 
                    StringComparison.InvariantCultureIgnoreCase
                )
            );

        if (selectedTemplate != null)
        {
            // The template that the editor selected is supported. Switch to it.
            args.SelectedTemplate = selectedTemplate;
        }
    }

    private bool SelectedTemplateIsPreviewController(TemplateModel selectedTemplate)
    {
        if (selectedTemplate == null || selectedTemplate.Tags == null)
        {
            return false;
        }

        return Array.IndexOf(selectedTemplate.Tags, RenderingTags.Preview) > -1;
    }
}

As a last step I can choose between the two templates in edit mode for the start page like this: