HomeDev GuideRecipesAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

Edit objects

Describes how to automatically create user interfaces for editing objects.

The edit objects framework generates editor views for .NET objects automatically, similar to scaffolding in MVC. Use it to build editing interfaces without writing custom UI code for each object type.

Optimizely Content Management System (CMS) extends the MVC metadata system with additional editing functionality. Define rules for object editing through class and property metadata attributes, as in the following example:

public class Humanoid {
  [Required]
  public string Name {
    get;
    set;
  }

  [Range(0, 999)]
  public int Age {
    get;
    set;
  }

  public bool IsSuperHero {
    get;
    set;
  }
}

The Humanoid class has three properties. Two have attributes that describe validation rules for editing the object. The Name property is required, and the Age property must be between 0 and 999 to be valid.

MVC has built-in editors for primitive types and a convention-based system for placing user controls within folders to add editors for a type. CMS also has built-in editors for primitive types. When CMS generates an editor for a type, it traverses the object graph until it finds a registered editor.

For this example class, no editor is registered for the Humanoid type. The object editor assembler checks the properties instead. Because the three properties are primitive types with registered editors, CMS generates three property editors.

Object editing metadata and EditorDescriptor

The metadata layer between model classes and the generated UI controls how CMS builds the editing interface. Override defaults and extend behavior through EditorDescriptor classes.

When CMS creates an editor for an object, it builds a metadata hierarchy by extracting metadata from the object properties and applying global metadata handlers. A global metadata handler extends or changes metadata from the classes.

The following example maps a client-side widget to a specific type through a metadata handler:

[EditorDescriptorRegistration(TargetType = typeof(string))]
public class StringEditorDescriptor : EditorDescriptor {
  public StringEditorDescriptor() {
    ClientEditingClass = "dijit/form/ValidationTextBox";
  }
}

The metadata handler inherits from EditorDescriptor. This class has properties that apply settings to the metadata object. The base implementation sets values only when the model does not specify a setting, so model-defined settings override the generic settings in this class.

The class also has a ModifyMetadata method that CMS calls after extracting metadata from the class attributes. Override this method to set custom settings that the EditorDescriptor properties do not support and access the metadata object directly.

CMS registers an editor descriptor as a singleton instance. Settings in the constructor apply to the application life cycle. Use the ModifyMetadata method for dynamic settings, such as user-specific settings or translations.

Metadata handler registry

The MetadataHandlerRegistry class maps types with metadata handlers and applies handlers to metadata extracted from an object. Use it to register custom metadata handlers that extend how CMS processes properties of specific types.

In the previous example, the EditorDescriptor class uses the EditorDescriptorRegistration attribute for registration. Add metadata handlers directly in an initialization module as an alternative:

private void SetupPrimitiveTypesEditors(EPiServer.Framework.Initialization.InitializationEngine context) {
  MetadataHandlerRegistry factory = ServiceLocator.Current.GetInstance<MetadataHandlerRegistry>();
  //string
  factory.RegisterMetadataHandler(typeof(string), new StringEditorDescriptor());
    
  //DateTime
  factory.RegisterMetadataHandler(typeof(DateTime), new DateTimeEditorDescriptor());
}

CMS calls StringEditorDescriptor each time it extracts metadata for a string property and DateTimeEditorDescriptor each time it extracts a DateTime.

Add a metadata handler for multiple types:

factory.RegisterMetadataHandler(new Type[] { typeof(decimal), typeof(decimal?) }, new DecimalEditorDescriptor());

Metadata providers

Implement a custom IMetadataProvider to take over metadata generation for an entire class. For example, when editing a PageData class, focus on the items in the Properties collection that contains the data for the class.

Another use case is a third-party assembly where metadata attributes are not available. Register a metadata handler that implements the IMetadataProvider interface. This interface has two methods: CreateMetadata for the top-level object, and GetMetadataForProperties for the sub-properties.

The following example implements the IMetadataProvider interface for the ExampleClass class:

public class MetadataProvider: IMetadataProvider {
  private readonly ExtensibleMetadataProvider _extensibleMetadataProvider;
  private readonly LocalizationService _localizationService;
  private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider;

