Views: 9519
Number of votes: 4
Average rating:

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!

    (By Jacob Khan , 20 January 2014 17:15, Permanent link)

    Great post, really helpful property control

    (By Marija Jemuovic , 29 May 2014 09:54, Permanent link)

    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?

    (By Marija Jemuovic , 30 May 2014 16:10, Permanent link)

    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.

    (By ath , 26 June 2014 15:08, Permanent link)

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

    (By Arve Systad , 16 January 2015 14:25, Permanent link)

    You should mention where these files should be located, though.
    <module>
    <dojo>
    <paths>
    <add name="myproject" path="" />
    </paths>
    </dojo>
    </module>

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

    (By Bharat Tyagi , 30 January 2015 07:15, Permanent link)

    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.

    (By Guest , 21 July 2015 18:54, Permanent link)

    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. 

    (By jani.rantanen , 06 May 2016 08:39, Permanent link)

    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?

    (By Tobias Gladh , 11 April 2017 08:06, Permanent link)

    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

  Please login to post a comment