Five New Optimizely Certifications are Here! Validate your expertise and advance your career with our latest certification exams. Click here to find out more
Five New Optimizely Certifications are Here! Validate your expertise and advance your career with our latest certification exams. Click here to find out more
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.
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:
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.
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:
[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.
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:
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:
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});
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”:
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.
Here is a reference list of metadata attributes that are used by the object editing system.
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. |
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. |
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.
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:
In MVC, we can 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. 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:
For the above example, the client widgets initial settings should look like:
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"' />
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:
[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:
<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:
[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:
[RegEx("\b[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b{0,50}", ErrorMessage = "Invalid Email address")]
public string Email { get; set; }
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:
[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:
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:
//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:
//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:
//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.
Last updated: Mar 21, 2013