Pages

Monday, June 27, 2016

SelectionFactory fun in Episerver - Part 1

Drop Down lists are a common request and question in the Episerver world, for anyone fairly new to the platform. That's because Drop Down lists are a great way to provide users with a straight forward interface for providing a value to a property; the options for values are right in front of them. They also make it easier for a developer working with those property values, because they know what the possible values are that they will be working with. Unfortunately, someone new to the Episerver platform might not know the best way to start creating properties with Drop Down lists.

Linus Ekstrom (http://world.episerver.com/blogs/Linus-Ekstrom/Dates/2013/12/SingleMultiple-selection-in-EPiServer-75/) blogged about the basic approach of using SelectOne and SelectMany with predefined values in 7.5, and Joel Abrahamsson (http://joelabrahamsson.com/enum-properties-with-episerver/) and Anders Nordby (https://andersnordby.wordpress.com/2014/10/16/enum-properties-with-selectone-and-selectmany/), along with several others, have written good blog articles about working with SelectOne and SelectMany properties sourced by Enums.

Aside from linking to those articles to familiarize you with the people (they're pretty cool) and the approaches, this article is to help provide additional methods for building out lists for SelectOne and SelectMany properties you want to use Drop Down lists for.

A Quick Intro

SelectOne and SelectMany

To create selection options in Episerver, you utilize the SelectOne and SelectMany attributes (http://world.episerver.com/documentation/Items/Developers-Guide/EPiServer-CMS/9/Content/Properties/Property-types/Single-or-multiple-list-options/), both located in the EPiServer.Shell.ObjectEditing namespace. The SelectMany attribute generates check box options in the UI, and the SelectOne attribute generates a Drop Down list. Each one requires a SelectionFactoryType to specify the source of the data for the selection options.

For this article, I am only going to focus on the SelectOne uses.

What is a SelectionFactory?

Think of what a factory is: a place where things are built or constructed. With that in mind, a SelectionFactory is the location where the selection options are constructed. Like a factory, the source of the parts can be from wherever you like, but the end result is always an IEnumerable<ISelectItem>. To define a SelectionFactory, you create a class that inherits the ISelectionFactory interface from the EPiServer.Shell.ObjectEditing namespace. You must also implement the GetSelections method, and inside there you can define your list with whatever SelectItem values you want.

Building Some Factories

As mentioned prior, the purpose of this article is to provide some alternative source approaches for SelectionFactories. The articles I linked to are a great way to get introduced to some common uses, and I actually use the Enum approach quite a bit, because it is very easy to work with an Enum throughout code. Sometimes, though, you need something a bit more dynamic.

A Simple App Settings Source

Despite all the additional methods for storing values in the CMS for settings and options, I am still a fan of using AppSettings for occasional items. For certain semi-permanent values that I would not expect to change often, but I want the flexibility from a developer standpoint, just in case, I might turn to AppSettings.

Creating a SelectionFactory that provides values from an AppSettings key is pretty straight forward if you have ever read AppSettings values before. The example below reads a simple comma delimited string of values from a "BaseClasses" key in the AppSettings, and creates an IEnumerable<ISelectItem> of those values:
[pre class="brush:xml" title="AppSettings Key"]
<add key="BaseClasses" value="bgRed,bgBrightRed,bgGreen,bgHighlighted"/>
[/pre]
[pre class="brush:csharp;" title="Simple AppSettings SelectionFactory"]
public class BaseClassSelectionFactory : ISelectionFactory
{
    public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
    {
        string selectionString = ConfigurationManager.AppSettings["BaseClasses"];
        if (!string.IsNullOrEmpty(selectionString))
        {
            var values = selectionString.Split(',');
            foreach (var value in values)
            {
                yield return new SelectItem { Text = value, Value = value };
            }
        }
    }
}
[/pre]

As you can see, the value is set to the Text and Value properties of the SelectItem, meaning the option the user sees will be the same as the value. Sometimes this is not the desired approach, as we might want to display a user-friendly option while keeping the underlying value. After all, that is why there is both a Text and Value property to set. We can modify the string to use a colon separator for Key and Value, and split the values in the factory:
[pre class="brush:xml" title="Modified AppSettings Key"]
<add key="BaseClasses" value="Red Alert:bgRed,Stop Red:bgBrightRed,Continue Green:bgGreen,Highlighted Section:bgHighlighted"/>
[/pre]
[pre class="brush:csharp;" title="Nicer AppSettings SelectionFactory"]
public class BaseClassSelectionFactory : ISelectionFactory
{
    public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
    {
        string selectionString = ConfigurationManager.AppSettings["BaseClasses"];
        if (!string.IsNullOrEmpty(selectionString))
        {
            var values = selectionString.Split(',');
            foreach (var value in values)
            {
                var textValue = value.Split(':');
                yield return new SelectItem { Text = textValue[0], Value = textValue[1] };
            }
        }
    }
}
[/pre]
And now we can specify the Text and Value via AppSettings entries.

To use this for a property, you decorate the Block or Page property with the SelectOne or SelectMany attribute, and specify the SelectionFactoryType:
[pre class="brush:csharp;" title="Using our Factory"]
[SelectOne(SelectionFactoryType = typeof(BaseClassSelectionFactory))]
public virtual string BaseClass { get; set; }
[/pre]

A Flexible AppSettings Source

The downside to the above approach is, since the Key for the AppSettings entry is specified in the SelectionFactory, we have to create a factory like this for each configuration we want to use it for. Instead, it would be easier if there was a way to pass the configuration Key to the SelectionFactory, and only define the factory once.

We can accomplish this by creating a custom attribute in addition to the SelectionFactory. By defining an Attribute that inherits from SelectOneAttribute in the EPiServer.Shell.ObjectEditing namespace, we can simplify the way we apply the factory. By also inheriting from the System.Web.Mvc.IMetadataAware interface, we can pass additional metadata to the factory, including Attribute properties.

To kick this off, we first must define our attribute. I am going to name it ConfigSelectionAttribute to indicate it's general purpose and how it relates to the SelectionFactory. I am also going to define a ConfigKey property to specify the Key of the AppSettings entry we want. Finally, because it's inheriting IMetadataAware, I am implementing an OnMetadataCreated method and defining the SelectionFactoryType there.
[pre class="brush:csharp;" title="ConfigSelectionAttribute"]
public class ConfigSelectionAttribute : SelectOneAttribute, IMetadataAware
{
    public ConfigSelectionAttribute(string configKey)
    {
        ConfigKey = configKey;
    }

    public string ConfigKey { get; set; }

    public new void OnMetadataCreated(ModelMetadata metadata)
    {
        SelectionFactoryType = typeof(ConfigSelectionFactory);
        base.OnMetadataCreated(metadata);
    }
}
[/pre]
With the attribute created, we can modify the previous SelectionFactory example to make it work with the ConfigKey property. First, let's change the name of the factory to match it's use, and call it ConfigSelectionFactory. Then, from within the factory, we need to get access to the ConfigKey property, and since that is within our attribute, we will need to grab our ConfigSelectionAttribute first. We can then use the ConfigKey property to get the proper AppSettings entry and build our IEnumerable<ISelectItem> the same as before.
[pre class="brush:csharp" title="ConfigSelectionFactory"]
public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
{
    // find the ConfigSelectionAttribute for this object
    var configSelectAttribute = metadata.Attributes.OfType<ConfigSelectionAttribute>().SingleOrDefault();

    if (configSelectAttribute != null)
    {
        // use the ConfigKey property from the attribute to get the AppSettings entry
        string selectionString = ConfigurationManager.AppSettings[configSelectAttribute.ConfigKey];

        if (!string.IsNullOrEmpty(selectionString))
        {
            var values = selectionString.Split(',');
            foreach (var value in values)
            {
                var textValue = value.Split(':');
                yield return new SelectItem { Text = textValue[0], Value = textValue[1] };
            }
        }
    }
}
[/pre]
The key to this is the ExtendedMetadata parameter passed to the GetSelections method that allows us to get the attributes. This is possible because of the inheritance from IMetadataAware on the attribute.

Now, the ConfigSelectionAttribute can be used to decorate our property, supplying the Key for the the configuration we want to use:
[pre class="brush:xml" title="Same AppSettings Key"]
<add key="BaseClasses" value="Red Alert:bgRed,Stop Red:bgBrightRed,Continue Green:bgGreen,Highlighted Section:bgHighlighted"/>
[/pre]
[pre class="brush:csharp" title="Using the New Approach"]
[ConfigSelection("BaseClasses")]
public virtual string BaseClass { get; set; }
[/pre]
With this approach, we can create any number of keys in the ApplicationSettings with the same string format to generate Drop Down lists for properties, just by specifying the name of the Key in the ConfigSelection attribute.

That's Not All

This isn't all the fun I want to have with the SelectionFactory, so I will be following up with a Part 2 post that expands this idea a bit for those who don't want to use AppSettings for storing data. After all, we are using a CMS, so why can't we do something without a config file?


1 comment: