Mark Stott
Aug 18, 2021
  1858
(3 votes)

Extending The Admin Interface in Optimizely CMS 12

In Optimizely CMS 11, developers could extend functionality with the Admin mode of the CMS by decorating standard MVC Controllers with the GuiPlugIn attribute.  The pages would then be rended within an iFrame to preserve the admin menus.  With the Optimizely CMS 12 .NET 5.0 preview, the GuiPlugIn attribute has been removed. According to the Breaking Changes in CMS 12 article, developers are instead advised to implement a Menu Provider instead.  Extending the Admin mode of the CMS turned out to be relatively simple using this paradigmn.

First off, we are going to create a simple controller for our new Admin function.  Note that there are no specific Optimizely references or decorators used within this controller, I have used a standard Microsoft Authorize attribute to ensure only the right users are able to use this functionality.

namespace OptimizelyTwelveTest.Features.CustomAdmin
{
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;

    [Authorize(Roles = "CmsAdmin,WebAdmins,Administrators")]
    [Route("[controller]")]
    public class CustomAdminPageController : Controller
    {
        [Route("[action]")]
        public IActionResult Index()
        {
            return View();
        }
    }
}

Next we need to set up a new menu provider; I needed to both decorate the class with the [MenuProvider] attribute and ensure that the class implemented the IMenuProvider interface in order for Optimizely CMS to recognise and render these menus.

namespace OptimizelyTwelveTest.Features.CustomAdmin
{
    using System.Collections.Generic;
    using EPiServer.Shell.Navigation;

    [MenuProvider]
    public class CustomAdminMenuProvider : IMenuProvider
    {
        public IEnumerable<MenuItem> GetMenuItems()
        {
            var urlMenuItem1 = new UrlMenuItem("Custom Admin Page", "/global/cms/admin/csp", "/CustomAdminPage/Index");
            urlMenuItem1.IsAvailable = context => true;
            urlMenuItem1.SortIndex = 100;
    
            return new List<MenuItem>(1)
            {
                urlMenuItem1
            };
        }
    }
}

The Menu Provider can be used to return a collection of UrlMenuItem and SectionMenuItem objects.  The UrlMenuItem takes three parameters, a display name, a path and a url while the SectionMenuItem only takes a display name and a path.  The path defines where in the standard episerver menu that the link will be rendered.  In the above case, I wanted the menu item to be rendered specifically within the admin section of the CMS.  Both object types expose a method for IsAvailable that can be assigned a function that takes a HttpContext which can be used to customise under what circumstances the menu would be visible to the user.  In the above case I have set this as always being visible.

When I navigate to the admin section of the CMS I can now clearly see my menu item:

As previously stated, when you click on this menu item, your custom admin page is no longer rendered in an iFrame and instead you are sent directly to your custom admin page.  This means that the user can no longer see the CMS Admin menu which can be a jarring experience for the user.  Thankfully Optomizely CMS 12 also comes with a razor helper method that renders the admin menu when you call @Html.CreatePlatformNavigationMenu(), but you will need to tweak your styles to ensure that the menu does not show as an overlay on top of your custom admin page.

@using EPiServer.Framework.Web.Resources
@using EPiServer.Shell.Navigation

@{
    Layout = string.Empty;
}

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Content Security Policy Management</title>

    <!-- Shell -->
    @ClientResources.RenderResources("ShellCore")

    <!-- LightTheme -->
    @ClientResources.RenderResources("ShellCoreLightTheme")
</head>
<body>
    @Html.CreatePlatformNavigationMenu()
    <div @Html.ApplyPlatformNavigation()>
        <h1>
            My Custom Admin Page
        </h1>
    </div>
</body>
</html>

This razor file will render as below, note that the menu rendering is smart enough to correctly highlight the Custom Admin Page menu item.

A Custom Admin Menu Section

Now lets say you want to create a new top level CMS menu section which in turn has two or more functions on the secondary menu like so:

You would need to define multiple UrlMenuItems within your MenuProvider.  No hierarchy of objects is required here as Optimizely organises your menu items based on the path values that you provide for each menu item.  I made the above work with the following MenuProvider; please note that I have not used a SectionMenuItem as the CMS user still needs something to happen when they click on the top menu for "Custom Admin Module".

namespace OptimizelyTwelveTest.Features.CustomAdmin
{
    using System.Collections.Generic;
    using EPiServer.Shell.Navigation;

    [MenuProvider]
    public class CustomAdminMenuProvider : IMenuProvider
    {
        public IEnumerable<MenuItem> GetMenuItems()
        {
            var adminModule = new UrlMenuItem("Custom Admin Module", "/global/cms/customadmin", "/CustomAdminPage/Index");
            adminModule.IsAvailable = context => true;
            adminModule.SortIndex = 100;
            var urlMenuItem1 = new UrlMenuItem("Custom Admin Page 1", "/global/cms/customadmin/pageone", "/CustomAdminPage/FunctionOne");
            urlMenuItem1.IsAvailable = context => true;
            urlMenuItem1.SortIndex = 100;
            var urlMenuItem2 = new UrlMenuItem("Custom Admin Page 2", "/global/cms/customadmin/pagetwo", "/CustomAdminPage/FunctionTwo");
            urlMenuItem2.IsAvailable = context => true;
            urlMenuItem2.SortIndex = 100;

            return new List<MenuItem>(3)
            {
                adminModule,
                urlMenuItem1,
                urlMenuItem2
            };
        }
    }
}

