The PropertyList is the new property introduced in EPiServer 9. It allows to use generic list on content model. List item property could be described with the same attributes as regular model properties. For example if we annotate property with Display attribute and set name, then name will be used as grid column header and as a label on the edit form. If we set the UiHint, like StringList then editing form will use selected editor.
There is an excellent Per Magne Skuseth tutorial that clearly describe how to use the list editor. Article can be found on the EPiServer blog.
List is represented as a grid. Each list element is a single grid row. There are two built-in data type formatters – for DateTime and Boolean. All other properties are converted directly to string. So for example if we set UiHint to UIHint.Image, then the editing form will use media selector.
… but the grid will show number (ContentReference).
I tried to improve this behavior and display image thumbnails on the list. It’s implemented by adding a custom formatter for images. Formatter is a function that takes unformatted grid cell value as input parameter and returns formatted value (it could be HTML).
Based on the Per’s example and added image property to the Contact class. The image is ContentReference and use UIHint.Image:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Contact { [UIHint(UIHint.Image)] public ContentReference Image { get; set; } public string Name { get; set; } public int Age { get; set; } [Display(Name = "Phone number")] public int PhoneNumber { get; set; } } |
To get thumbnailUrl for ContentReference we need to additionally load the content from repository. Unfortunately, grid formatters doesn’t allow to return deferred results from formatters, so code that returns promise won’t work:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function format(contentlink) { var def = new Deferred(); var registry = dependency.resolve("epi.storeregistry"); var store = registry.get("epi.cms.content.light"); var contentData; dojo.when(store.get(contentlink), function(contentData) { def.resolve(contentData.previewUrl); }); return def.promise; } |
It means that we need to have all conntentReference->URL mappings before rendering. All images for existing items will be returned from the server and all new images will be resolved during closing the edit dialog.
The server list will be provided using IMetadataAware attribute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class ImageThumbnailExtender : Attribute, IMetadataAware { public void OnMetadataCreated(ModelMetadata metadata) { var extendedMetadata = (ExtendedMetadata)metadata; var collection = ((ExtendingPropertyList.Models.Pages.ContactListProperty)metadata.Model).List .Where(cr => cr.Image != null) .Select(cr => new { id = cr.Image.ID, imageUrl = ServiceLocator.Current.GetInstance<IContentRepository>().Get<ImageFile>(cr.Image).PreviewUrl() }); extendedMetadata.EditorConfiguration.Add("mappedImages", collection); } } |
And client code for new images is handled in onExecuteDialog method:
1 2 3 4 5 6 7 8 9 10 11 12 |
onExecuteDialog: function () { var item = this._itemEditor.get("value"); var resolveImageMapping = extendedFormaters.resolveImageMapping(item.image); resolveImageMapping.then(lang.hitch(this,function () { if (this._editingItemIndex !== undefined) { this.model.saveItem(item, this._editingItemIndex); } else { this.model.addItem(item); } })); } |
To use image formatter we need to create new widget. It will extend original epi-cms/contentediting/editors/CollectionEditor editor and apply formatting and override onExecuteDialog method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
define([ "dojo/_base/array", "dojo/_base/declare", "dojo/_base/lang", "epi-cms/contentediting/editors/CollectionEditor", "alloy/editors/extendedFormatters" ], function ( array, declare, lang, CollectionEditor, extendedFormaters ) { return declare([CollectionEditor], { _getGridDefinition: function() { var result = this.inherited(arguments); for (var i = 0; i < this.mappedImages.length; i++) { extendedFormaters.setImageMapping(this.mappedImages[i].id, this.mappedImages[i].imageUrl); } result.image.formatter = extendedFormaters.imageFormatter; return result; }, onExecuteDialog: function () { var item = this._itemEditor.get("value"); var resolveImageMapping = extendedFormaters.resolveImageMapping(item.image); resolveImageMapping.then(lang.hitch(this,function () { if (this._editingItemIndex !== undefined) { this.model.saveItem(item, this._editingItemIndex); } else { this.model.addItem(item); } })); } }); }); |
Formatter is added as a separated file and connected with as a dependency.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
define([ // dojo "dojo/_base/lang", "dojo/Deferred", "epi/dependency" ], function ( // dojo lang, Deferred, dependency ) { function resolveContentData (contentlink, callback) { if (!contentlink) { return null; } var registry = dependency.resolve("epi.storeregistry"); var store = registry.get("epi.cms.content.light"); var contentData; dojo.when(store.get(contentlink), function(returnValue) { contentData = returnValue; callback(contentData); }); return contentData; }; var images = {}; var extendedFormatters = { imageFormatter: function (value) { if (!value) { return '-'; } if (!images[value]) { return value; } return "<img style='max-height: 100px;' src='" + images[value] + "'/>"; }, resolveImageMapping: function(contentLink) { var def = new Deferred(); resolveContentData(contentLink, function(contentData) { images[contentLink] = contentData.previewUrl; def.resolve(); }); return def.promise; }, setImageMapping: function(contentLink, imageUrl) { images[contentLink] = imageUrl; } } return extendedFormatters; }); |
To use new solution we need to add ClientEditor and ImageThumbnailExtender on the property of the model.
1 2 3 4 5 6 7 |
public class ProductPage : StandardPage { [EditorDescriptor(EditorDescriptorType = typeof(CollectionEditorDescriptor<Contact>))] [ClientEditor(ClientEditingClass = "alloy/editors/ExtendedCollectionEditor")] [ImageThumbnailExtender] public virtual IList<Contact> Contacts { get; set; } } |
After running application the editor will display thumbnails for images.
The source code is available on Gist.