KennyG
Oct 1, 2020
visibility 2947
star star star star star
(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.

error Please login to comment.
Latest blogs
Optimizely Commerce 14 Now Supports .NET 10

In my previous post, Optimizely CMS 12 Now Fully Supports .NET 10 , I noted one remaining limitation: Commerce 14 (Commerce Connect) did not yet...

Bien Nguyen | Aug 7, 2026

The Top 10 Things I'm Actually Using AI For

A while back I wrote about ReviewPR, an Azure Function that uses AI to review Azure DevOps pull requests. That was one specific use case, but over...

KennyG | Aug 5, 2026

Drag-and-Drop Reordering for Commerce Media Collection in Optimizely Commerce Connect

Optimizely Commerce Connect ships a polished asset editor for the CommerceMediaCollection  property on catalog entries. It lets editors add, remove...

Linh Doan Cuu | Aug 5, 2026

Order tabs with drag and drop V2

I earlier did a very simple Blazor version to be able to sort tabs with drag and drop. I wanted to update it a bit and also display how the tabs ar...

Per Nergård (MVP) | Aug 4, 2026