November Happy Hour will be moved to Thursday December 5th.

Loading...
Area: Optimizely CMS
Applies to versions: 12 and higher
Other versions:
ARCHIVED This content is retired and no longer maintained. See the version selector for other versions of this topic.

Content templates

Recommended reading 
Note: This documentation is for the preview version of the upcoming release of CMS 12/Commerce 14/Search & Navigation 14. Features included here might not be complete, and might be changed before becoming available in the public release. This documentation is provided for evaluation purposes only.

This topic introduces the concept of content templates used for rendering content in Optimizely. Using templates, you can apply multiple templates for any type of content, and have the system decide when to use which template.

See section Rendering for more information about rendering in CMS. 

In this topic

Note: The examples here are based on .NET 5. Refer to previous versions of this topic for ASP.NET MVC or Web Form-based examples.

MVC controllers and views

Models, controllers, and views in MVC provide a clear separation of data, business logic, and presentation. The controller contains the business logic, handles URL requests, and selects the view, which is a visual presentation of the data model. In MVC, the controller is the class that is registered to support specific page types, and can be regarded as the template. The template defines which content types it can render in a specific context.

Creating a controller and a view

To add new template controller, create a new MVC controller in the Controllers folder in your project. Your controller should inherit from EPiServer.Web.Mvc.ContentController<T> (or optionally EPiServer.Web.Mvc.PageController<T> for PageData instances), where T is your content model. This controller is called when an instance of the content type is routed to, if it is chosen as the renderer for the content instance. 

To add the corresponding view for the controller, create a subfolder under Views and add an item of type View. It is recommended to follow the standard naming conventions in MVC for your model, controllers, and views.

To allow for reuse of logic that we want multiple pages to use, we have implemented a page controller base class, which in this case holds logic for a logout action, and inherits from PageController. You can also add a view model, to be able to add more than just page objects to the views. For simpler examples, we have not used a view model here.

To render properties you can use HTML helpers in MVC, for example, Html.PropertyFor, which renders property values based on their property type. HTML helpers are described more below.

Example: This example uses the content types created in section Content types. The controller for displaying the Article page type, inheriting from PageControllerBase.

using AlloyTemplates.Controllers;
using AlloyTemplates.Models.Pages;
using EPiServer.Framework.DataAnnotations;
using EPiServer.Shell.Security;
using Microsoft.AspNetCore.Mvc;

namespace MyOptimizelySite.Controllers
{
    public class ArticlePageController : PageControllerBase<ArticlePage>
    {
        public ArticlePageController(UISignInManager uiSignInManager) : base(uiSignInManager)
        {}

        public ActionResult Index(ArticlePage currentPage)
        {
            return View(currentPage);
        }
    }
}
Example: The page controller base, inheriting from PageController, and with SitePageData as generic type.
namespace AlloyTemplates.Controllers
{
    public abstract class PageControllerBase<T> : PageController<T>, IModifyLayout
        where T : SitePageData
    {
        private readonly UISignInManager _uiSignInManager;

        public PageControllerBase(UISignInManager uiSignInManager)
        {
            _uiSignInManager = uiSignInManager;
        }

        public async Task<IActionResult> Logout()
        {
            await _uiSignInManager.SignOutAsync();
            return RedirectToAction("Index");
        }
    }
}

Example: The corresponding rendering view for displaying the Article page.

@using EPiServer.Core
@using EPiServer.Web.Mvc.Html

@model MyOptimizelySite.Models.Pages.ArticlePage
  <h1>
    @Html.DisplayFor(m => m.Heading)
  </h1>

  <h3>
    @Html.PropertyFor(m => m.Introduction)
  </h3>

  <div>
    @Html.PropertyFor(m => m.MainBody)
  </div>

With the added rendering using Html.PropertyFor, the property is now also editable in the On-Page editing view.

pagetypetemplates.png

Razor Pages

An alternative to use controllers and views to render requests is to use Razor Pages. A razor contains of two different files (similar to a controller and a view), one Razor Page file with extension .cshtml and a corresponding page model file with extension .cshtml.cs. This allows the separation of the logic of a page from its presentation.

Creating a Razor Page

To add new template as a Razor Page, create a new Razor Page in the Pages folder in your project. Your page model should inherit from EPiServer.Web.Mvc.RazorPageModel<T>, where T is your content model. This Razor Page is called when an instance of the content type is routed to, if it is chosen as the renderer for the content instance.

To render properties you can use HTML helpers in MVC, for example Html.PropertyFor, which render property values based on their property type. HTML helpers are described more below.

Example: This example uses the content types created in section Content types. The Razor Page for displaying the Article page type, where page model inheriting from RazorPageModel<ArticlePage>.

