Monday, October 27, 2014

Page tree icons in EPiServer CMS 7.5

One appreciated feature we had in our old EPiServer 6 framework here at Nansen was the ability to set different icons for different page types in the page tree. This makes it easier for editors to distinguish between different page types to generally make

CMS6 pagetree with custom icons
their life easier. And we all know the happier the editor, the better the content, and better content make for more visitors, right?

Anyways, the EPi 6 system we built was a bit of a hack, we added a ControlAdapter to the PageTree control to inject css classes on the different nodes depending on what page type it was.

Now in EPiServer 7.5 there are systems in place to make this a lot cleaner, using the tools provided by the CMS UI we can avoid using hacks, which make for a cleaner approach that will hopefully not break as easily should the underlying structure change in future updates.

The most basic way is to add a editor descriptor for your class, like so:

 [UIDescriptorRegistration]
 public class ContainerPageUIDescriptor : UIDescriptor
 {
  public ContainerPageUIDescriptor()
   : base(ContentTypeCssClassNames.Container)
  {
   DefaultView = CmsViewNames.AllPropertiesView;
  }
 }
Now this approach will get tedious once you start adding page types as you will need one UI descriptor for each page type. There are several ways to make this more streamlined. This post will show you one approach using attributes on your page type classes.

We are going to use the EPiServer.Shell.UIDescriptor objects registered for all content types, these are added to the EPiServer.Shell.UIDescriptorRegistry during site startup. The UIDescriptor contain one important property that makes all of this work, IconClass. This class is added to the icon shown for each item in the page tree.

Requirements

I used EPiServer CMS v7.14.1 and EPiServer CMS UI version 7.15.0 when building this.

The parts

We will need to glue together three different parts to achieve this:

  1. An attribute for page (content) type classes, where a icon class can be chosen
  2. An initialization module to add the icon class to the page types
  3. Some icons

1. The attribute

The attribute is pretty straight forward, all we need is the ability to select a css class for our content types:
 [AttributeUsage(AttributeTargets.Class)]
 public class ContentIconAttribute : Attribute
 {
  public ContentIconAttribute(string iconClass)
  {
   IconClass = iconClass;
  }

  /// <summary>
  /// Css class to apply to the icon
  /// </summary>
  public string IconClass { get; set; }
 }

2. The initialization module

The initialization module is invoked after the EPiServer.Shell.UIDescriptorRegistry is populated, and for each content type that have our custom attribute, append the css class to the IConClass attribute. This is, as per usual for attributes, done using reflection, but as it only happen once during site startup, the preformance impact is of no issue.
/// <summary>
/// Used in conjunction with <see cref="ContentIconAttribute"/> to add custom content icons for the page tree.
/// </summary>
/// <remarks><see cref="ContentIcon"/> and <see cref="ContentIconAttribute"/> for more information</remarks>
[InitializableModule]
[ModuleDependency(typeof(EPiServer.Cms.Shell.InitializableModule))]
public class ContentTypeUIDescriptorInitializer : IInitializableModule
{
 public void Initialize(InitializationEngine context)
 {
  var registry = context.Locate.Advanced.GetInstance<UIDescriptorRegistry>();
  var classes = GetDescriptorClasses();
  foreach(var descriptor in registry.UIDescriptors) {
   if(classes.ContainsKey(descriptor.ForType)) {
    descriptor.IconClass += classes[descriptor.ForType];
   }
  }
 }

 public void Uninitialize(InitializationEngine context) {}

 public void Preload(string[] parameters) {}

 private Dictionary<Type, string> GetDescriptorClasses()
 {
  var types = AppDomain.CurrentDomain.GetAssemblies()
        .SelectMany(ass => ass.GetTypes().Where(type => type.IsDefined(typeof(ContentIconAttribute), false)));

  var descriptors = from type in types
   select new {
    type,
    iconClass = ((ContentIconAttribute)Attribute.GetCustomAttribute(type, typeof(ContentIconAttribute))).IconClass
   };

  return descriptors.ToDictionary(key => key.type, value => value.iconClass);
 }
}
The initializer is pretty straightforward, it finds all classes with out ContentIcon attribute and use that to modify the registered UIDescriptors by appending our IconClass. One important detail is that it has a dependency on EPiServer.Cms.Shell.InitializableModule, which ensure it is initialized after the UIDescriptorRegistry is populated with all the UIDescriptors.

3. The icons

As all we can do is add a css class to each item in the page tree, we need icons that can be displayed using only a css class. This can be achieved in a number of ways, like adding the css for each icon, like so:

.myPageTypeIcon {
  background: url('../Images/Icons/pageIcons16x16.png') 0px -16px no-repeat;
  height: 16px;
  width: 16px;
}
This approach is almost as tedious as making one UI descriptor class for each page type, this is no good. A faster way is to use a icon library, where all icons are already configured. Here, we picked Icomoon (https://icomoon.io/), they have a free library of about 400 icons in a convenient woff font format.

I did make a few small changes to the css supplied, I added a prefix as the class names were very generic, and it didn't display entirely correct, the icons were compacted to 13px instead of 16px. Anyways, here is the base css for the icomoon font icons:
[class^="icomoon-icon-"], [class*=" icomoon-icon-"] {
 font: normal normal normal 16px/1 icomoon;
 speak: none;
 text-transform: none;
 line-height: 1;
 /* Better Font Rendering =========== */
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
}

.icomoon-icon-home:before {
 content: "\e600";
}
…
.icomoon-icon-IcoMoon:before {
 content: "\e7c2";
}

As you can see, I prefixed each icon with icomoon, and also modified the font directive in the base class slightly.

Once you have your icons, place everything somewhere under /ClientResources, In this example I placed them under clientresources/styles/icomoon, and modify your module.config file in the web root:

<clientResources>
  <add name="epi-cms.widgets.base" path="Styles/icomoon/icomoon.css" resourceType="Style"/>
<clientResources>

3b. Convenience factor - icon names

As there are a lot of icons, you can add constants for all the icons by using some clever search and replace, like so:

 public class ContentIcon
{
 /// <summary>
 /// IcoMoon free icons, see more here https://icomoon.io/#preview-free
 /// </summary>
 public static class IcoMoon
 {
  public const string accessibility = "icomoon-icon-accessibility";
  public const string address_book = "icomoon-icon-address-book";
  public const string aid = "icomoon-icon-aid";
  public const string airplane = "icomoon-icon-airplane";
  public const string alarm = "icomoon-icon-alarm";
  public const string alarm2 = "icomoon-icon-alarm2";
  …
  public const string youtube = "icomoon-icon-youtube";
  public const string youtube2 = "icomoon-icon-youtube2";
  public const string zoomin = "icomoon-icon-zoomin";
  public const string zoomout = "icomoon-icon-zoomout";
 }
}

Using it all

After gluing it all together, you can now in a simple way add page tree icons to your content using just one attribute per content item. The constant class help you avoid misspelling anything, and you can also use the icomoon website as quick reference for how each icon look:

[ContentType(
 GUID = "00000000-0000-0000-0000-000000000000",
 DisplayName = "Section page",
 GroupName = SystemTabNames.Settings)]
[ContentIcon(ContentIcon.IcoMoon.copy)]
public class MyVeryOwnPage : PageData
{
}

Finally

Here is the result in all its glory:
The fruits of our labor
You can get the c# code source parts from here: http://pastebin.com/znYXReXc