Take the community feedback survey now.

KennyG
Oct 1, 2020
  2457
(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
Optimizely CMS - Learning by Doing: EP06 - Create Header, Footer, Menu & Component/View for Blocks

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

Ratish | Nov 4, 2025 |

Going Headless: 3 Ways to Store Custom Data in Optimizely Graph

Welcome to another installment of my  Going Headless  series. Previously, we covered: Going Headless: Making the Right Architectural Choices Going...

Michał Mitas | Nov 3, 2025

A day in the life of an Optimizely OMVP - What's New in Optimizely CMS: A Comprehensive Recap of 2025 Updates

Hello and welcome to another instalment of a day in the life of an Optimizely OMVP. On the back of the presentation I gave in the October 2025 happ...

Graham Carr | Nov 3, 2025

Optimizely CMS Mixed Auth - Okta + ASP.NET Identity

Configuring mixed authentication and authorization in Optimizely CMS using Okta and ASP.NET Identity.

Damian Smutek | Oct 27, 2025 |

Optimizely: Multi-Step Form Creation Through Submission

I have been exploring Optimizely Forms recently and created a multi-step Customer Support Request Form with File Upload Functionality.  Let’s get...

Madhu | Oct 25, 2025 |

How to Add Multiple Authentication Providers to an Optimizely CMS 12 Site (Entra ID, Google, Facebook, and Local Identity)

Modern websites often need to let users sign in with their corporate account (Entra ID), their social identity (Google, Facebook), or a simple...

Francisco Quintanilla | Oct 22, 2025 |