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
  2538
(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
Cleaning Up Content Graph Webhooks in PaaS CMS: Scheduled Job

The Problem Bit of a niche issue, but we are building a headless solution where the presentation layer is hosted on Netlify, when in a regular...

Minesh Shah (Netcel) | Dec 17, 2025

A day in the life of an Optimizely OMVP - OptiGraphExtensions v2.0: Enhanced Search Control with Language Support, Synonym Slots, and Stop Words

Supercharge your Optimizely Graph search experience with powerful new features for multilingual sites and fine-grained search tuning. As search...

Graham Carr | Dec 16, 2025

A day in the life of an Optimizely OMVP - Optimizely Opal: Specialized Agents, Workflows, and Tools Explained

The AI landscape in digital experience platforms has shifted dramatically. At Opticon 2025, Optimizely unveiled the next evolution of Optimizely Op...

Graham Carr | Dec 16, 2025

Optimizely CMS - Learning by Doing: EP09 - Create Hero, Breadcrumb's and Integrate SEO : Demo

  Episode 9  is Live!! The latest installment of my  Learning by Doing: Build Series  on  Optimizely Episode 9 CMS 12  is now available on YouTube!...

Ratish | Dec 15, 2025 |