namespace AlloyMvcTemplates.Pages
{
    public class ProductModel : RazorPageModel<ArticlePage>, IPageViewModel<ArticlePage>
    {
        public ProductPage CurrentPage => CurrentContent;

        public LayoutModel Layout { get; set; }
        public IContent Section { get; set; }

        public void OnGet()
        {
            Layout = HttpContext.RequestServices.GetService<PageViewContextFactory>()
                .CreateLayoutModel(CurrentContent.ContentLink, HttpContext);
        }
    }
}
Example: The corresponding rendering view for displaying the Article page.
@page
@model AlloyMvcTemplates.Pages.ProductModel
@{
    Layout = "~/Views/Shared/Layouts/_Root.cshtml";
}
<h1 @Html.EditAttributes(x => x.CurrentContent.PageName)>@Model.CurrentContent.PageName</h1>
<p class="introduction" @Html.EditAttributes(x => x.CurrentContent.MetaDescription)>@Model.CurrentContent.MetaDescription</p>
<div class="row">
    <div class="span8 clearfix" @Html.EditAttributes(x => x.CurrentContent.MainBody)>
        @Html.DisplayFor(m => m.CurrentContent.MainBody)
    </div>
</div>
@Html.PropertyFor(x => x.CurrentContent.MainContentArea, new { CssClass = "row", Tag = Global.ContentAreaTags.TwoThirdsWidth })

Block components and views

In MVC, rendering of blocks is done by using view components and/or views and associated templates, similar to the way you render pages. The recommendation is to use a block component, if some logic needs to be applied when the block is rendered; otherwise, use a partial view.

  • Create a view component that inherits from EPiServer.Web.Mvc.BlockComponent<TBlockData> or EPiServer.Web.Mvc.AsyncBlockComponent<TBlockData>, where TBlockData is your block type. The system calls this view component for the block type, if it is chosen as the renderer of the current block instance . EPiServer.Web.Mvc.BlockComponent<TBlockData> has an implementation of the method Invoke, which calls a partial view according to the MVC conventions that applies to view components.
  • Create a partial view without a view component, naming the view the same as the block type. If the view is chosen as the renderer of the block type, the view is called with the block instance directly, without controller involvement. This approach is the recommended way to render blocks.

Creating a block component

To add a new template for a block as a view component, create a new ViewComponent in the Components folder in your project. Your component model should inherit from EPiServer.Web.Mvc.BlockComponent<TBlockData> or EPiServer.Web.Mvc.AsyncBlockComponent<TBlockData>

To add the corresponding view for the view component, create a view following the convention /Views/Shared/Components/{View Component Name}/Default.cshtml.

Example: The view component for a block:

namespace AlloyTemplates.Components
{
    public class PageListBlockViewComponent : BlockComponent<PageListBlock>
    {
        private readonly IContentLoader _contentLoader;
        public PageListBlockViewComponent(IContentLoader contentLoader)
        {
            _contentLoader = contentLoader;
        }

        public override IViewComponentResult Invoke(PageListBlock currentBlock)
        {
            var model = new PageListModel(currentBlock)
            {
                Pages = _contentLoader.GetChildren<PageData>(currentBlock.Root, new LoaderOptions(), 0, currentBlock.Count)
            };

            return View(model);
        }
    }
}

Example: The view for the PageListBlock view component:

@model PageListModel
@Html.FullRefreshPropertiesMetaData(new[] { "IncludePublishDate", "IncludeIntroduction", "Count", "SortOrder", "Root", "PageTypeFilter", "CategoryFilter", "Recursive" })
<h2 @Html.EditAttributes(x => x.Heading)>@Model.Heading</h2>
<hr />

@foreach(var page in Model.Pages)
{
    <div class="listResult @string.Join(" ", page.GetThemeCssClassNames())">
        <h3>
            @Html.PageLink(page)
        </h3>
        @if(Model.ShowPublishDate && page.StartPublish.HasValue)
        {
            <p class="date">@Html.DisplayFor(x => page.StartPublish)</p>
        }
        @if(Model.ShowIntroduction && page is SitePageData sitePageData)
        {
            <p>@sitePageData.TeaserText</p>
        }
        <hr />
    </div>
}

Creating a partial view

In Visual Studio, add a partial view with the same name as your block type and based on your block model class, to the Views/Shared folder of your project.

Example: The partial view for the TeaserBlock block type, displaying a heading and an image.

@model MyOptimizelySite.Models.Blocks.TeaserBlock

<div>
  <h2>@Html.PropertyFor(x => x.Heading)</h2>
  <img src="@Url.ContentUrl(Model.Image)" />
</div>

Partial templates

Other content types than blocks (for example pages) can also have partial templates (view components or partial views) associated. These are then used if the content item is added to a ContentArea. As for block templates, partial templates for other content types can also be either view components or partial views. If view components are used, then the base class should be either EPiServer.Web.Mvc.PartialContentComponent<TContentData> or EPiServer.Web.Mvc.AsyncPartialContentComponent<TContentData>

Endpoint templates

In addition to MVC controllers and Razor Pages, there is also a possibility to add a template mapping to a Endpoint. For example, content media request in CMS are handled through an endpoint template. Custom-content endpoints that serve content needs to add a ContentActionDescriptor to the endpoint meta data. It is also recommended to add the policies for CmsPolicyNames.Read and CmsPolicyNames.Preview to ensure access rights are checked.

Example: Below is an example on a custom endpoint template for PDF files that adds a header to the response.

    [TemplateDescriptor(Inherited = true, TemplateTypeCategory = TemplateTypeCategories.HttpHandler)]
    public class PdfEndpoint : Endpoint, IRenderTemplate<PdfFile>
    {
        private readonly static ContentActionDescriptor _mediaContentActionDescriptor = new ContentActionDescriptor { Inherited = true, ModelType = typeof(PdfFile) };
        private static readonly AuthorizeAttribute _authorizeAttribute = new AuthorizeAttribute(CmsPolicyNames.Read);
        private static readonly AuthorizeAttribute _previewAttribute = new AuthorizeAttribute(CmsPolicyNames.Preview);

        public PdfEndpoint(IBlobHttpHandler blobHttpHandler)
            : base(context => ProcessRequest(blobHttpHandler, context),
                  new EndpointMetadataCollection(_mediaContentActionDescriptor, _authorizeAttribute, _previewAttribute), nameof(PdfEndpoint))
        {}

        private static Task ProcessRequest(IBlobHttpHandler blobHttpHandler, HttpContext context)
        {
            context.Response.Headers.Add("Content-Disposition", "attachment");
            return blobHttpHandler.Invoke(context);
        }
    }

The example above sets a header for a media request. Even though it is possible to define custom endpoints for media, in many cases it is easier to extend the default media handling by registering an implementation of IStaticFilePreProcessor.

Example: Below is an example on a custom pre-processor that adds a header to the response for PDF requests.

  public class PdfStaticFilePreProcessor : IStaticFilePreProcessor
    {
        //The built-in pre processor that handles MediaOptions has order 0, run after that
        public int Order => 10;

        public void PrepareResponse(StaticFileResponseContext staticFileResponseContext)
        {
            if (staticFileResponseContext.Context.Response.ContentType == "application/pdf")
            {
                staticFileResponseContext.Context.Response.Headers.Add("Content-Disposition", "attachment");
            }
        }
    }​

And the static file pre-processor is registered using MediaOptions, like:

services.Configure<MediaOptions>(o => o.AddPreProcessor<PdfStaticFilePreProcessor>());​

Model binding

If an MVC controller has a parameter that is a content instance, then CMS will automatically bind the routed content to the parameter, see Action method on ArticleController above. For Razor Pages is the routed content bound to the property RazorPageModel<T>.CurrentContent, see usage in ArticleModel above. For view components, the current content is passed as arguments for arguments named currentContent or currentBlock.

Using HTML Helpers

You can also use other specific Optimizely HTML helpers as an alternative to Html.PropertyFor. There are helpers for rendering links, content areas, translations, and navigation. These HTML helpers are called through Html.DisplayFor, but you can also use them directly.

Using templates

Templates define how content is rendered. The template (controller, partial view etc) selected to render a content instance, depends on the specific context. Use the TemplateDescriptor attribute to add meta data and define default templates, and Tags to define which template to use. Based on this information, and any defined display channels and display options settings, the TemplateResolver decides which template to use in a specific context. Note that if you are using partial views and no view component for partial templates, you cannot implement the TemplateDescriptor. Instead, you can implement the IViewTemplateModelRegistrator interface to register templates. See Rendering and the CMS sample site for examples.

Shared blocks folders

As previously mentioned, shared blocks are stored, loaded and versioned individually as an own entity in the database. Shared blocks are structured using folders, and a Folder is an instance of EPiServer.Core.ContentFolder. Content folders do not have associated rendering, and therefore no visual appearance on the website.

A folder in the shared blocks structure can have other folders or shared blocks as children, and a shared block cannot have any children.

You set editorial access on folders to specify which folders that are available for an editor. The global folder root EPiServer.Web.SiteDefinition.Current.GlobalAssetsRoot, is the root folder for shared blocks that are available for sites in an enterprise scenario. There can be a site-specific folder EPiServer.Web.SiteDefinition.Current.SiteAssetsRoot, containing the folder structure for shared blocks. In a single-site scenario, GlobalBlocksRoot and SiteBlocksRoot typically point to the same folder.

Related topic

Do you find this information helpful? Please log in to provide feedback.

Last updated: Jul 02, 2021

Recommended reading