Edit objects
Describes how to automatically create user interfaces for editing objects; similar to working with visitor groups in Optimizely Content Management System (CMS) or scaffolding in MVC; taking any .NET object and automatically generating an editor view for it.
The Optimizely Content Management System (CMS) implementation is built on the MVC implementation but extends it with functionality and additional usage scenarios.
As with Visitor Group and MVC implementations, CMS 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. 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 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 CMS 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 CMS 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 you did not set them, so the settings defined on the model override the generic settings defined in this class.
The class also calls a ModifyMetadata
method 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 set up in the constructor apply to 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 MetadataHandlerRegistry
 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) {
MetadataHandlerRegistry factory = ServiceLocator.Current.GetInstance<MetadataHandlerRegistry>();
//string
factory.RegisterMetadataHandler(typeof (string), new StringEditorDescriptor());
//DateTime
factory.RegisterMetadataHandler(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.RegisterMetadataHandler(new Type[] { typeof(decimal), typeof(decimal?) }, new DecimalEditorDescriptor());
Metadata providers
Sometimes, you want to take over the generation of metadata for an entire class, such as implementing PageData
in CMS. When you edit a PageData
class, you should focus on editing 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 an option to add metadata attributes. You can register 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 {
private readonly ExtensibleMetadataProvider _extensibleMetadataProvider;
private readonly LocalizationService _localizationService;
private readonly ServiceAccessor<HttpContextBase> _httpContext;
public MetadataProvider(
ExtensibleMetadataProvider extensibleMetadataProvider,
LocalizationService localizationService,
ServiceAccessor<HttpContextBase>httpContext) {
_extensibleMetadataProvider = extensibleMetadataProvider;
_localizationService = localizationService;
_httpContext = httpContext;
}
public ExtendedMetadata CreateMetadata(IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName) {
ExtendedMetadata metadata = new ExtendedMetadata(
containerType,
modelAccessor,
modelType,
propertyName,
attributes.OfType<DisplayColumnAttribute>().FirstOrDefault(),
_extensibleMetadataProvider,
_localizationService,
_httpContext());
metadata.DisplayName = "My Property Name";
metadata.Description = "Foo bar2";
return metadata;
}
public IEnumerable<ExtendedMetadata> GetMetadataForProperties(object container, Type containerType) {
return new List<ExtendedMetadata>();
}
}
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
Attribute | Property | Effect |
---|---|---|
Display | ShortDisplayName | The name is shown as a label. |
 | Order | How this property is ordered compared to other properties. |
 | GroupName | The identifier of the group to which the property belongs. |
Editable | AllowEdit | Whether the property is read-only. This attribute overrides the ReadOnly attribute if the Editable and ReadOnly attributes are defined. |
ReadOnly | IsReadOnly | Whether the property is read-only. |
Required | - | The property becomes required. |
ScaffoldColumn | ShowForEdit | Whether the property is shown when editing. |
Additional EPiServer attributes
Attribute | Property | Effect |
---|---|---|
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. |
Validate according to the MVC style model
The CMS 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 the 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. 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 of them into the client'sregEx
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 a result, StringLength
 is not processed. The correct way to do this is to define the 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;
}
Validate 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(['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 the widget is created, the framework tries to mix in validation properties. 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 indicates 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 the onChange
event and call the validate method.
Updated 7 months ago