Enabling out of the box editing with the SelectMany attribute for a IList of ContentReferences
Episervers built in property type List<ContentReference> is a very powerful feature. At Epinova we've used this for adding relations between items, for instance adding tags where a tag might be another content item. However, there's an issue with editing this property in Episerver that you need to fix in order to use the out of the box editing. In this post I'll show how to solve this.
In a recent project, I wanted to enable a tag property where tags were the pages under a given node. This would be done through editor selection of one or many pages, where the pages where to be automatically fetched using a selection factory, to enable an easy to use system for editors where they could simply add more categories, that would also work as landing pages for the tags. I already had the selection factory in place for single "category" selections (ContentReference), so I simply added another property using the IList pattern:
[SelectMany(SelectionFactoryType = typeof(UnionCategorizationPageSelectionFactory))]
public virtual IList<ContentReference> Tags { get; set; }
Opening the editorial interface, the property is surely displayed with check boxes, just as expected. However, once you start editing the property, we are presented with an error saying that Episerver can't convert the property when saving:
That's strange. The editorial UI should automatically send the property value, in this case in form of an JSON array, to the server that would then use JSON.NET to parse this into the property value. Time for some investigation...
SelectMany uses the Dijit widget CheckBoxListEditor which is part of the epi-cms client side package. To view the uncompressed JavaScript files or even enable debugging of these in the browser, I always turn to Ben McKernans old blog post about this. You can unzip the package inside the debug version and simply browse through all the client side files for the editorial UI.
Looking through the code for the CheckBoxListEditor we can see that there is a lot of logic conscerning values that differes depending on a property named valueIsCsv
var values = [];
if (value)
{
values = this.valueIsCsv ? value.split(",") : value;
}
Looking at the property declaration, you can see that the default value for this property is true. This means that Episerver expects values to be joined as a comma-separated string before sending to the server by default. This is a bad choice according to me, and it probably has historical reasons. Changing this would also require a breaking change that is hard to find for anyone dependant on this, since it will not give any compile time errors.
So how can we solve this? I made a blog post about how you can send settings for a property editor, for instance overriding a default setting, a number of years back. Using those practices, we can create a custom editor descriptor that overrides the default behaviour.
using EPiServer.Core;
using EPiServer.Shell.ObjectEditing;
using EPiServer.Shell.ObjectEditing.EditorDescriptors;
using System;
using System.Collections.Generic;
namespace MySite.Infrastructure.Properties
{
[EditorDescriptorRegistration(TargetType = typeof(IList<ContentReference>), EditorDescriptorBehavior = EditorDescriptorBehavior.ExtendBase)]
public class ContentReferenceListEditorDescriptor : EditorDescriptor
{
public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
{
//Episervers check box editor handles valus as csv, aka comma separated sting, as default.
//Need to make sure that the value is handled as an array.
metadata.EditorConfiguration["valueIsCsv"] = false;
}
}
}
Note: We are using EditorDescriptorBehavior = EditorDescriptorBehavior.ExtendBase. This will ensure that this code is run after the default built in editor descriptor has been run. This will affect all IList<ContentReference> in the solution, possible even properties that are built in or from add-ons. You can always scope the change by adding a UIHint to the property definition as well as the editor descriptor registration:
Change editor descriptor registration to:
[EditorDescriptorRegistration(TargetType = typeof(IList<ContentReference>), UIHint = "contentreferencelist", EditorDescriptorBehavior = EditorDescriptorBehavior.ExtendBase)]
Add add a UIHint attribute to the properties that you want to affect:
[SelectMany(SelectionFactoryType = typeof(UnionCategorizationPageSelectionFactory))]
[UIHint("contentreferencelist")]
public virtual IList<ContentReference> Tags { get; set; }
Note: For production code, place the UI hint in a static string instead of using a hard coded string around your different classes.
With this in place, we can now successfully edit and update IList<ContentReference> properties.