A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

KennyG
Oct 1, 2020
  2641
(1 votes)

Using Custom Views for Editable Documentation

We’ve got some very structured hierarchical data that makes up a good portion of our site content.

Think Corporate > State > Metro > City > Community etc.

However, it can be very confusing for our site editors to know when a value should be set on the item itself, on its parent, grandparent, or worse still a related item. Sometimes you need to provide a little more info that what fits into the Description.

This calls for some clear documentation. And the best documentation is nearby without you needing to leave the system to find it.

 

I’ve based this on the awesome Custom View samples provided by Glen Lalas (Github repo, blog article).

I started with a basic text page and container folder.

Documentation Page

using System.ComponentModel.DataAnnotations;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;

namespace Features.Pages.DocumentationPage
{
    [ContentType(DisplayName = "Documentation Page", GUID = "xxxxxxxxxxxxxxxxxxxxxxxx", Description = "In Place Documentation Page")]
    public class DocumentationPage : PageData
    {

        [CultureSpecific]
        [Display(
            Name = "Main body",
            Description = "The main body will be shown in the main content area of the page, using the XHTML-editor you can insert for example text, images and tables.",
            GroupName = SystemTabNames.Content,
            Order = 10)]
        public virtual XhtmlString MainBody { get; set; }

    }
}

Documentation Folder

using Century.Website.Constants;
using Century.Website.Features.Pages.DocumentationPage;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;

namespace Website.Data
{
    [ContentType(
        DisplayName = "Documentation Container", 
        GUID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        Description = "Container for Documentation Pages",
        GroupName = PageGroups.DataPages)]
    [AvailableContentTypes(Include = new[] {
        typeof(DocumentationContainer), typeof(DocumentationPage)
    })]
    public class DocumentationContainer : PageData
    {
    }
}

These are limited to a dark corner of the site via AllowedTypes on a parent node/page.

[AvailableContentTypes(Include = new[] {
    typeof(DocumentationContainer)
})]

These are the basic pages to hold the documentation. Now we need to associate/map them to page types. (One editable page to many instances of the page type.) I stored these mappings on the homepage as a PropertyList.

Mappings PropertyList

[CultureSpecific]
[Display(
    Name = "Documentation Page Mappings",
    Description = "Map PageType to Documentation Page",
    GroupName = SystemTabNames.Settings)]
[EditorDescriptor(EditorDescriptorType = typeof(CollectionEditorDescriptor<DocumentationPageMapping>))]
public virtual IList<DocumentationPageMapping> DocumentationPageMappings { get; set; }
using System.ComponentModel.DataAnnotations;
using Website.Constants;
using Website.Features.Pages.DocumentationPage;
using EPiServer.Core;
using EPiServer.DataAnnotations;
using EPiServer.PlugIn;

namespace Features.CustomViews
{
    public class DocumentationPageMapping
    {
        [Display(Name = "Type Of Page")]
        [UIHint(CmsUIHints.DocumentationTypes)]
        public string PageTypeName { get; set; }
        [Display(Name = "Documentation Page")]
        [AllowedTypes(typeof(DocumentationPage))]
        public ContentReference pageReference { get; set; }

    }

    [PropertyDefinitionTypePlugIn]
    public class DocumentationMappings : PropertyList<DocumentationPageMapping>
    {
    }
}

Just to make life easier on everybody we're using a SelectionFactory to drive the PageType names for the mapping.

using EPiServer.Shell.ObjectEditing;
using System.Collections.Generic;

namespace Website.Business.SelectionFactory
{
    public class DocumentationTypeSelectionFactory : ISelectionFactory
    {
        public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
        {
            var selections = new List<SelectItem>();
            selections.Add(new SelectItem { Text = "Community", Value = "CommunityData" });
            selections.Add(new SelectItem { Text = "Metro", Value = "MetroData" });
            selections.Add(new SelectItem { Text = "Lot", Value = "LotData" });
            selections.Add(new SelectItem { Text = "Plan", Value = "PlanData" });
            return selections; 
        }
    }
}

Now we need to display these pages in the Custom View tab. We register the route: 

protected override void RegisterRoutes(RouteCollection routes)
{
    base.RegisterRoutes(routes);

    routes.MapRoute("AboutThisPageType", "AboutThisPageType", new { controller = "AboutThisPageType", action = "Index" });
}

We setup the page controller to get the documentation page mapped to the current pagetype.

AboutThisPageType Controller

using Website.Features.Pages.DocumentationPage;
using Website.Features.Pages.HomePage;
using EPiServer;
using EPiServer.Core;
using EPiServer.ServiceLocation;
using System.Linq;
using System.Web.Mvc;

namespace Website.Features.CustomViews
{
    public class AboutThisPageTypeController : Controller
    {
        public ActionResult Index()
        {
            var contentRepo = ServiceLocator.Current.GetInstance<IContentRepository>();
            // Since we're in an iFrame, need to do some manipulation to get the actual PageData object...
            var epiId = System.Web.HttpContext.Current.Request.QueryString["id"];
            var currentPage = contentRepo.Get<PageData>(new ContentReference(epiId));
            var homePage = contentRepo.Get<HomePage>(ContentReference.StartPage);
            var mapping = homePage.DocumentationPageMappings?.Where(x => x.PageTypeName == currentPage.PageTypeName)?.FirstOrDefault();
            var docPage = mapping != null ? contentRepo.Get<DocumentationPage>(mapping?.pageReference) : null ;
            
            return View("~/Features/CustomViews/AboutThisPageType.cshtml", docPage);
        }
    }
}

