Country-Region drop down lists in All properties mode
I have gotten quite a few questions from both the forum and developer support regarding how to populate a drop down list depends on value of another drop down list in All Properties mode. In a recent blog post, my colleague Linus Ekström has mentioned a possible solution by using FormContainer to create a custom editor for the country/region block. In this post, I will present a working example using another approach which in my opinion is more lightweight and more conformal to the architecture behind the All Properties mode.
All Properties mode overview
In EPiServer 7, All Properties mode (formerly know as Form editing mode) is built upon the object editing framework which was designed to be highly extensible and customizable.
The very core server side part of the Object Editing framework is the Metadata provider which inspect the edited object’s and return its metadata. We have different metadata providers for different kinds of object. For example, the annotation based metadata provider for plain C# objects or the content metadata provider for EPiServer’s content objects. All the metadata providers allow developers to extend the generated metadata by implementing metadata extenders. You might have been familiar with editor descriptors which basically are metadata extenders.
On the client side, FormContainer is the central piece. It responsible for creating the entire UI from the retrieved metadata via the Widget Factory. The created structure consists of not only the editor widgets but also the layout containers. The FormContainer does not care how are the widgets arranged and how do they interact with each other. Those responsibilities are delegated to the layout containers. We have 2 types of layout containers: object container and group container. For a better imagination, please have a look at the following examples:
Example 1: Layout containers for pages and shared blocks. Object container is the tab container and group containers are the tab pages
Example 2: Layout containers for block properties (complex properties). Both object container and group containers are a simple flow container with a title bar.
The key point here is that we can configure which container type to be used for both object level and group level. This can be done by changing some certain settings in a metadata extender.
The country-region dropdown lists problem
In this section, I will go through an example on how to re-populate the region drop down list according to the selected country.
Location block
Let’s start with a block type consists of 2 properties Country and Region.
[ContentType(AvailableInEditMode=false)]
public class LocationBlock : BlockData
{
public virtual string Country { get; set; }
public virtual string Region { get; set; }
}
Note that we mark the content type as not available in edit mode since we will only use the block type for complex properties.
[ContentType]
public class TestPage : PageData
{
public virtual LocationBlock Location { get; set; }
}
In the edit mode we can see 2 text boxes. To tell EPiServer to use dropdown lists instead, we need create selection factories and assign them to the properties using the SelectOne attribute.
class Country : ISelectItem
{
public string CountryCode { get; set; }
public string Name { get; set; }
public string Text
{
get
{
return Name;
}
}
public object Value
{
get
{
return CountryCode;
}
}
}
class Region : ISelectItem
{
public string CountryCode { get; set; }
public string RegionCode { get; set; }
public string Name { get; set; }
public string Text
{
get
{
return Name;
}
}
public object Value
{
get
{
return String.Format("{0}-{1}", CountryCode, RegionCode);
}
}
}
class CountrySelectionFactory : ISelectionFactory
{
public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
{
return new Country[]
{
new Country() { CountryCode = "US", Name = "United States" },
new Country() { CountryCode = "SE", Name = "Sweden" }
};
}
}
class RegionSelectionFactory : ISelectionFactory
{
public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
{
return new Region[]
{
new Region() { CountryCode = "US", RegionCode = "NY", Name = "New York" },
new Region() { CountryCode = "US", RegionCode = "CA", Name = "California" },
new Region() { CountryCode = "SE", RegionCode = "AB", Name = "Stockholm" },
new Region() { CountryCode = "SE", RegionCode = "O", Name = "Västra Götaland" }
};
}
}
[ContentType(AvailableInEditMode=false)]
public class LocationBlock : BlockData
{
[SelectOne(SelectionFactoryType = typeof(CountrySelectionFactory))]
public virtual string Country { get; set; }
[SelectOne(SelectionFactoryType = typeof(RegionSelectionFactory))]
public virtual string Region { get; set; }
}
So far so good. We have got 2 drop down list for our properties. However they are not inter-connected so user can still chose country as Sweden and region as California! Make no sense?
FilterableSelectionEditor
When we mark the property with SelectOne attribute, it will use the built-in widget SelectionEditor. Unfortunately, this widget doesn’t support filtering the available options. We need to extend it a little bit:
define([
"dojo/_base/declare",
"dojo/_base/array",
"epi-cms/contentediting/editors/SelectionEditor"
],
function (
declare,
array,
SelectionEditor
) {
return declare([SelectionEditor], {
_allOptions: null,
filter: null,
_setOptionsAttr: function (options) {
// summary: set the options
this._allOptions = options;
this.inherited(arguments, [array.filter(options, this.filter || function () {
// return all options if no filter function provided.
return true;
}, this)]);
},
_setFilterAttr: function (filter) {
// summary: set the option filter function
this._set("filter", filter);
this.set("options", this._allOptions);
}
});
});
Wrap everything up
Let’s look at the diagram again. There are 2 green boxed. They are the things that we need to extend to achieve what we want. First, we need to create a custom group container for our block, then replace the default one using an editor descriptor.
By default, in All properties mode, the group containers are "epi/shell/layout/SimpleContainer". We just extend this container, override the addChild method and hook up the widget we are interested in. Add a new file named LocationBlockContainer under the ClientResources folder.
define([
"dojo/_base/declare",
"dojo/_base/lang",
"epi/shell/layout/SimpleContainer"
],
function (
declare,
lang,
SimpleContainer
) {
return declare([SimpleContainer], {
countryDropdown: null,
regionDropdown: null,
addChild: function (child) {
// Summar: Add a widget to the container
this.inherited(arguments);
if (child.name.indexOf("country") >= 0) {
// If it's the country drop down list
this.countryDropdown = child;
// Connect to change event to update the region drop down list
this.own(this.countryDropdown.on("change", lang.hitch(this, this._updateRegionDropdown)));
} else if (child.name.indexOf("region") >= 0) {
// If it's the region drop down list
this.regionDropdown = child;
// Update the region drop down
this._updateRegionDropdown(this.countryDropdown.value);
}
},
_updateRegionDropdown: function (country) {
// Summary: Update the region drop down list according to the selected country
// Clear the current value
this.regionDropdown.set("value", null);
// Set the filter
this.regionDropdown.set("filter", function (region) {
// Oops, the region code is prefixed with country code, for the simplicity
return region.value.indexOf(country) === 0;
});
}
});
});
And tell EPiServer that we want to use it for the LocationBlock:
[EditorDescriptorRegistration(TargetType = typeof(LocationBlock))]
public class LocationBlockEditorDescriptor : EditorDescriptor
{
public override void ModifyMetadata(Shell.ObjectEditing.ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
{
base.ModifyMetadata(metadata, attributes);
metadata.Properties.Cast<ExtendedMetadata>().First().GroupSettings.ClientLayoutClass = "alloy/LocationBlockContainer";
}
}
Last but not least, we need to indicate that we will use the FilterableSelectionEditor for the Region property.
[ClientEditor(ClientEditingClass = "alloy/editors/FilterableSelectionEditor", SelectionFactoryType = typeof(RegionSelectionFactory))]
public virtual string Region { get; set; }
And we are done!
Conclusion
I hope that this small example will help to explain a little bit more on how the Edit mode is built in EPiServer. It’s not a cake walk to extend at the moment but doable with not so much trouble. We will hopefully improve the public APIs and probably add more convenient methods, extension points, … in the future release to make it easier and more fun. Happy extending and overriding!
Comments