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!
Great post, really helpful property control
Hey, I've implemented my own similar functionality that contains: Url + dropdown, where I reload the dropdown when a URL is selected.
The problem I am having is the first load - the change event for the previously selected URL doesn't fire. Is there any other event I can hook to and trigger the change automatically?
Hey, I've found a workaround for this issue: http://www.mogul.com/en/about-mogul/blog/workaround-for-extending-the-linkitemcollection-to-support-anchors
If you have time to look at it and you know a neater way, please let me know.
This is really great. I wonder if we can find a good generic way to extend this - perhaps through adding a 'DependentProperty' parameter to the SelectOne attribute...
You should mention where these files should be located, though.
And then a folder "LocationBlockEditor" beneath ClientResources. Then setting ClientEditingClass to "myproject.LocationBlockEditor.FilterableSelectionEditor" and ClientLayoutClass in the editor descriptor to "myproject.LocationBlockEditor.LocalBranchContainer".
I followed all the steps given above but couldn't accomplish this. I am getting a number of JS errors..Can somebody please post all the steps..including minor once to complete this..Will be a great help. Thanks.
I followed all the steps given above but I could accomplish till populating the dropdowns, then couldn't accomplish further steps. Can somebody please post detailed steps..including minor once to complete this..Will be a great help. Thanks.
Tried to follow these instructions, but I couldn't make this work. Dropdown doesn't filter, but shows all items instead.
Could someone post simple project that contains working example?
Having issues with this after upgrade to EpiServer 10.0.1. http://world.episerver.com/forum/developer-forum/-Episerver-75-CMS/Thread-Container/2017/4/linked-dropdown-does-not-display-value/. The value of the second dropdown is reset in alla properties mode when publishing although the correct value is set on the page/block
Where should be the files placed? I am talking about JS files.
Thanks
So I have two js files under
ClientResources/Scripts/Editors
In my module.config
But the filter doesn't seem to work properly. No matter which country I choose I have all the regions.
Using Episerver 10.10.4 and selected value not displaying on refresh.
Needed to add paths the module.config as in other comments. And also the line:
[ClientEditor(ClientEditingClass = "alloy/editors/FilterableSelectionEditor", SelectionFactoryType = typeof(RegionSelectionFactory))]
replaces
[SelectOne(SelectionFactoryType = typeof(RegionSelectionFactory))]
which is maybe not clear