Please do let me know if this post has been useful to you and feel free to ask any questions :)

Aug 18, 2021

Comments

David Knipe
David Knipe Aug 18, 2021 02:00 PM

Hey Mark

Thanks for sharing. Its worth noting that if you wanted to go all out and start building the admin extension in React than there are some NPM packages to help render the platform navigation and also some of the new UI components that the updated admin UI is built with:

https://www.npmjs.com/package/@episerver/ui-framework 

https://www.npmjs.com/package/@episerver/platform-navigation 

David

Arjan Paauw
Arjan Paauw May 3, 2022 04:09 AM

Really helpful thanks. 
I'm using an admin page with some features that call actions on a controller.
So the url changes from /CustomAdminPage/Index to /CustomAdminPage/DoStuff.
The (sub)navigation "Custom Admin Page 1" disappears however.. how to keep it?  

Mark Stott
Mark Stott May 3, 2022 07:30 AM

Hello Arjan,

I've had a similar need to yourself in regards to this.  I'm currently working on a second plugin for use by the community, but instead of using traditional MVC post back events, I created a single landing page using a standard MVC controller and on the razor file I have added a React App.  The React App has led to a lot of learning and a much richer interface by combining react and react-bootstrap components.  I'll be show casing this at the next EMEA Developer Happy Hour on May 27th 2022: https://world.optimizely.com/community/optimizely-dev-happy-hours/happy-hour-emea/ #ShamelessPlug :)

If you don't want to go down the react route, then you'll have to declare all of your MVC controllers and actions in your IMenuProvider implementation.

Assuming this controller, I want the Index menu item to be visible in the menu, but I want the SomethingElse action to render the same menu context but not to show up as it's own menu item.

    [Authorize(Roles = "CmsAdmin,WebAdmins,Administrators")]
    public class MyPageController : Controller
    {
        [HttpGet]
        [Route("[controller]/[action]")]
        public IActionResult Index()
        {
            return View();
        }

        [HttpGet]
        [Route("[controller]/[action]")]
        public IActionResult SomethingElse()
        {
            return View("Index");
        }
    }

My IMenuProvider then looks like this:

        public IEnumerable<MenuItem> GetMenuItems()
        {
            var listMenuItem = new UrlMenuItem("CSP", "/global/cms/admin/my.module", "/MyPage/Index")
            {
                IsAvailable = context => true,
                SortIndex = SortIndex.Last + 1,
                AuthorizationPolicy = CmsPolicyNames.CmsAdmin
            };

            var listMenuItem2 = new UrlMenuItem("CSP", "/global/cms/admin/my.module", "/MyPage/SomethingElse")
            {
                IsAvailable = context => false,
                SortIndex = SortIndex.Last + 1,
                AuthorizationPolicy = CmsPolicyNames.CmsAdmin
            };

            return new List<MenuItem> { listMenuItem, listMenuItem2 };
        }

I hope that helps :)

Mark Stott
Mark Stott May 3, 2022 07:52 AM

I should add that I have been trying the IMenuProvider solution this morning on CMS 12.1.0 and I can't get the action child action not to show as per this help article: https://world.optimizely.com/documentation/developer-guides/CMS/user-interface/extending-the-navigation/how-to-highlight-parent-menu-items/

            var listMenuItem2 = new UrlMenuItem(string.Empty, "/global/cms/admin/my.module/SomethingElse", "/MyPage/SomethingElse")
            {
                IsAvailable = context => false,
                SortIndex = SortIndex.Last + 1,
                AuthorizationPolicy = CmsPolicyNames.CmsAdmin
            };

Even when I change the menu text to string.Empty, and IsAvailable to false I can't get the child item to not render in the menu.  You may well be better suited to using posting changes via Ajax events rather than using MVC post back events.  You can look how I'm taking that approach here: https://github.com/GeekInTheNorth/Stott.Optimizely.RobotsHandler/tree/main/src/Stott.Optimizely.RobotsHandler

Please login to comment.
Latest blogs
Optimizely community meetup - Sept 29 (virtual + Melbourne)

Super excited to be presenting this Thursday the 29th of September at the Optimizely community meetup. For the full details and RSVP's see the...

Ynze | Sep 27, 2022 | Syndicated blog

Preview multiple Visitor Groups directly while browsing your Optimizely site

Visitor groups are great - it's an easy way to add personalization towards market segments to your site. But it does come with it's own set of...

Allan Thraen | Sep 26, 2022 | Syndicated blog

The Report Center is finally back in Optimizely CMS 12

With Episerver.CMS.UI 12.12.0 the Report Center is finally re-introduced in the core product.

Tomas Hensrud Gulla | Sep 26, 2022 | Syndicated blog

Dynamic Route in ASP.NET Core When MapDynamicControllerRoute Does Not Work

Background Creating one of the add-on for Optimizely I had to deal with challenge to register dynamically route for the API controller. Dynamic rou...

valdis | Sep 25, 2022 | Syndicated blog