Skip to content

Create Custom Property Types

⏱ 25 minutes advanced

Built-in property types cover common scenarios β€” strings, numbers, content references, rich text. Sometimes your content model requires a value that does not map cleanly to any of those. A color picker, a geographic coordinate, or a structured address field all benefit from a dedicated property type. Custom property types give editors a purpose-built UI and give you type-safe access in code.

  1. Define a backing class that holds the property value
  2. Create a PropertyData subclass that serializes and deserializes the value
  3. Register an editor descriptor so the CMS editor renders a custom widget
  4. Use the property on a content type

Start with a plain C# class that represents the structured value you want to store.

Backing model for a map coordinate
csharp
namespace MySite.Models.Properties;

public class GeoCoordinate
{
    public double Latitude { get; set; }
    public double Longitude { get; set; }

    public override string ToString() =>
        $"{Latitude},{Longitude}";

    public static GeoCoordinate Parse(string raw)
    {
        var parts = raw.Split(',');
        return new GeoCoordinate
        {
            Latitude = double.Parse(parts[0]),
            Longitude = double.Parse(parts[1])
        };
    }
}

Keep the backing model serializable. The CMS stores property values as strings or JSON in the database, so your type must round-trip cleanly.

The PropertyData subclass bridges your backing model and the CMS storage layer.

PropertyData implementation
csharp
using Optimizely.Cms.Core.PropertyDefinitions;

namespace MySite.Models.Properties;

[PropertyDefinitionTypePlugIn(
    DisplayName = "Geo Coordinate",
    Description = "Latitude and longitude pair")]
public class PropertyGeoCoordinate : PropertyData
{
    private GeoCoordinate? _coordinate;

    public override Type PropertyValueType => typeof(GeoCoordinate);

    public override object? Value
    {
        get => _coordinate;
        set
        {
            SetPropertyValue(value, () =>
            {
                _coordinate = value switch
                {
                    GeoCoordinate gc => gc,
                    string s when !string.IsNullOrEmpty(s) => GeoCoordinate.Parse(s),
                    _ => null
                };
            });
        }
    }

    protected override string? LongString => _coordinate?.ToString();

    public override void ParseToSelf(string rawValue)
    {
        Value = rawValue;
    }
}

Key points:

  • PropertyValueType tells the CMS what .NET type the property exposes.
  • LongString controls how the value persists to the database.
  • ParseToSelf handles deserialization when content loads.

An editor descriptor tells the CMS which Dojo widget to render for your property.

Editor descriptor
csharp
using Optimizely.Cms.Shell.ObjectEditing;
using Optimizely.Cms.Shell.ObjectEditing.EditorDescriptors;

namespace MySite.EditorDescriptors;

[EditorDescriptorRegistration(
    TargetType = typeof(GeoCoordinate),
    UIHint = "GeoCoordinate")]
public class GeoCoordinateEditorDescriptor : EditorDescriptor
{
    public override void ModifyMetadata(
        ExtendedMetadata metadata,
        IEnumerable<Attribute> attributes)
    {
        ClientEditingClass =
            "mysitewidgets/GeoCoordinateEditor";
        base.ModifyMetadata(metadata, attributes);
    }
}

The ClientEditingClass path points to a Dojo module under your ClientResources folder. You build the JavaScript widget there to render latitude/longitude inputs or a map picker.

Using the custom property
csharp
using Optimizely.Cms.Core;
using Optimizely.Cms.Core.Attributes;
using System.ComponentModel.DataAnnotations;

namespace MySite.Models.Pages;

[ContentType(
    DisplayName = "Store Location",
    GUID = "a1b2c3d4-e5f6-7890-abcd-123456789abc")]
public class StoreLocationPage : PageData
{
    [Display(Name = "Store Name", Order = 10)]
    [Required]
    public virtual string StoreName { get; set; }

    [Display(Name = "Coordinates", Order = 20)]
    [UIHint("GeoCoordinate")]
    public virtual GeoCoordinate? Coordinates { get; set; }
}

The [UIHint] value must match the one you registered in the editor descriptor.

IssueCauseFix
Property shows as plain text boxEditor descriptor not foundVerify ClientEditingClass path and UIHint match
Value lost on saveLongString returns nullEnsure serialization in LongString is correct
Type not available in adminMissing PropertyDefinitionTypePlugInAdd the attribute to your PropertyData class