Hide menu Last updated: Oct 27 2016
Area: Episerver CMS Applies to versions: 10 and higher
Other versions:

Editing objects

This topic describes how to automatically create user interfaces for editing objects; similar to working with visitor groups in Episerver or scaffolding in MVC; taking any .NET object and automatically generating an editor view for it. The Episerver implementation is built on the MVC implementation but extends it with functionality and additional usage scenarios.

How it works

As with Visitor Group and MVC implementations, Episerver uses metadata for the classes and properties to define rules for how you can edit an object, such 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 when you edit the object. The Name property is required, and the Age property must be between 0 and 999 to be valid.

To create an editor for a Humanoid object instance, MVC has built-in editors for primitive types and a convention-based system where you can place user controls within folders to add editors for a type. Episerver also has built-in editors for primitive types. When Episerver generates an editor for a type, it traverses the object graph until it finds a registered editor for a type. For the example class, an editor is not registered for the Humanoid type, so the object editor assembler checks the properties for the Humanoid class. Because the three properties are of primitive types that Episerver has registered editors for, you get three editors for the properties of the class.

Object editing metadata and EditorDescriptor

An intermediate layer contains metadata for how you should create the user interface. When Episerver creates an editor for an object, it creates a metadata hierarchy that represents the editors for the object graph by extracting metadata from the object’s properties and applying global metadata handlers. A global metadata handler can extend or change metadata from the classes.

The following example shows how to implement a metadata handler to map which client-side widgets can edit a specific type.

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

The metadata handler inherits from EditorDescriptor. This class has properties you can set and a base implementation that applies these settings to the metadata object. The base implementation only sets the values if no setting was specified, so settings defined on the model override the generic settings defined in this class. The class also has a ModifyMetadata method that is called after the metadata is extracted from the classes’ attributes. To set custom settings not supported by the properties on EditorDescriptor, you can override this method and access the metadata object directly.

An editor descriptor is registered as a singleton instance so that any settings that are set up in the constructor apply for the application's life cycle. Use the ModifyMetadata method to define settings that should be dynamic, such as user-specific settings or translations.

Metadata handler registry

Apply a metadata handler to the metadata extracted from an object with the ObjectEditingMetadataHandlerRegistry class, which maps types with metadata handlers. In the previous example, you registered the metadata class using the EditorDescriptorRegistration attribute. You also can add metadata handlers directly in an initialization module:

private void SetupPrimitiveTypesEditors(EPiServer.Framework.Initialization.InitializationEngine context)
{
    ObjectEditingMetadataHandlerRegistry factory = context.Container.GetInstance<ObjectEditingMetadataHandlerRegistry>();
    //string
    factory.RegisterEditor(typeof(string), new StringEditorDescriptor());

    //DateTime
    factory.RegisterEditor(typeof(DateTime), new DateTimeEditorDescriptor());
}

The StringEditorDescriptor is called each time you extract metadata for a property of the type string and the DateTimeEditorDescriptor each time you extract a DateTime.

You also can add a metadata handler for a list of types:

factory.RegisterEditor(new Type[] { typeof(decimal), typeof(decimal?) },
    var initialConfiguration = string.Format("constraints: {{ max: {0}, min: {1}}}", decimal.MaxValue, decimal.MinValue);
    new DecimalEditorDescriptor(){ InitialConfiguration = initialConfiguration});

Metadata providers

Sometimes, you want to take over the generation of metadata for an entire class, such as implementing PageData in Episerver CMS. When you edit a PageData class, you should focus on editing the items in the Properties collection that have information about the real data for the PageData class.

Another example could be that you are using a third-party assembly where you do not have an option to add metadata attributes. This is possible through registering a metadata handler that implements the IMetadataProvider interface, which has two methods you need to implement: CreateMetadata and GetMetadataForProperties. CreateMetadata is called for the top-level object, and GetMetadataForProperties for the sub-properties. The following example shows a class that implements the IMetadataProvider interface for the ExampleClass class:

public class MetadataProvider : IMetadataProvider
{
    public ExtendedMetadata CreateMetadata(IEnumerable<Attribute> attributes,
                                            Type containerType,
                                            Func<object> modelAccessor,
                                            Type modelType,
                                            string propertyName)
    {
        ExtendedMetadata metadata = new ExtendedMetadata(containerType, modelAccessor, modelType, propertyName);
        metadata.DisplayName = "My Property Name";
        metadata.Description = "Foo bar2";

        return metadata;
    }

    public IEnumerable<ExtendedMetadata> GetMetadataForProperties(object container, Type containerType)
    {
        List<ExtendedMetadata> propertyMetadata = new List<ExtendedMetadata>();
        if (containerType == typeof(ExampleClass))
        {
            propertyMetadata.Add(new ExtendedMetadata(containerType, null, typeof(SubClass),
                                    "MySubProperty") { DisplayName = "Some property 2" });
        }
        else if (containerType == typeof(SubClass))
        {
            propertyMetadata.Add(new ExtendedMetadata(containerType, null, typeof(string),
                                    "MyProperty") { DisplayName = "Some property 2" });
        }
        return propertyMetadata;
    }
}

Note: Metadata extenders are called even for metadata extracted from a custom IMetadataProvider.

Common attributes

The object editing system uses the following metadata attributes.

.NET Attributes

AttributePropertyEffect
Display ShortDisplayName The name that is shown as a label.
  Order How this property is ordered compared to other properties.
  GroupName The identifier of the group that the property belongs to.
Editable AllowEdit If the property is read only. This attribute overrides the ReadOnly attribute if both the Editable and ReadOnly attributes are defined.
ReadOnly IsReadOnly If the property is read only.
Required - The property becomes required.
ScaffoldColumn ShowForEdit If the property is shown when editing.

Additional EPiServer attributes

AttributePropertyEffect
ClientEditor ClientEditingClass The Dojo widget class name
  ClientEditingPackage The Dojo package that is required to load the widget. This only is necessary if the required package differs from the widget name.
  DefaultValue The default value of the widget.
  EditorConfiguration Settings that are passed to the widget as configuration. For instance: min and max values for an integer.
  LayoutClass The widget class that is responsible for the layout for this object and its children.
GroupSettings Name The identifier that matches the GroupName for the property.
  Title Title can be used to display a title for the group in the widget.
  ClientLayoutClass The widget class for the group.

Validating according to the MVC style model

The Episerver object editing framework supports MVC 3’s built-in annotation-based validators. The following example shows 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:

  • Name. Should not be more than 50 characters long. It should not be left empty.
  • YearOfBirth. Should be from 1900 to 2000.
  • Email. Should be a valid email address.

In MVC, decorate the class with validation attributes like:

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, 2000, ErrorMessage = "The person should be born between 1900 and 2000")]
    public int YearOfBirth { get; set; }

    [RegEx("\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. Validation information is sent to the client editors. Because the framework tries to map MVC validation rules to the rules in Dojo, there is no further work needed to make validation work on the client side. In detail, the following widget settings are used:

  • required. Set to true if the editing property is marked with [Required].
  • constraints. The min and max constraints used to tell the widget to do a range validation.
  • regEx. Directly mapped to the pattern set by [RegEx] attribute.
  • missingMessage. The error message specified in [Required] attribute.
  • invalidMessage. A combination of the error messages specified by validation attributes rather than [Required], separated by new-line characters (CR/LF).

For the example, the client widget's initial settings should 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 when using multiple validation attributes

There are limitations to validation when you use specific combinations of attributes, because several validation attributes use the regex field for the client widget. For example, if you specify the regEx attribute and constraints settings using EditorConfiguration property of the ClientEditor attribute, you override the model validation rules. Therefore, you should specify validation information also. For example, for a property that looks like this:

[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; }

Unfortunately, you will not get the expected validation. The widget you get 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 conclusion is that the StringLength and RegEx attributes do not work together. This is because the framework translates both of them into the client’s regEx setting. In this case, the RegEx attribute takes higher priority. For example, if you do not want the Email address to be too long, you would write:

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

As the result, StringLength is not processed. The correct way to do is to define length constraint in your regular expression pattern:

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

Validating a custom widget

Although the widgets that inherit dijit.form.ValidationTextBox already fit to model validations, you may write a custom widget from scratch. The following example shows the rules you must follow to have your widget work well with MVC model validations. In this example, create a widget that is used to edit a money amount. The value should be a ranged number, followed by a currency sign like $ or E. In the model class, validation information is set as follows:

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

Next, create a widget that inherits dijit._Widget and renders an HTML input. The start-up skeleton to follow looks as follows:

define('some.example.Widget', ['dojo', 'dijit', 'dijit._Widget'], function(dojo, dijit) {
    dojo.declare('some.example.NumberWidget', [dijit._Widget], {
        templateString: '<input type="textbox" id="widget_${id}" dojoattachevent="onchange: _onValueChanged" />',

        //properties declaration

        //method declaration

        //event handlers
        _onValueChanged: function() {
        }
    });
});

When the widget is created, the framework tries to mix in validation properties. Therefore, define those in the widget as follows:

//properties declaration        
required: false,

missingMessage: 'Value cannot be empty',
invalidMessage: 'Entered value is invalid',

regExp: '.*',
constraints: {},

To ensure that validation properties are correctly set, override the postMixinProperties method:

//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 does validation, it looks into the child widgets for validate methods, whose return value indicate if it is valid. Then, use validation information properties to write your own validation logic:

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

Almost all of the dijit.* widgets support on-the-fly validation. To do the same, listen to onChange event and call the validate method.

Related topics

Comments