nguyen
Jan 20, 2014
  24746
(4 votes)

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.

ObjectEditing

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:

containers1

Example 1: Layout containers for pages and shared blocks. Object container is the tab container and group containers are the tab pages

containers2

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.

country-region

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!

Jan 20, 2014

Comments

Jan 20, 2014 05:15 PM

Great post, really helpful property control

Marija Jemuovic
Marija Jemuovic May 29, 2014 09:54 AM

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?

Marija Jemuovic
Marija Jemuovic May 30, 2014 04:10 PM

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.

Jun 26, 2014 03:08 PM

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...

Arve Systad
Arve Systad Jan 16, 2015 02:25 PM

You should mention where these files should be located, though.








And then a folder &quot;LocationBlockEditor&quot; beneath ClientResources. Then setting ClientEditingClass to "myproject.LocationBlockEditor.FilterableSelectionEditor" and ClientLayoutClass in the editor descriptor to "myproject.LocationBlockEditor.LocalBranchContainer".

Bharat  Tyagi
Bharat Tyagi Jan 30, 2015 07:15 AM

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.

Jul 21, 2015 06:54 PM

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. 

jani.rantanen
jani.rantanen May 6, 2016 08:39 AM

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?

tobias.gladh@knowit.se
tobias.gladh@knowit.se Apr 11, 2017 08:06 AM

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

Chaudhry Mohsin Ali
Chaudhry Mohsin Ali Dec 11, 2017 01:48 PM

Where should be the files placed? I am talking about JS files. 

Thanks

Chaudhry Mohsin Ali
Chaudhry Mohsin Ali Dec 11, 2017 02:52 PM

So I have two js files under

ClientResources/Scripts/Editors

In my module.config










    [EditorDescriptorRegistration(TargetType = typeof(LocationBlock))]
    public class LocationBlockEditorDescriptor : EditorDescriptor
    {
        public override void ModifyMetadata(EPiServer.Shell.ObjectEditing.ExtendedMetadata metadata, IEnumerable attributes)
        {
            base.ModifyMetadata(metadata, attributes);
            metadata.Properties.Cast().First().GroupSettings.ClientLayoutClass = "alloy/editors/LocationBlockContainer";

            metadata.Properties.Cast().First().ClientEditingClass = "alloy/editors/FilterableSelectionEditor";
        }
    }

But the filter doesn't seem to work properly. No matter which country I choose I have all the regions. 

Sam
Sam May 17, 2019 10:58 AM

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

Please login to comment.
Latest blogs
Increase timeout for long running SQL queries using SQL addon

Learn how to increase the timeout for long running SQL queries using the SQL addon.

Tomas Hensrud Gulla | Dec 20, 2024 | Syndicated blog

Overriding the help text for the Name property in Optimizely CMS

I recently received a question about how to override the Help text for the built-in Name property in Optimizely CMS, so I decided to document my...

Tomas Hensrud Gulla | Dec 20, 2024 | Syndicated blog

Resize Images on the Fly with Optimizely DXP's New CDN Feature

With the latest release, you can now resize images on demand using the Content Delivery Network (CDN). This means no more storing multiple versions...

Satata Satez | Dec 19, 2024

Simplify Optimizely CMS Configuration with JSON Schema

Optimizely CMS is a powerful and versatile platform for content management, offering extensive configuration options that allow developers to...

Hieu Nguyen | Dec 19, 2024