Creating Modular Settings with Blocks

2014-05-16: Updated the code to include code that disappeared due to non html-encoded code.

Through the years there has been many ways to add settings to an EPiServer site. To mention a few there are GuiPlugins in the Admin Mode Plug-in Manager, Dynamic Properties, Global Properties on the Start Page, special Settings Page Types etc.

First place: The Settings Page!

SettingsPage One of my favourites was the concept of a “Settings Page”! You’ve got version history, language management, you’re not mixing content and settings! But I would like to have it a little more modular where I could contain the Settings data close to where it is used. For example if I had a Gallery I would like to keep the Gallery settings in that Namespace or even that assembly. But after only a year in a busy project, that Settings Page Type could be bloated with properties, tabs and what not! That I had to add more Properties to that greedy all-knowing SettingsPage class each time I need to add a few more settings made that Page Type to something big and ugly that only a mother would love. Not to mention how difficult it is to find that exact property in Edit Mode and reversing a version in the version manager without accidentally reversing some other setting for some other module.

The runner up: Plug-ins!

GuiPlugIn This is an old sturdy trustworthy concept. I can even drop a complete assembly containing almost everything I need to have my Image Gallery (just as an example) up and running and for settings can easily be managed in the Plug-in Manager without being mixed with other settings in the code structure. But with today’s features where we’re getting used to version and language management we are no longer interested in this old plain data management. Not to forget that the user has to be an administrator to be able to access the settings and we’re easily getting back to the bloated editor view filled with many fields screaming for attention.

The new kid in town: Blocks!

SettingsBlock Now with Blocks in EPiServer 7 the concept “A Page in EPiServer is necessarily not something you can browse to” becomes a fact since a PageData in previous versions was used to contain data of various sorts – such as Settings. By using Blocks in EPiServer for settings, not only can the settings be modular, but you also have some sweet toys such as

  • Access Rights management through the ACL (and the forget-me-not Access to Tabs). If you play your cards right you could also set different settings for different users based on VisitorGroups and Roles.
  • Version management that only handles versions for your current module.
  • Language Management and even being able to set Fallback Languages for a specific Settings.
  • Fallback/Default values capabilities where you can add logic to the Get/Set Properties as well as the SetDefaultValues method.

I’m aware of the cons where it can end up with multiple Blocks instead of a single view filled with multiple fields. I think that this is a start where we can add a rich Settings Manager and improve the experience for the Editor and still have a strong and flexible data layer underneath. I have made a quick Proof of Concept of my idea and it can be found on GitHub.

Cut to the Code!

First, I created an Interface, in this Draft it doesn’t do anything else except helping me identify that this is one of my Settings.

public interface ISettingsBlock : IContentData
{
}

All I need to do is to implement this to my Block. Note that I’ve also made this unavailable for editors. I don’t want this thing being created all over the place!

[ContentType(DisplayName = "My Settings", GUID = "bc9d37bd-c725-4b8b-8a23-039710f476bf", Description = "", AvailableInEditMode = false)]
public class MySettingsBlock : BlockData, ISettingsBlock
{
    [Display(
        Name = "My Setting",
        GroupName = SystemTabNames.Content,
        Order = 1)]
    public virtual string MySetting { get; set; }
        
    [Display(
        Name = "My Setting with a Fallback",
        GroupName = SystemTabNames.Content,
        Order = 2)]
    public virtual string MySettingWithFallback {
        get
        {
            var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
            return this["MySettingWithFallback"] as string
                ?? contentLoader.Get(ContentReference.StartPage).Name;
        }
        set { this["MySettingWithFallback"] = value; } }
}

And since we want to easily find our Settings Blocks and don’t want to manually create them, I’ve added a little utility class for this.

public class SettingsRepository<T> where T : BlockData, ISettingsBlock
{
    #region [ Private Fields ]
    private const string RootFolderName = "[Settings]";
    private static readonly ConcurrentDictionary<string, object> MiniLocks = new ConcurrentDictionary<string, object>();

    #endregion

    protected virtual ContentReference ParentFolder
    {
        get
        {
            return GetOrCreate(ContentReference.SiteBlockFolder, "[Settings]");
        }
    }

    protected virtual string DefaultBlockName
    {
        get
        {
            var repo = ServiceLocator.Current.GetInstance<BlockTypeRepository>();
            var blockType = repo.Load<T>();
            return blockType.DisplayName;
        }
    }

    public T Instance
    {
        get
        {
            return GetOrCreateSettingsBlock();
        }
    }

    protected virtual T GetOrCreateSettingsBlock()
    {
        ContentReference contentLink = this.GetOrCreate(this.GetOrCreateContentFolder(), this.DefaultBlockName);
        var repo = ServiceLocator.Current.GetInstance<IContentRepository>();
        return repo.Get<T>(contentLink);
    }

    private ContentReference GetOrCreateContentFolder()
    {
        var repo = ServiceLocator.Current.GetInstance<IContentRepository>();
        ContentReference contentLink;
        var child = repo.GetChildren<ContentFolder>(ContentReference.SiteBlockFolder, LanguageSelector.AutoDetect(true)).FirstOrDefault(x => x.Name.Equals(RootFolderName));

        if (child == null)
        {
        const string Key = "ContentFolder";

        // Lock to prevent duplicates from being created.
        object miniLock = MiniLocks.GetOrAdd(Key, k => new object());

        lock (miniLock)
        {
            var childInLock = repo.GetChildren<ContentFolder>(ContentReference.SiteBlockFolder, LanguageSelector.AutoDetect(true)).FirstOrDefault(x => x.Name.Equals(RootFolderName));

            if (childInLock == null)
            {
            var clone = repo.GetDefault<ContentFolder>(ContentReference.SiteBlockFolder);
            clone.Name = RootFolderName;
            contentLink = repo.Save(clone, SaveAction.Publish, AccessLevel.NoAccess);
            }
            else
            {
            contentLink = childInLock.ContentLink;
            }

            // Saving some space
            object temp1;

            if (MiniLocks.TryGetValue(Key, out temp1) && (temp1 == miniLock))
            {
            object temp2;
            MiniLocks.TryRemove(Key, out temp2);
            }
        }
        }
        else
        {
        contentLink = child.ContentLink;
        }

        return contentLink;
    }

    private ContentReference GetOrCreate(ContentReference parentLink, string name)
    {
        var repo = ServiceLocator.Current.GetInstance<IContentRepository>();
        ContentReference contentLink;
        var child = repo.GetChildren<T>(parentLink, LanguageSelector.AutoDetect(true)).FirstOrDefault() as IContent;

        if (child == null)
        {
        string key = typeof(T).ToString();
        
        // Lock to prevent duplicates from being created.
        object miniLock = MiniLocks.GetOrAdd(key, k => new object());

        lock (miniLock)
        {
            var childInLock = repo.GetChildren<T>(parentLink, LanguageSelector.AutoDetect(true)).FirstOrDefault() as IContent;

            if (childInLock == null)
            {
            var clone = repo.GetDefault<T>(parentLink) as IContent;
            clone.Name = name;
            contentLink = repo.Save(clone, SaveAction.Publish, AccessLevel.NoAccess);
            }
            else
            {
            contentLink = childInLock.ContentLink;
            }

            // Saving some space
            object temp1;

            if (MiniLocks.TryGetValue(key, out temp1) && (temp1 == miniLock))
            {
            object temp2;
            MiniLocks.TryRemove(key, out temp2);
            }
        }
        }
        else
        {
        contentLink = child.ContentLink;
        }

        return contentLink;
    }
}

To get the setting I’m interested in, all I need to do is

var mySetting = new SettingsRepository<MySettingsBlock>().Instance.MySetting;

My next step would be to maybe move some of the information such as the Block Name to the Interface (and by that to the Block class itself) instead of the Repository but that feels more like a religious discussion. But the thing that would really improve this concept would be a some kind of interface to get an overview of (and possibly edit) all Settings.

Advertisements

6 thoughts on “Creating Modular Settings with Blocks

  1. Really nice. Will have a look in the next project. Not sure i have ever bloated the settingspage but this might be a nice concept though.

  2. Isn’t it so that your code fragments may be missing some generic type parameters (e.g. SettingsRepository)?

  3. GBG-Glenn says:

    How do you resolve your settings page?

    _contentLoader.GetChildren(ContentReference.RootPage)? Would that be efficient?

    • Alf says:

      Hi, could you explain what you meant with “resolving my settings page”?

      To edit the settings I as an editor go to the Block folder called [Settings].
      To user the settings I as developer user new SettingsRepository().Instance.MySetting

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: