U4-9718 - Cannot create a custom property editor

Created by Ben Palmer 03 Apr 2017, 16:12:25 Updated by Ben Palmer 03 Apr 2017, 16:12:25

I'm attempting to create a custom property editor that is based upon the Umbraco.Multitextstrings property editor. However, I'm doing this for a package which will also be a Nuget package. As such, I'm wanting to create the property editor manually. I've found a way to do this by declaring a new DataTypeDefinition:

var dataType = new DataTypeDefinition(dataTypeContainer.Result.Entity.Id, "Umbraco.Multitextstrings"); dataType.Name = "Allowed Content Types"; Services.DataTypeService.Save(dataType);

This works just fine but I can't however set defaults. For example, I want to create a multi text string editor and specify a maximum of 3 values. I can't seem to do this via the method of creating the new data type.

So, I've rummaged through the Umbraco forums and the source code to try and figure out a way to do this. I've ended up duplicating a few classes to set up what is essentially a duplicate of the core repeatable strings editor.

First, I created a class for the property editor itself:

using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web.PropertyEditors;

namespace UmbracoSandbox.PropertyEditors { [PropertyEditor("UmbracoRssFeeds.AllowedContentTypesPropertyEditor", "Umbraco Rss Feeds Allowed Content Types", "multipletextbox", ValueType = PropertyEditorValueTypes.Text, Icon="icon-ordered-list", Group="lists")] public class UmbracoRssFeedsAllowedContentTypesPropertyEditorPropertyEditor : PropertyEditor { protected override PropertyValueEditor CreateValueEditor() { return new UmbracoRssFeedsAllowedContentTypesPropertyEditorPropertyValueEditor(base.CreateValueEditor()); }

    protected override PreValueEditor CreatePreValueEditor()
    {
        return new UmbracoRssFeedsAllowedContentTypesPropertyEditorPreValueEditor();
    }

    /// <summary>
    /// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored.
    /// </summary>
    internal class UmbracoRssFeedsAllowedContentTypesPropertyEditorPreValueEditor : PreValueEditor
    {
        public UmbracoRssFeedsAllowedContentTypesPropertyEditorPreValueEditor()
        {
            //create the fields
            Fields.Add(new PreValueField(new UmbracoRssFeedsIntegerValidator())
            {
                Description = "Enter the minimum amount of text boxes to be displayed",
                Key = "min",
                View = "requiredfield",
                Name = "Minimum"
            });
            Fields.Add(new PreValueField(new UmbracoRssFeedsIntegerValidator())
            {
                Description = "Enter the maximum amount of text boxes to be displayed, enter 0 for unlimited",
                Key = "max",
                View = "requiredfield",
                Name = "Maximum"
            });
        }

        /// <summary>
        /// Need to change how we persist the values so they are compatible with the legacy way we store values
        /// </summary>
        /// <param name="editorValue"></param>
        /// <param name="currentValue"></param>
        /// <returns></returns>
        public override IDictionary<string, PreValue> ConvertEditorToDb(IDictionary<string, object> editorValue, PreValueCollection currentValue)
        {
            //the values from the editor will be min/max fieds and we need to format to json in one field
            var min = (editorValue.ContainsKey("min") ? editorValue["min"].ToString() : "0").TryConvertTo<int>();
            var max = (editorValue.ContainsKey("max") ? editorValue["max"].ToString() : "0").TryConvertTo<int>();

            var json = JObject.FromObject(new {Minimum = min.Success ? min.Result : 0, Maximum = max.Success ? max.Result : 0});

            return new Dictionary<string, PreValue> { { "0", new PreValue(json.ToString(Formatting.None)) } };
        }

        /// <summary>
        /// Need to deal with the legacy way of storing pre-values and turn them into nice values for the editor
        /// </summary>
        /// <param name="defaultPreVals"></param>
        /// <param name="persistedPreVals"></param>
        /// <returns></returns>
        public override IDictionary<string, object> ConvertDbToEditor(IDictionary<string, object> defaultPreVals, PreValueCollection persistedPreVals)
        {
            var preVals = persistedPreVals.FormatAsDictionary();
            var stringVal = preVals.Any() ? preVals.First().Value.Value : "";
            var returnVal = new Dictionary<string, object> { { "min", 0 }, { "max", 0 } };
            if (stringVal.IsNullOrWhiteSpace() == false)
            {
                try
                {
                    var json = JsonConvert.DeserializeObject<JObject>(stringVal);
                    if (json["Minimum"] != null)
                    {
                        //by default pre-values are sent out with an id/value pair
                        returnVal["min"] = json["Minimum"].Value<int>();
                    }
                    if (json["Maximum"] != null)
                    {
                        returnVal["max"] = json["Maximum"].Value<int>();
                    }
                }
                catch (Exception e)
                {
                    //this shouldn't happen unless there's already a bad formatted pre-value
                    LogHelper.WarnWithException<UmbracoRssFeedsAllowedContentTypesPropertyEditorPreValueEditor>("Could not deserialize value to json " + stringVal, e);
                    return returnVal;
                }
            }

            return returnVal;
        }
    }

    /// <summary>
    /// Custom value editor so we can format the value for the editor and the database
    /// </summary>
    internal class UmbracoRssFeedsAllowedContentTypesPropertyEditorPropertyValueEditor : PropertyValueEditorWrapper
    {
        public UmbracoRssFeedsAllowedContentTypesPropertyEditorPropertyValueEditor(PropertyValueEditor wrapped) : base(wrapped)
        {
        }
        
        /// <summary>
        /// The value passed in from the editor will be an array of simple objects so we'll need to parse them to get the string
        /// </summary>
        /// <param name="editorValue"></param>
        /// <param name="currentValue"></param>
        /// <returns></returns>
        /// <remarks>
        /// We will also check the pre-values here, if there are more items than what is allowed we'll just trim the end
        /// </remarks>
        public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue)
        {
            var asArray = editorValue.Value as JArray;
            if (asArray == null)
            {
                return null;
            }

            var preVals = editorValue.PreValues.FormatAsDictionary();
            var max = -1;
            if (preVals.Any())
            {
                try
                {
                    var json = JsonConvert.DeserializeObject<JObject>(preVals.First().Value.Value);
                    max = int.Parse(json["Maximum"].ToString());
                }
                catch (Exception)
                {
                    //swallow
                    max = -1;
                }                    
            }

            //The legacy property editor saved this data as new line delimited! strange but we have to maintain that.
            var array = asArray.OfType<JObject>()
                               .Where(x => x["value"] != null)
                               .Select(x => x["value"].Value<string>());
            
            //only allow the max if over 0
            if (max > 0)
            {
                return string.Join(Environment.NewLine, array.Take(max));    
            }
            
            return string.Join(Environment.NewLine, array);
        }

        /// <summary>
        /// We are actually passing back an array of simple objects instead of an array of strings because in angular a primitive (string) value
        /// cannot have 2 way binding, so to get around that each item in the array needs to be an object with a string.
        /// </summary>
        /// <param name="property"></param>
        /// <param name="propertyType"></param>
        /// <param name="dataTypeService"></param>
        /// <returns></returns>
        /// <remarks>
        /// The legacy property editor saved this data as new line delimited! strange but we have to maintain that.
        /// </remarks>
        public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService)
        {
            return property.Value == null
                              ? new JObject[] {}
                              : property.Value.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
                                       .Select(x => JObject.FromObject(new {value = x}));


        }

    }
}

}

This complained about IntegerValidator because of it's protection level. To combat this, I then created another class which again, was just a copy of the core umbraco IntegerValidator class:

using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors;

namespace UmbracoSandbox.PropertyEditors { ///

/// A validator that validates that the value is a valid integer /// [ValueValidator("Integer")] internal sealed class UmbracoRssFeedsIntegerValidator : ManifestValueValidator, IPropertyValidator { public override IEnumerable Validate(object value, string config, PreValueCollection preValues, PropertyEditor editor) { if (value != null && value.ToString() != string.Empty) { var result = value.TryConvertTo(); if (result.Success == false) { yield return new ValidationResult("The value " + value + " is not a valid integer", new[] { "value" }); } } }

    public IEnumerable<ValidationResult> Validate(object value, PreValueCollection preValues, PropertyEditor editor)
    {
        return Validate(value, "", preValues, editor);
    }
}

}

Similarly, this complained again because of the protection class on the ValueValidator attribute. I then implemented my own copy of that class to fix this:

using System;

namespace UmbracoSandbox.PropertyEditors { [AttributeUsage(AttributeTargets.Class)] internal sealed class ValueValidatorAttribute : Attribute { public ValueValidatorAttribute(string typeName) public string TypeName { get; private set; } } }

This all built fine. Once I'd done that, I then set up the new data type like so:

var dataType = new DataTypeDefinition(dataTypeContainer.Result.Entity.Id, "UmbracoRssFeeds.AllowedContentTypesPropertyEditor"); dataType.Name = "Allowed Content Types"; Services.DataTypeService.Save(dataType);

This successfully creates the data type but when I view this in the CMS, I get the following error:

System.InvalidOperationException: The class UmbracoSandbox.PropertyEditors.UmbracoRssFeedsIntegerValidator is not attributed with the Umbraco.Core.PropertyEditors.ValueValidatorAttribute attribute

So, it feels like a bit of a dead end, I'm not sure if I've overthought the process, if it's possible, or not but some clarification would be good. Also, an easy way to extend Umbraco via these properties would be ideal.

A simple change could be to simply change the protection level of Umbraco.Core.PropertyEditors.IntegerValidator so it can be used. I'm not sure of the ease or implications but any help would be massively appreciated!

Thanks,

Ben

Comments

Priority: Normal

Type: Bug

State: Submitted

Assignee:

Difficulty: Normal

Category:

Backwards Compatible: True

Fix Submitted:

Affected versions:

Due in version:

Sprint:

Story Points:

Cycle: