Since few months PropertyList<int> property is available in EPiServer. It allows to create list of blocks. List value is serialized to JSON and stored together with Page. The property doesn’t allow to work with simple types like strings or integers. An example of string list property can be found in AlloyTech demo. Editing widget is represented by textarea, so the editor needs to know that items are separated with line breaks. For other types like numbers or dates I didn’t find the implementation. That’s why I prepared PropertyValue<int> property where T is a simple type like string, integer or date.
PropertyValueList architecture
I had to introduce new backing types, editor descriptors and client editing widget. For common functionality of backing types and editors descriptors I use abstract classes.
If we want to work with a new simple type we have to implement two classes. First class will inherit from PropertyValueListBackingType<T> and the second one from PropertyValueListEditorDescriptor<T>. It’s just few lines of code. For example for IList<int> the backing type implementation:
1 2 3 4 5 6 7 8 9 10 |
[EditorHint(PropertyIntListEditorDescriptor.UIHint)] [PropertyDefinitionTypePlugIn(DisplayName = "Int List", Description = "Int List")] [Serializable] public class PropertyIntListBackingType : PropertyValueListBackingType<int> { protected override PropertyValueListBackingType<int> CreateSelfInstance() { return new PropertyIntListBackingType(); } } |
and for editor descriptor:
1 2 3 4 5 6 7 8 9 10 |
[EditorDescriptorRegistration(TargetType = typeof(IList<int>), UIHint = UIHint)] public class PropertyIntListEditorDescriptor : PropertyValueListEditorDescriptor<int> { public const string UIHint = "PropertyIntList"; public PropertyIntListEditorDescriptor(LocalizationService localizationService, IMetadataStoreModelCreator metadataStoreModelCreator, ServiceAccessor<HttpContextBase> httpContextServiceAccessor) : base(localizationService, metadataStoreModelCreator, httpContextServiceAccessor) { } } |
Nothing more. Now we can use integer list in our content model:
1 2 3 4 5 6 |
[SiteContentType(GUID = "B1B589EB-6961-42B2-B275-AAD21991A5C6")] public class CollectionTestPage : EPiServer.Core.PageData { [BackingType(typeof(PropertyIntList))] public virtual IList<int> CollectionInt2 { get; set; } } |
The full source code contains ConcreteProperties namespace with implementations for string, int and DateTime.
BackingType
The PropertyValueList base backing type inherits from PropertyList<T> which inherits from PropertyJson. It means that list value will be stored in database as a JSON object.
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 |
public abstract class PropertyValueListBackingType<T> : PropertyList<T> { public override IPropertyControl CreatePropertyControl() { return null; } public override PropertyData ParseToObject(string value) { var valueList = CreateSelfInstance(); valueList.ParseToSelf(value); return valueList; } protected override T ParseItem(string value) { if (!string.IsNullOrEmpty(value)) { var typeConverter = new TypeConverter(); return (T)(typeConverter.ConvertTo(value, typeof(T))); } return default(T); } protected abstract PropertyValueListBackingType<T> CreateSelfInstance(); } |
The concrete class needs to add PropertyDefinitionTypePlugIn and EditorHint definition attributes and implement CreateSelfInstance method which returns backing type instance.
EditorDescriptor
The base EditorDescriptor is responsible for creating metadata schema for single list item.
To create metadata model I used ExtensibleMetadataProvider. Then ModelMetadata has to be converted to MetadataStoreModel. The MetadataStoreModel is used on client side to build property widget. It can be created using IMetadataStoreModelCreator service.
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 |
public abstract class PropertyValueListEditorDescriptor<T> : EditorDescriptor { private readonly LocalizationService _localizationService; private readonly IMetadataStoreModelCreator _metadataStoreModelCreator; private readonly ServiceAccessor<HttpContextBase> _httpContextServiceAccessor; protected PropertyValueListEditorDescriptor(LocalizationService localizationService, IMetadataStoreModelCreator metadataStoreModelCreator, ServiceAccessor<HttpContextBase> httpContextServiceAccessor) { _localizationService = localizationService; _metadataStoreModelCreator = metadataStoreModelCreator; _httpContextServiceAccessor = httpContextServiceAccessor; ClientEditingClass = "propertyValueList/CustomCollectionWidget"; } public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes) { base.ModifyMetadata(metadata, attributes); //Used using ServiceLocator to protect from StructureMap: "Bi-directional dependency relationship detected!" var metadataHandlerRegistry = ServiceLocator.Current.GetInstance<MetadataHandlerRegistry>(); var extensibleMetadataProvider = new PropertyValueListExtensibleMetadataProvider(metadataHandlerRegistry, _localizationService, _httpContextServiceAccessor, metadata.PropertyName, typeof(T)); var modelMetadata = extensibleMetadataProvider.GetMetadataForType( () => Activator.CreateInstance(metadata.Parent.Model.GetType()), metadata.Parent.Model.GetType()); var metadataStoreModel = _metadataStoreModelCreator.Create(modelMetadata); var propertyMetadata = metadataStoreModel.Properties.FirstOrDefault(p => p.Name == metadata.PropertyName); metadata.CustomEditorSettings["innerPropertySettings"] = propertyMetadata; } } |
CustomTypeDescriptor
Content model has property of type IList<T> while I had to prepare schema for type T. The ExtensibleMetadataProvider use TypeDescriptor to get information about the model type. So I prepared my own version of ExtensibleMetadataProvider which use different TypeDescriptor.
Now when ExtensibleMetadataProvider ask for type information:
I change original property (Property1) type to T:
Client editor
Client editor (propertyValueList.CustomCollectionWidget) use MetadataTransformer to convert serialized MetadataStoreModel to component definition. The component definition is used by WidgetFactory to create editor instance. Instance is not used on the list directly. I prepared propertyValueList.CollectionItem wrapper, to add delete button and to support items sorting.
To save the value I have to collect all nested widgets values and push them to an array. And when rendering value from the server I have to create widgets for each array element.
Using list
To use the property model we have to add BackingType attribute with concreate backing type implementation. For example for string we use PropertyExtendedStringList:
1 2 3 4 5 6 |
[SiteContentType(GUID = "A7B8FF39-ED99-474E-9FFB-2518D49EAD61")] public class : EPiServer.Core.PageData { [BackingType(typeof(PropertyExtendedStringList))] public virtual IList<string> CollectionInt2 { get; set; } } |
For dates it will be rendered as list of DateTimePicker editors:
All attributes used on property will be applied when create model schema. For example if we need list of integers with values between 10 and 20 we could use range attribute:
1 2 3 4 5 6 7 |
[SiteContentType(GUID = "B1B589EB-6961-42B2-B275-AAD21991A5C6")] public class CollectionTestPage : EPiServer.Core.PageData { [BackingType(typeof(PropertyIntList))] [Range(10,20)] public virtual IList<int> CollectionInt2 { get; set; } } |
The only limitation is related with UIHints. If we set the UIHint attribute on property list, EPIServer won’t find the PropertyValueListEditorDescriptor
1 2 3 4 5 6 7 |
[SiteContentType(GUID = "A7B8FF39-ED99-474E-9FFB-2518D49EAD61")] public class CollectionTestPageString : EPiServer.Core.PageData { [BackingType(typeof(PropertyExtendedStringList))] [InnerPropertyUIHint(UIHint.Textarea)] public virtual IList<string> CollectionInt2 { get; set; } } |
and the list will use textareas as items
There are much more files than on other posts, so I placed the code on Github instead of Gists