This content is archived. See latest version here.

Last updated: Mar 21 2013

Table of Contents

Introduction

This document describes how to to automatically create user interfaces for editing objects. If you have worked with visitor groups in EPiServer CMS 6 R2 or “scaffolding” in MVC 2 and above you probably have a pretty good idea of what it is. The idea is to take any .NET object and auto-generate an editor view for it. The EPiServer implementation is built upon the MVC implementation but extends it both with functionality and additional usage scenarios.

The Basics

As with the EPiServer CMS 6 R2 and MVC implementations, the idea is to use metadata for the classes and properties to define rules for how the object should be edited. Take a look at the following basic example:

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

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

    public bool IsSuperHero { get; set;}
}

The class above has three properties. Two of them have attributes that describes validation rules when editing the object. The “Name” property is required and the “Age” property must be between 0 and 999 to be valid. But how do you create an editor for a Humanoid object instance? MVC has built in editors for primitive types and a convention based system where user controls can be placed within folders at given locations to add editors for a given 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 our example class it is pretty simple. We have not registered an editor for the Humanoid type so the object editor assembler checks the properties for the Humanoid class. Since the three properties are of primitive types that EPiServer has registered editors for, we will get three editors for the properties of the class.

Object Editing Metadata

In the description above we have simplified the generation somewhat. We described the object editing assembler that generates user interfaces for editing the object. But there is actually an intermediate layer that contains metadata for how the user interface should be created. When EPiServer creates an editor for an object, the first thing that will happen is to create a metadata hierarchy that represents the editors for the object graph. This process involves extracting metadata from the object’s properties as well as applying global metadata handlers. A global metadata handler has the ability to extend or change metadata from the classes. An example of a metadata handler is the mapping of which client side widgets should be used to edit a specific type. Take a look at how it could be implemented:

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

First of all, note that it inherits from EditorDescriptor. This class has some properties you can set and a base implementation that will apply these settings to the metadata object. The base implementation will only set the values if no setting has been specified which means that settings defined on the model will override the generic settings defined in this class. The class also has a method called ModifyMetadata. This method will be called after the metadata has been extracted from the classes’ attributes. If you need to set custom settings that are not supported by the properties on EditorDescriptor you can override this method and access the metadata object directly.

Metadata Handler Registry

How do we know when to apply a metadata handler to the metadata extracted from an object? The answer is the class ObjectEditingMetadataHandlerRegistry that is responsible for mapping types with metadata handlers. In the example above we registered our metadata class using the EditorDescriptorRegistration attribute. It's also possible to add metadata handlers directly in an initialization module:

CopyC#
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());
}

In the example above we state that the StringEditorDescriptor will be called each time we extract metadata for a property of the type string and the DateTimeEditorDescriptor each time a DateTime is extracted. It is also possible to add a metadata handler for a list of types:

CopyC#
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

So far we have covered extraction of metadata through attributes and metadata extenders that might change the metadata. But sometimes you want to take over the generation of metadata for an entire class. An example is the implementation of PageData in EPiServer CMS. When editing a PageData class we are really not interested in editing the properties on the class but rather the items in the “Properties” collection that has 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 the option to add metadata attributes. This is possible through registering a metadata handler that implements the IMetadataProvider interface. The interface has two methods that you need to implement, CreateMetadata and GetMetadataForProperties. CreateMetadata will be called for the top level object and GetMetadataForProperties for the sub properties. Here is is a simple example of a class that implements the IMetadataProvider interface for the class “ExampleClass”:

CopyC#
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;
    }
}

It is worth noting that metadata extenders will be called even for metadata extracted from a custom IMetadataProvider.

Common Attributes

Here is a reference list of metadata attributes that are used by the object editing system.

.NET Attributes

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

Additional EPiServer Attributes

Attribute Property Effect
ClientSideEditor ClientSideEditorClass The Dojo widget class name
ClientEditingPackage The Dojo package that is required to load the widget. This is only required if the required package differs from the widget name.
DefaultValue The default value of the widget.
InitialConfiguration 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. For instance, we have a Person class, with three properties: Name, YearOfBirth, and Email.

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

We want the editor generated by the framework to validate entered data as:

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

In MVC, we can decorate the class with validation attributes like:

CopyC#
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. Since 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 is set to true if the editing property is marked with [Required].
  • constraints are the min and max constraints used to tell the widget to do a range validation.
  • regEx is directly mapped to the pattern set by [RegEx] attribute.
  • missingMessage is the error message specified in [Required] attribute.
  • invalidMessage is a combination of all the error messages specified by validation attributes rather than [Required], separated by “New line“ characters (CR/LF).

For the above example, the client widgets initial settings should look like:

Name:
CopyXML
<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:
CopyXML
<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:
CopyXML
<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 some limitations to the validation when using specific combinations of attributes since several validation attributes use the regex field for the client widget. For instance, if you specify the regEx attribute and constraints settings using InitialConfiguration property of the ClientSideEditor attribute, you will override the model validation rules. Therefore, you should also specify validation information yourself. For example, for a property that looks like this:

CopyC#
[ClientSideEditor(ClientEditingClass = "dijit.form.HorizontalSlider", DefaultValue = "0",
    InitialConfiguration = "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:

CopyXML
<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. The reason for this is that the framework translates both of them into the client’s regEx setting. In this case, RegEx attribute will take the higher priority. For example, if you did not want the Email address too long, you would write:

CopyC#
[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 will not be processed. The correct way to do is to define length constraint in your regular expression pattern:

CopyC#
[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 all the widgets which inherit dijit.form.ValidationTextBox already fit to model validations, you may want to write a custom widget from scratch. This section covers a simple example showing the rules you must follow to have your widget works well with MVC model validations. In this example we want to 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:

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

Now, we create a widget which inherits dijit._Widget and renders an html input. The start-up skeleton to follow looks like:

CopyJavaScript
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 will try to mix validation properties in. Therefore, we have to define those in our widget:

CopyJavaScript
//properties declaration        
required: false,

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

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

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

CopyJavaScript
//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 or not. Then we use validation information properties to write our own validation logic:

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

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

Comments