Filtered display options menu based on content type in EPiServer
This blog post is kind of part 2 of my earlier post How to limit rendering to specific display options and set default display option for a block. I end it with the following question: "Is it possible to disable the display option select item in the context menu if no template is found to support it? Please let me know. It bugs me a lot.".
Since nobody answered my question I just had to figure it out myself. So, here it is. Maybe it's not the most pretty solution but it works.
Specification
I have these display options defined for my site:

For a given block I only want support for Full and Half width so I want the menu filtered into this:

The display option tags is defined in code like this:
public static class DisplayOptions
{
public const string FullWidth = "FullWidth";
public const string ThreeFourthsWidth = "ThreeFourthsWidth";
public const string TwoThirdsWidth = "TwoThirdsWidth";
public const string HalfWidth = "HalfWidth";
public const string OneThirdWidth = "OneThirdWidth";
public const string OneFourthWidth = "OneFourthWidth";
}
Server side code
ISpecialRenderingContent interface
I need a way to set the supported display options for a content type so I created this interface to implement.
public interface ISpecialRenderingContent
{
string[] SupportedDisplayOptions { get; }
}
Block example
[ContentType(DisplayName = "My full and half width block", GUID = "some-guid")]
public class MyBlock : BlockData, ISpecialRenderingContent
{
public virtual string Heading { get; set; }
public virtual XhtmlString MainBody { get; set; }
public string[] SupportedDisplayOptions
{
get
{
return new[]
{
DisplayOptions.FullWidth,
DisplayOptions.HalfWidth
};
}
}
}
RestStore - SupportedDisplayOptionsStore
I need some way to get the supported display options for a given ContentReference from my client script code (I'll get back to that). I decided to create a simple RestStore for this.
[RestStore("supporteddisplayoptions")]
public class SupportedDisplayOptionsStore : RestControllerBase
{
private readonly IContentLoader _contentLoader;
private readonly DisplayOptions _displayOptions;
public SupportedDisplayOptionsStore(IContentLoader contentLoader,
DisplayOptions displayOptions)
{
if (contentLoader == null) throw new ArgumentNullException("contentLoader");
if (displayOptions == null) throw new ArgumentNullException("displayOptions");
_contentLoader = contentLoader;
_displayOptions = displayOptions;
}
[HttpGet]
public RestResultBase Get(string id)
{
ContentReference contentLink;
if (!ContentReference.TryParse(id, out contentLink))
{
return Default();
}
IContent content;
if (!_contentLoader.TryGet(contentLink, out content))
{
return Default();
}
var specialRenderingContent = content as ISpecialRenderingContent;
if (specialRenderingContent != null)
{
var supportedDisplayOptions =
_displayOptions
.Where(x => specialRenderingContent.SupportedDisplayOptions.Contains(x.Tag));
return Rest(supportedDisplayOptions.Select(x => x.Id));
}
// Default to all display options.
return Default();
}
private RestResultBase Default()
{
return Rest(_displayOptions.Select(x => x.Id));
}
}
Client side code
~/ClientResources/Scripts/ModuleInitializer.js
To register the rest store I created a module initializer.
define([
"dojo",
"dojo/_base/declare",
"epi/_Module",
"epi/dependency",
"epi/routes"
], function (
dojo,
declare,
_Module,
dependency,
routes
) {
return declare("app.ModuleInitializer", [_Module], {
initialize: function () {
this.inherited(arguments);
var registry = this.resolveDependency("epi.storeregistry");
//Register the store
registry.create("supporteddisplayoptions", this._getRestPath("supporteddisplayoptions"));
},
_getRestPath: function (name) {
return routes.getRestPath({ moduleArea: "app", storeName: name });
}
});
});
~/ClientResources/Scripts/CacheManager.js
I don't want to call the rest store every time an editor selects a display option so I created a simple manager to cache them on the client side.
define([
"dojo/_base/array",
"dojo/_base/declare",
"dojo/_base/lang"
], function(
array,
declare,
lang
) {
return declare("app.CacheManager", null, {
_data: null,
_cacheLength: null,
_length: null,
constructor: function (args) {
this._data = {};
this._cacheLength = 50;
this._length = 0;
},
add: function(key, value) {
if (this._length > this._cacheLength) {
this.flush();
}
if (!this._data[key]) {
this._length++;
}
this._data[key] = value;
},
load: function(key) {
if (this._length < 1) {
return null;
}
if (this._data[key]) {
return this._data[key];
}
return null;
},
flush: function() {
this._data = {};
this._length = 0;
}
});
});
~/ClientResources/Scripts/widget/DisplayOptionSelector.js
This is the "kind of" ugly part of this. I had to extract the built-in display option selector code from EPiServer and modify it to my needs. The namespace and class name is important and shouldn't be modified.
define("epi-cms/widget/DisplayOptionSelector", [
"dojo/_base/array",
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/when",
"dijit/MenuSeparator",
"epi/dependency",
"epi-cms/widget/SelectorMenuBase",
// Resouces
"epi/i18n!epi/cms/nls/episerver.cms.contentediting.editors.contentarea.displayoptions",
// Widgets used in template
"epi/shell/widget/RadioMenuItem",
"app/CacheManager"
], function (
array,
declare,
lang,
when,
MenuSeparator,
dependency,
SelectorMenuBase,
// Resouces
resources,
RadioMenuItem,
CacheManager
) {
return declare([SelectorMenuBase], {
// summary:
// Used for selecting display options for a block in a content area
//
// tags:
// internal
// model: [public] epi-cms.contentediting.viewmodel.ContentBlockViewModel
// View model for the selector
model: null,
// _resources: [private] Object
// Resource object used in the template
headingText: resources.title,
_rdAutomatic: null,
_separator: null,
// Custom properties for hacked implementation.
supportedDisplayOptions: null,
_cacheManager: null,
// Adapted from EPiServer and slightly modified.
// Added initialization of cache manager.
postCreate: function () {
// summary:
// Create the selector template and query for display options
this.inherited(arguments);
this.own(this._rdAutomatic = new RadioMenuItem({
label: resources.automatic,
value: ""
}));
this.addChild(this._rdAutomatic);
this._rdAutomatic.on("change", lang.hitch(this, this._restoreDefault));
this.own(this._separator = new MenuSeparator({ baseClass: "epi-menuSeparator" }));
this.addChild(this._separator);
this._cacheManager = this._getCacheManager();
},
// Adapted from EPiServer. No modifications.
_restoreDefault: function () {
this.model.modify(function () {
this.model.set("displayOption", null);
}, this);
},
// Adapted from EPiServer. No modifications.
_setModelAttr: function (model) {
this._set("model", model);
this._setup();
},
// Adapted from EPiServer. No modifications.
_setDisplayOptionsAttr: function (displayOptions) {
this._set("displayOptions", displayOptions);
this._setup();
},
// Adapted from EPiServer and modified.
// Called on load and when a display option is selected.
_setup: function () {
if (!this.model || !this.displayOptions) {
return;
}
//Destroy the old menu items
this._removeMenuItems();
var selectedDisplayOption = this.model.get("displayOption");
if (this.supportedDisplayOptions == null) {
this._setSupportedDisplayOptions(selectedDisplayOption);
} else {
this._setMenuItems(this.supportedDisplayOptions, selectedDisplayOption);
}
},
// Custom method. Called in _setup method. Calls the rest store to
// get supported display options. The result is cached.
_setSupportedDisplayOptions: function (selectedDisplayOption) {
var cacheKey = this._getCacheKey();
var cachedData = this._cacheManager.load(cacheKey);
if (cachedData != null) {
this.supportedDisplayOptions = cachedData;
this._setMenuItems(this.supportedDisplayOptions, selectedDisplayOption);
return;
}
var storeRegistry = dependency.resolve("epi.storeregistry");
var store = storeRegistry.get("supporteddisplayoptions");
when(store.get(this.model.contentLink), lang.hitch(this, function (supportedDisplayOptions) {
this.supportedDisplayOptions = array.filter(this.displayOptions, function (displayOption) {
return array.indexOf(supportedDisplayOptions, displayOption.id) > -1;
});
// Add to cache
this._cacheManager.add(this._getCacheKey(), this.supportedDisplayOptions);
// Create menu items.
this._setMenuItems(this.supportedDisplayOptions, selectedDisplayOption);
}), lang.hitch(this, function (err) {
// An error occured. Fallback to unfiltered/standard display options.
this.supportedDisplayOptions = this.displayOptions;
// Create menu items.
this._setMenuItems(this.supportedDisplayOptions, selectedDisplayOption);
}));
},
// Extracted from the original _setup method.
_setMenuItems: function (displayOptions, selectedDisplayOption) {
array.forEach(displayOptions, function (displayOption) {
var item = new RadioMenuItem({
label: displayOption.name,
iconClass: displayOption.iconClass,
displayOptionId: displayOption.id,
checked: selectedDisplayOption === displayOption.id,
title: displayOption.description
});
this.own(item.watch("checked", lang.hitch(this, function (property, oldValue, newValue) {
if (!newValue) {
return;
}
// Modify the model
this.model.modify(function () {
this.model.set("displayOption", displayOption.id);
}, this);
})));
this.addChild(item);
}, this);
this._rdAutomatic.set("checked", !selectedDisplayOption);
},
// Adapted from EPiServer. No modifications.
_removeMenuItems: function () {
var items = this.getChildren();
items.forEach(function (item) {
if (item === this._rdAutomatic || item == this._separator) {
return;
}
this.removeChild(item);
item.destroy();
}, this);
},
// Custom method. Gets the global cache manager.
_getCacheManager: function () {
window.supportedDisplayOptionsCache = window.supportedDisplayOptionsCache || new CacheManager();
return window.supportedDisplayOptionsCache;
},
// Custom method. Gets the supported display options cache key for current content.
_getCacheKey: function() {
return "SupportedDisplayOptions-" + this.model.contentLink;
}
});
});
Wrapping it up
For all this to work you need to update/create module.config with this code:
<?xml version="1.0" encoding="utf-8"?>
<module clientResourceRelativePath="" loadFromBin="false">
<assemblies>
<!-- The assembly containing the rest store needs to be added. -->
<add assembly="MyAssemblyName" />
</assemblies>
<dojo>
<paths>
<add name="app" path="ClientResources/Scripts" />
</paths>
</dojo>
<clientModule initializer="app.ModuleInitializer">
<moduleDependencies>
<add dependency="CMS" type="RunAfter" />
</moduleDependencies>
</clientModule>
<clientResources>
<!-- Inject our custom Display Option selector -->
<add name="epi-cms.widgets.base"
path="~/ClientResources/Scripts/widget/DisplayOptionSelector.js"
resourceType="Script"
/>
</clientResources>
</module>
Kontakta oss