Five New Optimizely Certifications are Here! Validate your expertise and advance your career with our latest certification exams. Click here to find out more

nguyen
Jan 20, 2014
  13901
(0 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

Please login to comment.
Latest blogs
Optimizely Configured Commerce and Spire CMS - Figuring out Handlers

I recently entered the world of Optimizely Configured Commerce and Spire CMS. Intriguing, interesting and challenging at the same time, especially...

Ritu Madan | Mar 12, 2025

Another console app for calling the Optimizely CMS REST API

Introducing a Spectre.Console.Cli app for exploring an Optimizely SaaS CMS instance and to source code control definitions.

Johan Kronberg | Mar 11, 2025 |

Extending UrlResolver to Generate Lowercase Links in Optimizely CMS 12

When working with Optimizely CMS 12, URL consistency is crucial for SEO and usability. By default, Optimizely does not enforce lowercase URLs, whic...

Santiago Morla | Mar 7, 2025 |

Optimizing Experiences with Optimizely: Custom Audience Criteria for Mobile Visitors

In today’s mobile-first world, delivering personalized experiences to visitors using mobile devices is crucial for maximizing engagement and...

Nenad Nicevski | Mar 5, 2025 |

Unable to view Optimizely Forms submissions when some values are too long

I discovered a form where the form submissions could not be viewed in the Optimizely UI, only downloaded. Learn how to fix the issue.

Tomas Hensrud Gulla | Mar 4, 2025 |

CMS 12 DXP Migrations - Time Zones

When it comes to migrating a project from CMS 11 and .NET Framework on the DXP to CMS 12 and .NET Core one thing you need to be aware of is the...

Scott Reed | Mar 4, 2025