AboutThisPageType.cshtml

@using EPiServer.Core
@using EPiServer.Web.Mvc.Html
@using Website.Data;
@using Website.Features.Pages.DocumentationPage

@model DocumentationPage

@{ Layout = null; }
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="/dist/main.css">
    <title>Documentation</title>
</head>
<body>

    <main role="main" class="container">
        <br/>
        <br />
        @if (Model != null)
        {
            @Html.PropertyFor(x => x.MainBody, new { HasItemContainer = false }) 
        }
        else
        {
            <p>Documentation has not been provided for this item.</p>
        }

    </main>
</body>
</html>

We provide a simple message if a documentation page hasn't been mapped.

We also need to tell Epi to add some view configurations to add the custom view for each of the page types we are working with.

using Website.Data;
using EPiServer.ServiceLocation;
using EPiServer.Shell;

namespace Website.Features.CustomViews
{

    [ServiceConfiguration(typeof(EPiServer.Shell.ViewConfiguration))]
    public class AboutCommunityPagesViewConfig : ViewConfiguration<CommunityData>
    {
        public AboutCommunityPagesViewConfig()
        {
            Key = "aboutThisPage";
            Name = "Community Documentation";
            Description = "Info about Community pages";
            ControllerType = "epi-cms/widget/IFrameController";
            ViewType = "/AboutThisPageType/";
            IconClass = "aboutThisPageType";
        }
    }

    [ServiceConfiguration(typeof(EPiServer.Shell.ViewConfiguration))]
    public class AboutMetroPagesViewConfig : ViewConfiguration<MetroData>
    {
        public AboutMetroPagesViewConfig()
        {
            Key = "aboutThisPage";
            Name = "Metro Documentation";
            Description = "Info about Metro pages";
            ControllerType = "epi-cms/widget/IFrameController";
            ViewType = "/AboutThisPageType/";
            IconClass = "aboutThisPageType";
        }
    }

    [ServiceConfiguration(typeof(EPiServer.Shell.ViewConfiguration))]
    public class AboutLotPagesViewConfig : ViewConfiguration<LotData>
    {
        public AboutLotPagesViewConfig()
        {
            Key = "aboutThisPage";
            Name = "Lot Documentation";
            Description = "Info about Lot pages";
            ControllerType = "epi-cms/widget/IFrameController";
            ViewType = "/AboutThisPageType/";
            IconClass = "aboutThisPageType";
        }
    }

    [ServiceConfiguration(typeof(EPiServer.Shell.ViewConfiguration))]
    public class AboutPlanPagesViewConfig : ViewConfiguration<PlanData>
    {
        public AboutPlanPagesViewConfig()
        {
            Key = "aboutThisPage";
            Name = "Plan Documentation";
            Description = "Info about Plan pages";
            ControllerType = "epi-cms/widget/IFrameController";
            ViewType = "/AboutThisPageType/";
            IconClass = "aboutThisPageType";
        }
    }

}

These all share the same icon and view type. We add the icon image and a little bit of css under ClientResources

.Sleek .aboutThisPageType {
    background: url('images/icons8-about-24.png') no-repeat;
    height: 24px;
    width: 24px;
}

The css file is pulled in via the module.config file

<?xml version="1.0" encoding="utf-8"?>
<module>
	<clientResources>
		<add name="epi-cms.widgets.base" path="/ClientResources/epi-cms.css" resourceType="Style" />
	</clientResources>
</module>

If I've glossed over anything with the Custom View setup take a look at the Adage blog post that was my inspiration: https://adagetechnologies.com/enhancing-edit-mode-custom-views-episerver/

So there you have it. It might seem like a little bit of smoke and mirros because this is just another page in the site that you are providing but it puts it pretty much in context of where the editors are working and maintenance is self-contained.

Oct 01, 2020

Comments

Joe Mayberry
Joe Mayberry Oct 15, 2020 08:00 PM

Very cool idea. I love the idea that you are using the CMS to maintain the documentation for your users, and putting it in a context aware location for the users. Great job.

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP: Learning Optimizely Just Got Easier: Introducing the Optimizely Learning Centre

On the back of my last post about the Opti Graph Learning Centre, I am now happy to announce a revamped interactive learning platform that makes...

Graham Carr | Jan 31, 2026

Scheduled job for deleting content types and all related content

In my previous blog post which was about getting an overview of your sites content https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/sche...

Per Nergård (MVP) | Jan 30, 2026

Working With Applications in Optimizely CMS 13

💡 Note:  The following content has been written based on Optimizely CMS 13 Preview 2 and may not accurately reflect the final release version. As...

Mark Stott | Jan 30, 2026

Experimentation at Speed Using Optimizely Opal and Web Experimentation

If you are working in experimentation, you will know that speed matters. The quicker you can go from idea to implementation, the faster you can...

Minesh Shah (Netcel) | Jan 30, 2026