  public MetadataProvider(
    ExtensibleMetadataProvider extensibleMetadataProvider,
    LocalizationService localizationService,
    IValidationAttributeAdapterProvider validationAttributeAdapterProvider) {
    _extensibleMetadataProvider = extensibleMetadataProvider;
    _localizationService = localizationService;
    _validationAttributeAdapterProvider = validationAttributeAdapterProvider;
  }

  public ExtendedMetadata CreateMetadata(IEnumerable<Attribute> attributes,
    Type containerType,
    Func<object> modelAccessor,
    Type modelType,
    string propertyName) {
    var defaultMetadata = string.IsNullOrEmpty(propertyName)
      ? _extensibleMetadataProvider.DefaultProvider.GetMetadataForType(modelType) as DefaultModelMetadata
      : _extensibleMetadataProvider.DefaultProvider.GetMetadataForProperty(containerType, propertyName) as DefaultModelMetadata;

    var metadata = new ExtendedMetadata(
      defaultMetadata,
      _validationAttributeAdapterProvider,
      _extensibleMetadataProvider,
      containerType,
      modelAccessor,
      _localizationService);

    metadata.DisplayName = "My Property Name";
    metadata.Description = () => "Foo bar2";
    return metadata;
  }

  public IEnumerable<ExtendedMetadata> GetMetadataForProperties(object container, Type containerType) {
    return new List<ExtendedMetadata>();
  }
}
📘

Note

CMS calls metadata extenders even for metadata extracted from a custom IMetadataProvider.

Common attributes

Reference these metadata attributes to control how the object editing system processes properties.

.NET attributes

The following .NET attributes define validation and display behavior for properties:

AttributePropertyEffect
DisplayShortDisplayNameThe name displayed as a label.
OrderHow this property is ordered compared to other properties.
GroupNameThe identifier of the group that the property belongs to.
EditableAllowEditWhether the property is read-only. This attribute overrides ReadOnly when both Editable and ReadOnly are defined.
ReadOnlyIsReadOnlyWhether the property is read-only.
RequiredNoneThe property becomes required.
ScaffoldColumnShowForEditWhether the property displays when editing.

Additional EPiServer attributes

The following EPiServer-specific attributes control client-side editing widgets and layout:

AttributePropertyEffect
ClientEditorClientEditingClassThe Dojo widget class name.
ClientEditingPackageThe Dojo package required to load the widget. Required only when the package differs from the widget name.
DefaultValueThe default value of the widget.
EditorConfigurationSettings passed to the widget as configuration, such as min and max values for an integer.
LayoutClassThe widget class responsible for the layout of this object and its children.
GroupSettingsNameThe identifier that matches GroupName for the property.
TitleThe title displayed for the group in the widget.
ClientLayoutClassThe widget class for the group.

Validate with the MVC-style model

The CMS object editing framework supports MVC built-in annotation-based validators. Use validation attributes on model properties to enforce constraints on both server and client sides automatically.

The following example has a Person class with three properties: Name, YearOfBirth, and Email.

public class Person {
  public string Name {
    get;
    set;
  }
  public int YearOfBirth {
    get;
    set;
  }
  public string Email {
    get;
    set;
  }
}

The editor generated by the framework validates entered data as follows:

  • Name – One to 50 characters long and not empty.
  • YearOfBirth – 1900 to 2010.
  • Email – A valid email address.

In MVC, decorate the class with validation attributes:

public class Person {
  [StringLength(50, ErrorMessage = "Name should not be longer than 50 character")]
  [Required(ErrorMessage = "Person must have some name")]
  public string Name {
    get;
    set;
  }

  [Range(1900, 2010, ErrorMessage = "The person should be born between 1900 and 2010")]
  public int YearOfBirth {
    get;
    set;
  }

  [RegularExpression(@"\b[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b", ErrorMessage = "Invalid Email address")]
  public string Email {
    get;
    set;
  }
}

The object editing framework understands these validation attributes and sends validation information to the client editors. The framework maps MVC validation rules to Dojo rules, so client-side validation works without further configuration. The following widget settings apply:

  • required – Set to true when the editing property has [Required].
  • constraints – The min and max constraints for range validation.
  • regEx – Mapped directly to the pattern in [RegularExpression].
  • missingMessage – The error message from [Required].
  • invalidMessage – A combination of error messages from validation attributes other than [Required], separated by newline characters (CR/LF).

For the example, the client widget initial settings look as follows:

Name

<div name='Name'
     data-dojoType='dijit/form/ValidationTextBox'
     data-dojoProps='required: true, regEx: "^.{0,50}$", 
     missingMessage: "Person must have some name", 
     invalidMessage: "Name should not be longer than 50 character"' />

YearOfBirth

<div name='YearOfBirth'
     data-dojoType='dijit/form/NumberSpinner'
     data-dojoProps='constraints: {min: 1900, max: 2000}, 
     invalidMessage: "The person should be born between 1900 and 2000"' />

Email

<div name='Email'
     data-dojoType='dijit/form/ValidationTextBox'
     data-dojoProps='regEx: "\b[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b", 
     invalidMessage: "Invalid Email address"' />

Limitations with multiple validation attributes

Certain attribute combinations create validation conflicts because several validation attributes use the regEx field for the client widget. Specifying the regEx attribute and constraints settings through the EditorConfiguration property of the ClientEditor attribute overrides the model validation rules. For a property like the following:

[ClientEditor(ClientEditingClass = "dijit/form/HorizontalSlider", DefaultValue = "0",
  EditorConfiguration = "'constraints': { 'min': 0, 'max': 10 }")]
[Range(1900, 2000, ErrorMessage = "The person should be born between 1900 and 2000")]
public int YearOfBirth {
  get;
  set;
}

The widget does not produce the expected validation. The resulting widget is:

<div name='YearOfBirth'
     data-dojoType='dijit/form/HorizontalSlider'
     data-dojoProps='constraints: {min: 0, max: 10, 
     invalidMessage: "The person should be born between 1900 and 2000"' />

The StringLength and RegularExpression attributes do not work together because the framework translates both into the client regEx setting. The RegularExpression attribute takes higher priority. For example, to prevent the Email address from exceeding a length limit:

[RegularExpression(@"\b[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b", ErrorMessage = "Invalid Email address")]
[StringLength(50)]
public string Email { 
  get; 
  set; 
}

In this case, CMS does not process StringLength. Define the length constraint in the regular expression pattern instead:

[RegularExpression(@"^(?=.{0,50}$)\b[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b", ErrorMessage = "Invalid Email address")]
public string Email { 
  get; 
  set; 
}

Validate a custom widget

Widgets that inherit dijit.form.ValidationTextBox support model validations by default. For a custom widget built from scratch, follow these rules to integrate with MVC model validations.

This example creates a widget for editing a money amount. The value is a ranged number followed by a currency sign like $ or E. In the model class, set validation information as follows:

[Range(0, 50)]
[RegularExpression("^\\d*[\\$,E]$")]
[ClientEditor(ClientEditingClass = "some/example/MoneyEditor")]
public int Amount { 
  get; 
  set;
}

Create a widget that inherits dijit._Widget and renders an HTML input. The startup skeleton follows:

define(['dijit/_Widget'], function (_Widget) {
  return declare([_Widget], {
    templateString: '<input type="textbox" id="widget_${id}" dojoattachevent="onchange: _onValueChanged" />',
    //properties declaration
    //method declaration
    //event handlers
    _onValueChanged: function () {}
  });
});

When CMS creates the widget, the framework mixes in validation properties. Define those in the widget:

//properties declaration        
    required       : false,
    
    missingMessage : 'Value cannot be empty',
    invalidMessage : 'Entered value is invalid',
    
    regExp         : '.*',
    constraints    : {},

Override the postMixinProperties method to verify that validation properties are set correctly:

//method declaration
validate: function () {
  var value = dojo.byId('widget_' + this.id).value;
  var amount = value.length > 0 ? value.substring(0, value.length - 1) : null;

  if (this.required && !value) {
    //display error message using this.missingMessage
    return false;
  }

  if (this.regEx && value.test && !value.test(new RegExp(this.regEx))) {
    //display error message using this.invalidMessage
    return false;
  }

  if ((amount !== null) && (amount < this.constraints.min || amount > this.constraints.max)) {
    //display error message using this.invalidMessage
    return false;
  }

  return true;
}

When the containing form validates, it checks child widgets for validate methods. The return value indicates validity. Use the validation information properties to write validation logic:

//event handlers
_onValueChanged: function () {
  this.validate();
}

Most dijit.* widgets support on-the-fly validation. Listen to the onChange event and call the validate method for the same behavior.