Don't miss out Virtual Happy Hour this Friday (April 26).

Try our conversational search powered by Generative AI!

KennyG
Oct 1, 2020
  1659
(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
Solving the mystery of high memory usage

Sometimes, my work is easy, the problem could be resolved with one look (when I’m lucky enough to look at where it needs to be looked, just like th...

Quan Mai | Apr 22, 2024 | Syndicated blog

Search & Navigation reporting improvements

From version 16.1.0 there are some updates on the statistics pages: Add pagination to search phrase list Allows choosing a custom date range to get...

Phong | Apr 22, 2024

Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog