Home » Stardard Values

Category Archives: Stardard Values

Expand New Tokens Added to Standard Values on All Items Using Its Template in Sitecore

If you have read some of my older posts, you probably know by now how much I love writing code that expands tokens on Items in Sitecore, and decided to build another solution that expands new tokens added to Standard Values Items of Templates — out of the box, these aren’t expanded on preexisting Items that use the Template of the Standard Values Item, and end up making their way in fields on those preexisting Items (for an alternative solution, check out this older post I wrote some time ago).

In the following solution — this solution is primarily composed of a custom pipeline — tokens that are added to fields on the Standard Values Item will be expanded on all Items that use the Template of the Standard Values Item after the Standard Values Item is saved in the Sitecore client (I hook into the <saveUI> pipeline for this action on save).

We first need a class whose instance serves as the custom pipeline’s argument object:

using Sitecore.Data.Items;
using Sitecore.Pipelines;
using System.Collections.Generic;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class ExpandNewTokensOnAllItemsArgs : PipelineArgs
    {
        public Item StandardValuesItem { get; set; }

        private List<Item> items;
        public List<Item> Items 
        {
            get
            {
                if(items == null)
                {
                    items = new List<Item>();
                }

                return items;
            }
            set
            {
                items = value;
            }
        }
    }
}

The caller of the custom pipeline is required to pass the Standard Values Item that contains the new tokens. One of the processors of the custom pipeline will collect all Items that use its Template — these are stored in the Items collection property.

The instance of the following class serves as the first processor of the custom pipeline:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class EnsureStandardValues
    {
        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem");
            if(IsStandardValues(args.StandardValuesItem))
            {
                return;
            }

            args.AbortPipeline();
        }

        protected virtual bool IsStandardValues(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return StandardValuesManager.IsStandardValuesHolder(item);
        }
    }
}

This processor basically just ascertains whether the Item passed as the Standard Values Item is indeed a Standard Values Item — the code just delegates to the static IsStandardValuesHolder() method on Sitecore.Data.StandardValuesManager (this lives in Sitecore.Kernel.dll).

The instance of the next class serves as the second step of the custom pipeline:

using System.Collections.Generic;
using System.Linq;

using Sitecore.Data.Fields;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class EnsureUnexpandedTokens
    {
        private List<string> Tokens { get; set; }

        public EnsureUnexpandedTokens()
        {
            Tokens = new List<string>();
        }

        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem");
            if (!Tokens.Any())
            {
                args.AbortPipeline();
                return;
            }

            args.StandardValuesItem.Fields.ReadAll();
            foreach(Field field in args.StandardValuesItem.Fields)
            {
                if(HasUnexpandedTokens(field))
                {
                    return;
                }
            }
            
            args.AbortPipeline();
        }

        protected virtual bool HasUnexpandedTokens(Field field)
        {
            Assert.ArgumentNotNull(field, "field");
            foreach(string token in Tokens)
            {
                if(field.Value.Contains(token))
                {
                    return true;
                }
            }

            return false;
        }
    }
}

A collection of tokens are injected into the class’ instance via the Sitecore Configuration Factory — see the patch configuration file further down in this post — and determines if tokens exist in any of its fields. If no tokens are found, then the pipeline is aborted. Otherwise, we exit the Process() method immediately.

The instance of the following class serves as the third processor of the custom pipeline:

using System;
using System.Collections.Generic;
using System.Linq;

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class CollectAllItems
    {
        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem");
            args.Items = GetAllItemsByTemplateID(args.StandardValuesItem.TemplateID);
            if(args.Items.Any())
            {
                return;
            }

            args.AbortPipeline();
        }

        protected virtual List<Item> GetAllItemsByTemplateID(ID templateID)
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(templateID), "templateID", "templateID cannot be null or empty!");
            using (var context = ContentSearchManager.GetIndex("sitecore_master_index").CreateSearchContext())
            {
                var query = context.GetQueryable<SearchResultItem>().Where(i => i.TemplateId == templateID);
                return query.ToList().Select(result => result.GetItem()).ToList();
            }  
        }
    }
}

This class uses the Sitecore.ContentSearch API to find all Items that use the Template of the Standard Values Item. If at least one Item is found, we exit the Process() method immediately. Otherwise, we abort the pipeline.

The instance of the class below serves as the fourth processor of the custom pipeline:

using System;
using System.Collections.Generic;
using System.Linq;

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class FilterStandardValuesItem
    {
        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Items, "args.Items");
            if(!args.Items.Any())
            {
                return;
            }

            args.Items = args.Items.Where(item => !IsStandardValues(item)).ToList();
        }

        protected virtual bool IsStandardValues(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return StandardValuesManager.IsStandardValuesHolder(item);
        }
    }
}

The code in this class ensures the Stardard Values Item is not in the collection of Items. It’s probably not a good idea to expand tokens on the Standard Values Item. 🙂

The instance of the next class serves as the final processor of the custom pipeline:

using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Data;

namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems
{
    public class ExpandTokens
    {
        private MasterVariablesReplacer TokenReplacer { get; set; }

        public ExpandTokens()
        {
            TokenReplacer = GetTokenReplacer();
        }

        public void Process(ExpandNewTokensOnAllItemsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Items, "args.Items");
            if (!args.Items.Any())
            {
                args.AbortPipeline();
                return;
            }

            foreach(Item item in args.Items)
            {
                ExpandTokensOnItem(item);
            }
        }

        protected virtual void ExpandTokensOnItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            item.Fields.ReadAll();
            item.Editing.BeginEdit();
            TokenReplacer.ReplaceItem(item);
            item.Editing.EndEdit();
        }

        protected virtual MasterVariablesReplacer GetTokenReplacer()
        {
            return Factory.GetMasterVariablesReplacer();
        }
    }
}

The code above uses the instance of Sitecore.Data.MasterVariablesReplacer (subclass or otherwise) — this is defined in your Sitecore configuration at settings/setting[@name=”MasterVariablesReplacer”] — and passes all Items housed in the pipeline argument instance to its ReplaceItem() method — each Item is placed in an editing state before having their tokens expanded.

I then built the following class to serve as a <saveUI> pipeline processor (this pipeline is triggered when someone saves an Item in the Sitecore client):

using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Pipelines.Save;

using Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems;

namespace Sitecore.Sandbox.Pipelines.SaveUI
{
    public class ExpandNewStandardValuesTokens
    {
        private string ExpandNewTokensOnAllItemsPipeline { get; set; }

        public void Process(SaveArgs args)
        {
            Assert.IsNotNullOrEmpty(ExpandNewTokensOnAllItemsPipeline, "ExpandNewTokensOnAllItemsPipeline must be set in configuration!");
            foreach (SaveArgs.SaveItem saveItem in args.Items)
            {
                Item item = GetItem(saveItem);
                if(IsStandardValues(item))
                {
                    ExpandNewTokensOnAllItems(item);
                }
            }
        }

        protected virtual Item GetItem(SaveArgs.SaveItem saveItem)
        {
            Assert.ArgumentNotNull(saveItem, "saveItem");
            return Client.ContentDatabase.Items[saveItem.ID, saveItem.Language, saveItem.Version];
        }

        protected virtual bool IsStandardValues(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return StandardValuesManager.IsStandardValuesHolder(item);
        }

        protected virtual void ExpandNewTokensOnAllItems(Item standardValues)
        {
            CorePipeline.Run(ExpandNewTokensOnAllItemsPipeline, new ExpandNewTokensOnAllItemsArgs { StandardValuesItem = standardValues });
        }
    }
}

The code above invokes the custom pipeline when the Item being saved is a Standard Values Item — the Standard Values Item is passed to the pipeline via a new ExpandNewTokensOnAllItemsArgs instance.

I then glued all of the pieces above together in the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <expandNewTokensOnAllItems>
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.EnsureStandardValues, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.EnsureUnexpandedTokens, Sitecore.Sandbox">
          <Tokens hint="list">
            <Token>$name</Token>
            <Token>$id</Token>
            <Token>$parentid</Token>
            <Token>$parentname</Token>
            <Token>$date</Token>
            <Token>$time</Token>
            <Token>$now</Token>
          </Tokens>
        </processor>
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.CollectAllItems, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.FilterStandardValuesItem, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.ExpandTokens, Sitecore.Sandbox" />
      </expandNewTokensOnAllItems>
    </pipelines>
    <processors>
      <saveUI>
        <processor patch:before="saveUI/processor[@type='Sitecore.Pipelines.Save.Save, Sitecore.Kernel']" 
                   mode="on" type="Sitecore.Sandbox.Pipelines.SaveUI.ExpandNewStandardValuesTokens">
          <ExpandNewTokensOnAllItemsPipeline>expandNewTokensOnAllItems</ExpandNewTokensOnAllItemsPipeline>
        </processor>  
      </saveUI>
    </processors>
  </sitecore>
</configuration>

Let’s see this in action!

I added three new fields to a template, and added some tokens in them:

added-new-tokens

After clicking save, I navigated to one of the content Items that use this Template:

tokens-expanded

As you can see, the tokens were expanded. 🙂

If you have any thoughts on this, please drop a comment.

Advertisement

Choose Template Fields to Display in the Sitecore Content Editor

The other day I was going through search terms people had used to get to my blog, and discovered a few people made their way to my blog by searching for ‘sitecore hide sections in data template’.

I had built something like this in the past, but no longer remember how I had implemented that particular solution — not that I could show you that solution since it’s owned by a previous employer — and decided I would build another solution to accomplish this.

Before I move forward, I would like to point out that Sitecore MVP Andy Uzick wrote a blog post recently showing how to hide fields and sections in the content editor, though I did not have much luck with hiding sections in the way that he had done it — sections with no fields were still displaying for me in the content editor — and I decided to build a different solution altogether to make this work.

I thought it would be a good idea to let users turn this functionality on and off via a checkbox in the ribbon, and used some code from a previous post — in this post I had build an object to keep track of the state of a checkbox in the ribbon — as a model. In the spirit of that object, I defined the following interface:

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public interface IRegistrySettingToggle
    {
        bool IsOn();

        void TurnOn();

        void TurnOff();
    }
}

I then created the following abstract class which implements the interface above, and stores the state of the setting defined by the given key — the key of the setting and the “on” value are passed to it via a subclass — in the Sitecore registry.

using Sitecore.Diagnostics;

using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public abstract class RegistrySettingToggle : IRegistrySettingToggle
    {
        private string RegistrySettingKey { get; set; }

        private string RegistrySettingOnValue { get; set; }

        protected RegistrySettingToggle(string registrySettingKey, string registrySettingOnValue)
        {
            SetRegistrySettingKey(registrySettingKey);
            SetRegistrySettingOnValue(registrySettingOnValue);
        }

        private void SetRegistrySettingKey(string registrySettingKey)
        {
            Assert.ArgumentNotNullOrEmpty(registrySettingKey, "registrySettingKey");
            RegistrySettingKey = registrySettingKey;
        }

        private void SetRegistrySettingOnValue(string registrySettingOnValue)
        {
            Assert.ArgumentNotNullOrEmpty(registrySettingOnValue, "registrySettingOnValue");
            RegistrySettingOnValue = registrySettingOnValue;
        }

        public bool IsOn()
        {
            return Registry.GetString(RegistrySettingKey) == RegistrySettingOnValue;
        }

        public void TurnOn()
        {
            Registry.SetString(RegistrySettingKey, RegistrySettingOnValue);
        }

        public void TurnOff()
        {
            Registry.SetString(RegistrySettingKey, string.Empty);
        }
    }
}

I then built the following class to toggle the display settings for our displayable fields:

using System;

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public class ShowDisplayableFieldsOnly : RegistrySettingToggle
    {
        private const string RegistrySettingKey = "/Current_User/Content Editor/Show Displayable Fields Only";
        private const string RegistrySettingOnValue = "on";

        private static volatile IRegistrySettingToggle current;
        private static object lockObject = new Object();

        public static IRegistrySettingToggle Current
        {
            get
            {
                if (current == null)
                {
                    lock (lockObject)
                    {
                        if (current == null)
                        {
                            current = new ShowDisplayableFieldsOnly();
                        }
                    }
                }

                return current;
            }
        }

        private ShowDisplayableFieldsOnly()
            : base(RegistrySettingKey, RegistrySettingOnValue)
        {
        }
    }
}

It passes its Sitecore registry key and “on” state value to the RegistrySettingToggle base class, and employs the Singleton pattern — I saw no reason for there to be multiple instances of this object floating around.

In order to use a checkbox in the ribbon, we have to create a new command for it:

using System.Linq;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;

using Sitecore.Sandbox.Utilities.ClientSettings;

namespace Sitecore.Sandbox.Commands
{
    public class ToggleDisplayableFieldsVisibility : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            ToggleDisplayableFields();
            Refresh(context);
        }

        private static void ToggleDisplayableFields()
        {
            IRegistrySettingToggle showDisplayableFieldsOnly = ShowDisplayableFieldsOnly.Current;
            if (!showDisplayableFieldsOnly.IsOn())
            {
                showDisplayableFieldsOnly.TurnOn();
            }
            else
            {
                showDisplayableFieldsOnly.TurnOff();
            }
        }

        public override CommandState QueryState(CommandContext context)
        {
            if (!ShowDisplayableFieldsOnly.Current.IsOn())
            {
                return CommandState.Enabled;
            }

            return CommandState.Down;
        }

        private static void Refresh(CommandContext context)
        {
            Refresh(GetItem(context));
        }

        private static void Refresh(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", item.ID), 1);
        }

        private static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            return context.Items.FirstOrDefault();
        }
    }
}

The command above leverages the instance of the ShowDisplayableFieldsOnly class defined above to turn the displayable fields feature on and off.

I followed the creation of the command above with the definition of the ribbon checkbox in the core database:

show-displayable-fields-checkbox-defined

The command name above — which is set in the Click field — is defined in the patch configuration file towards the end of this post.

I then created the following data template with a TreelistEx field to store the displayable fields:

displayable-fields-template

The TreelistEx field above will pull in all sections and their fields into the TreelistEx dialog, but only allow the selection of template fields, as is dictated by the following parameters that I have mapped in its Source field:

DataSource=/sitecore/templates/Sample/Sample Item&IncludeTemplatesForSelection=Template field&IncludeTemplatesForDisplay=Template section,Template field&AllowMultipleSelection=no

I then set this as a base template in my sandbox solution’s Sample Item template:

set-displayable-fields-template-as-base

In order to remove fields, we need a <getContentEditorFields> pipeline processor. I built the following class for to serve as one:

using System;

using Sitecore.Configuration;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Data.Templates;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields;

using Sitecore.Sandbox.Utilities.ClientSettings;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields
{
    public class RemoveUndisplayableFields
    {
        public void Process(GetContentEditorFieldsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Item, "args.Item");
            Assert.ArgumentCondition(!string.IsNullOrWhiteSpace(DisplayableFieldsFieldName), "DisplayableFieldsFieldName", "DisplayableFieldsFieldName must be set in the configuration file!");
            if (!ShowDisplayableFieldsOnly.Current.IsOn())
            {
                return;
            }

            foreach (Editor.Section section in args.Sections)
            {
                AddDisplayableFields(args.Item[DisplayableFieldsFieldName], section);
            }
        }

        private void AddDisplayableFields(string displayableFieldIds, Editor.Section section)
        {
            Editor.Fields displayableFields = new Editor.Fields();
            foreach (Editor.Field field in section.Fields)
            {
                if (IsDisplayableField(displayableFieldIds, field))
                {
                    displayableFields.Add(field);
                }
            }

            section.Fields.Clear();
            section.Fields.AddRange(displayableFields);
        }

        private bool IsDisplayableField(string displayableFieldIds, Editor.Field field)
        {
            if (IsStandardValues(field.ItemField.Item))
            {
                return true;
            }

            if (IsDisplayableFieldsField(field.ItemField))
            {
                return false;
            }

            return IsStandardTemplateField(field.ItemField)
                    || string.IsNullOrWhiteSpace(displayableFieldIds)
                    || displayableFieldIds.Contains(field.ItemField.ID.ToString());
        }
        
        private bool IsDisplayableFieldsField(Field field)
        {
            return string.Equals(field.Name, DisplayableFieldsFieldName, StringComparison.CurrentCultureIgnoreCase);
        }

        private static bool IsStandardValues(Item item)
        {
            if (item.Template.StandardValues != null)
            {
                return item.Template.StandardValues.ID == item.ID;
            }

            return false;
        }

        private bool IsStandardTemplateField(Field field)
        {
            Assert.IsNotNull(StandardTemplate, "The Stardard Template could not be found.");
            return StandardTemplate.ContainsField(field.ID);
        }

        private static Template GetStandardTemplate()
        {
            return TemplateManager.GetTemplate(Settings.DefaultBaseTemplate, Context.ContentDatabase);
        }

        private Template _StandardTemplate;
        private Template StandardTemplate
        {
            get
            {
                if (_StandardTemplate == null)
                {
                    _StandardTemplate = GetStandardTemplate();
                }

                return _StandardTemplate;
            }
        }

        private string DisplayableFieldsFieldName { get; set; }
    }
}

The class above iterates over all fields for the supplied item, and adds only those that were selected in the Displayable Fields TreelistEx field, and also Standard Fields — we don’t want to remove these since they are shown/hidden by the Standard Fields feature in Sitecore — to a new Editor.Fields collection. This new collection is then set on the GetContentEditorFieldsArgs instance.

Plus, we don’t want to show the Displayable Fields TreelistEx field when the feature is turned on, and we are on an item. This field should only display when we are on the standard values item when the feature is turned on — this is how we will choose our displayable fields.

Now we have to handle sections without fields — especially after ripping them out via the pipeline processor above.

I built the following class to serve as a <renderContentEditor> pipeline processor to do this:

using System.Linq;

using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor
{
    public class FilterSectionsWithFields
    {
        public void Process(RenderContentEditorArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.Sections = GetSectionsWithFields(args.Sections);
        }

        private static Editor.Sections GetSectionsWithFields(Editor.Sections sections)
        {
            Assert.ArgumentNotNull(sections, "sections");
            Editor.Sections sectionsWithFields = new Editor.Sections();
            foreach (Editor.Section section in sections)
            {
                AddIfContainsFields(sectionsWithFields, section);
            }

            return sectionsWithFields;
        }

        private static void AddIfContainsFields(Editor.Sections sections, Editor.Section section)
        {
            Assert.ArgumentNotNull(sections, "sections");
            Assert.ArgumentNotNull(section, "section");
            if (!ContainsFields(section))
            {
                return;
            }

            sections.Add(section);
        }

        private static bool ContainsFields(Editor.Section section)
        {
            Assert.ArgumentNotNull(section, "section");
            return section.Fields != null && section.Fields.Any();
        }
    }
}

It basically builds a new collection of sections that contain at least one field, and sets it on the RenderContentEditorArgs instance being passed through the <renderContentEditor> pipeline.

In order for this to work, we must make this pipeline processor run before all other processors.

I tied all of the above together with the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="contenteditor:ToggleDisplayableFieldsVisibility" type="Sitecore.Sandbox.Commands.ToggleDisplayableFieldsVisibility, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <getContentEditorFields>
        <processor type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields.RemoveUndisplayableFields, Sitecore.Sandbox">
          <DisplayableFieldsFieldName>Displayable Fields</DisplayableFieldsFieldName>
        </processor>  
      </getContentEditorFields>
      <renderContentEditor>
        <processor patch:before="processor[@type='Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderSkinedContentEditor, Sitecore.Client']"
                   type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.FilterSectionsWithFields, Sitecore.Sandbox"/>
      </renderContentEditor>
    </pipelines>
  </sitecore>
</configuration>

Let’s try this out.

I navigated to the standard values item for my Sample Item data template, and selected the fields I want to display in the content editor:

selected-some-fields

I then went to an item that uses this data template, and turned on the displayable fields feature:

displayable-fields-on

As you can see, only the fields we had chosen display — along with standard fields since the Standard Fields checkbox is checked.

I then turned off the displayable fields feature:

displayable-fields-off

Now all fields for the item display.

I then turned the displayable fields feature back on, and turned off Standard Fields:

standard-fields-off-displayable-fields-on

Now only our selected fields display.

If you have any thoughts on this, or ideas around making this better, please share in a comment.

Empower Your Content Authors to Expand Standard Values Tokens in the Sitecore Client

Have you ever seen a Standard Values token — $name is an example of a Standard Values token — in an item’s field, and ask yourself “how the world did that get there”, or alternatively, “what can be done to replace it with what should be there?”

This can occur when tokens are added to an item’s template’s Standard Values node after the item was created — tokens are expanded once an item is created, not after the fact.

John West wrote a blog article highlighting one solution for eradicating this issue by using the Sitecore Rules Engine.

In this post, I am proposing a completely different solution — one that empowers content authors to expand unexpanded tokens by clicking a link in a custom content editor warning box.

First, I created a series of utility objects that ascertain whether “Source” objects contain substrings that we are interested in. All implement the following interface:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Sitecore.Sandbox.Utilities.StringUtilities.Base
{
    public interface ISubstringsChecker<T>
    {
        T Source { get; set; }
        
        IEnumerable<string> Substrings { get; set; }

        bool ContainsSubstrings();
    }
}

All of our “checkers” will have a “Source” object, a collection of substrings to look for, and a method to convey to calling code whether a substring in the collection of substrings had been found in the “Source” object.

I decided to create a base abstract class with some abstract methods along with one method to serve as a hook for asserting the Source object — albeit I did not use this method anywhere in my solution (come on Mike, you’re forgetting YAGNI — let’s get with the program).

Utimately, I am employing the template method pattern — all subclasses of this abstract base class will just fill in the defined abstract stubs, and the parent class will take care of the rest:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Sitecore.Sandbox.Utilities.StringUtilities.Base
{
    public abstract class SubstringsChecker<T> : ISubstringsChecker<T>
    {
        public T Source { get; set; }

        public virtual IEnumerable<string> Substrings { get; set; }

        protected SubstringsChecker(T source)
        {
            SetSource(source);
        }

        private void SetSource(T source)
        {
            AssertSource(source);
            Source = source;
        }

        protected virtual void AssertSource(T source)
        {
            // a hook for subclasses to assert Source objects
        }

        public bool ContainsSubstrings()
        {
            if (CanDoCheck())
            {
                return DoCheck();
            }

            return false;
        }

        protected abstract bool CanDoCheck();
        protected abstract bool DoCheck();
    }
}

The first “checker” I created will find substrings in a string. It made sense for me to start here, especially when we are ultimately checking strings at the most atomic level in our series of “checker” utility objects.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.StringUtilities.Base;
using Sitecore.Sandbox.Utilities.StringUtilities.DTO;

namespace Sitecore.Sandbox.Utilities.StringUtilities
{
    public class StringSubstringsChecker : SubstringsChecker<string>
    {
        private StringSubstringsChecker(IEnumerable<string> substrings)
            : this(null, substrings)
        {
        }

        private StringSubstringsChecker(string source, IEnumerable<string> substrings)
            : base(source)
        {
            SetSubstrings(substrings);
        }

        private void SetSubstrings(IEnumerable<string> substrings)
        {
            AssertSubstrings(substrings);
            Substrings = substrings;
        }

        private static void AssertSubstrings(IEnumerable<string> substrings)
        {
            Assert.ArgumentNotNull(substrings, "substrings");
            Assert.ArgumentCondition(substrings.Any(), "substrings", "substrings must contain as at least one string!");
        }

        protected override bool CanDoCheck()
        {
            return !string.IsNullOrEmpty(Source);
        }

        protected override bool DoCheck()
        {
            Assert.ArgumentNotNullOrEmpty(Source, "Source");

            foreach (string substring in Substrings)
            {
                if (Source.Contains(substring))
                {
                    return true;
                }
            }

            return false;
        }

        public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(IEnumerable<string> substrings)
        {
            return new StringSubstringsChecker(substrings);
        }

        public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(string source, IEnumerable<string> substrings)
        {
            return new StringSubstringsChecker(substrings);
        }
    }
}

The next level up from strings would naturally be Fields. I designed this “checker” to consume an instance of the string “checker” defined above — all for the purposes of reuse. The field “checker” delegates calls to its string “checker” instance.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Data.Fields;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.StringUtilities.Base;

namespace Sitecore.Sandbox.Utilities.StringUtilities
{
    public class FieldSubstringsChecker : SubstringsChecker<Field>
    {
        private ISubstringsChecker<string> StringSubstringsChecker { get; set; }

        public override IEnumerable<string> Substrings 
        {
            get
            {
                return StringSubstringsChecker.Substrings;
            }
            set
            {
                StringSubstringsChecker.Substrings = value;
            }
        }

        private FieldSubstringsChecker(ISubstringsChecker<string> stringSubstringsChecker)
            : this(null, stringSubstringsChecker)
        {
        }

        private FieldSubstringsChecker(Field source, ISubstringsChecker<string> stringSubstringsChecker)
            : base(source)
        {
            SetStringSubstringsChecker(stringSubstringsChecker);
        }

        private void SetStringSubstringsChecker(ISubstringsChecker<string> stringSubstringsChecker)
        {
            Assert.ArgumentNotNull(stringSubstringsChecker, "stringSubstringsChecker");
            StringSubstringsChecker = stringSubstringsChecker;
        }

        protected override bool CanDoCheck()
        {
            return Source != null;
        }

        protected override bool DoCheck()
        {
            Assert.ArgumentNotNull(Source, "Source");
            StringSubstringsChecker.Source = Source.Value;
            return StringSubstringsChecker.ContainsSubstrings();
        }

        public static ISubstringsChecker<Field> CreateNewFieldSubstringsChecker(ISubstringsChecker<string> stringSubstringsChecker)
        {
            return new FieldSubstringsChecker(stringSubstringsChecker);
        }

        public static ISubstringsChecker<Field> CreateNewFieldSubstringsChecker(Field source, ISubstringsChecker<string> stringSubstringsChecker)
        {
            return new FieldSubstringsChecker(source, stringSubstringsChecker);
        }
    }
}

If I were to ask you what the next level up in our series of “checkers” would be, I hope your answer would be Items. Below is a “checker” that uses an Item as its “Source” object, and delegates calls to an instance of a field “checker”.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.StringUtilities.Base;

namespace Sitecore.Sandbox.Utilities.StringUtilities
{
    public class ItemSubstringsChecker : SubstringsChecker<Item>
    {
        private ISubstringsChecker<Field> FieldSubstringsChecker { get; set; }

        public override IEnumerable<string> Substrings 
        {
            get
            {
                return FieldSubstringsChecker.Substrings;
            }
            set
            {
                FieldSubstringsChecker.Substrings = value;
            }
        }

        private ItemSubstringsChecker(ISubstringsChecker<Field> fieldSubstringsChecker)
            : this(null, fieldSubstringsChecker)
        {
        }

        private ItemSubstringsChecker(Item source, ISubstringsChecker<Field> fieldSubstringsChecker)
            : base(source)
        {
            SetFieldSubstringsChecker(fieldSubstringsChecker);
        }

        private void SetFieldSubstringsChecker(ISubstringsChecker<Field> fieldSubstringsChecker)
        {
            Assert.ArgumentNotNull(fieldSubstringsChecker, "fieldSubstringsChecker");
            FieldSubstringsChecker = fieldSubstringsChecker;
        }

        protected override bool CanDoCheck()
        {
            return Source != null && Source.Fields != null && Source.Fields.Any();
        }

        protected override bool DoCheck()
        {
            Assert.ArgumentNotNull(Source, "Source");
            bool containsSubstrings = false;

            for (int i = 0; !containsSubstrings && i < Source.Fields.Count; i++)
            {
                containsSubstrings = containsSubstrings || DoesFieldContainSubstrings(Source.Fields[i]);
            }

            return containsSubstrings;
        }

        private bool DoesFieldContainSubstrings(Field field)
        {
            FieldSubstringsChecker.Source = field;
            return FieldSubstringsChecker.ContainsSubstrings();
        }

        public static ISubstringsChecker<Item> CreateNewItemSubstringsChecker(ISubstringsChecker<Field> fieldSubstringsChecker)
        {
            return new ItemSubstringsChecker(fieldSubstringsChecker);
        }

        public static ISubstringsChecker<Item> CreateNewItemSubstringsChecker(Item source, ISubstringsChecker<Field> fieldSubstringsChecker)
        {
            return new ItemSubstringsChecker(source, fieldSubstringsChecker);
        }
    }
}

Next, I wrote code for a content editor warning box. I’m am completely indebted to .NET Reflector in helping out on this front — I looked at other content editor warning pipelines in Sitecore.Pipelines.GetContentEditorWarnings within Sitecore.Kernel to see how this is done:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Configuration;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Pipelines.GetContentEditorWarnings;

using Sitecore.Sandbox.Utilities.StringUtilities.Base;
using Sitecore.Sandbox.Utilities.StringUtilities;

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings
{
    public class HasUnexpandedTokens
    {
        private static readonly ISubstringsChecker<Item> SubstringsChecker = CreateNewItemSubstringsChecker();

        public void Process(GetContentEditorWarningsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (CanExpandTokens(args.Item))
            {
                AddHasUnexpandedTokensWarning(args);
            }
        }

        private static bool CanExpandTokens(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return !IsStandardValues(item) && DoesItemContainUnexpandedTokens(item);
        }

        private static bool DoesItemContainUnexpandedTokens(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            SubstringsChecker.Source = item;
            return SubstringsChecker.ContainsSubstrings();
        }

        private static bool IsStandardValues(Item item)
        {
            return item.Template.StandardValues.ID == item.ID;
        }

        private static void AddHasUnexpandedTokensWarning(GetContentEditorWarningsArgs args)
        {
            GetContentEditorWarningsArgs.ContentEditorWarning warning = args.Add();
            warning.Title = Translate.Text("Some fields contain unexpanded tokens.");
            warning.Text = Translate.Text("To expand tokens, click Expand Tokens.");
            warning.AddOption(Translate.Text("Expand Tokens"), "item:expandtokens");
        }

        private static ISubstringsChecker<Item> CreateNewItemSubstringsChecker()
        {
            return ItemSubstringsChecker.CreateNewItemSubstringsChecker(CreateNewFieldSubstringsChecker());
        }

        private static ISubstringsChecker<Field> CreateNewFieldSubstringsChecker()
        {
            return FieldSubstringsChecker.CreateNewFieldSubstringsChecker(CreateNewStringSubstringsChecker());
        }

        private static ISubstringsChecker<string> CreateNewStringSubstringsChecker()
        {
            return StringSubstringsChecker.CreateNewStringSubstringsContainer(GetTokens());
        }

        private static IEnumerable<string> GetTokens()
        {
            return Factory.GetStringSet("tokens/token");
        }
    }
}

In my pipeline above, I am pulling a collection of token names from Sitecore configuration — these are going to be defined in a patch include file below. I had to go down this road since these tokens are not publically exposed in the Sitecore API.

Plus, we should only allow for the expansion of tokens when not on the Standard values item — it wouldn’t make much sense to expand these tokens here.

Since I’m referencing a new command in the above pipeline — a command that I’ve named “item:expandtokens” — it’s now time to create that new command:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;

using Sitecore.Sandbox.Utilities.StringUtilities;
using Sitecore.Sandbox.Utilities.StringUtilities.Base;
using Sitecore.Sandbox.Utilities.StringUtilities.DTO;

namespace Sitecore.Sandbox.Commands
{
    public class ExpandTokens : Command
    {
        public override void Execute(CommandContext commandContext)
        {
            if (!DoesCommandContextContainOneItem(commandContext))
            {
                return;
            }

            ExpandTokensInItem(GetCommandContextItem(commandContext));
        }

        private static void ExpandTokensInItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            item.Fields.ReadAll();
            item.Editing.BeginEdit();
            ExpandTokensViaMasterVariablesReplacer(item);
            item.Editing.EndEdit();
        }

        private static void ExpandTokensViaMasterVariablesReplacer(Item item)
        {
            MasterVariablesReplacer masterVariablesReplacer = Factory.GetMasterVariablesReplacer();
            masterVariablesReplacer.ReplaceItem(item);
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            if (ShouldHideCommand(commandContext))
            {
                return CommandState.Hidden;
            }
            
            if (ShouldDisableCommand(commandContext))
            {
                return CommandState.Disabled;
            }

            return base.QueryState(commandContext);
        }

        private bool ShouldHideCommand(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            return !DoesCommandContextContainOneItem(commandContext) 
                    || !HasField(GetCommandContextItem(commandContext), FieldIDs.ReadOnly);
        }

        private static bool ShouldDisableCommand(CommandContext commandContext)
        {
            AssertCommandContextItems(commandContext);
            Item item = GetCommandContextItem(commandContext);

            return !item.Access.CanWrite()
                    || Command.IsLockedByOther(item)
                    || !Command.CanWriteField(item, FieldIDs.ReadOnly);
        }

        private static Item GetCommandContextItem(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items");
            return commandContext.Items.FirstOrDefault();
        }

        private static bool DoesCommandContextContainOneItem(CommandContext commandContext)
        {
            AssertCommandContextItems(commandContext);
            return commandContext.Items.Length == 1;
        }

        private static void AssertCommandContextItems(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items");
        }
    }
}

This new command just uses an instance of Sitecore.Data.MasterVariablesReplacer to expand Standard Values tokens on our item.

I had to register this command in the /App_Config/Commands.config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<! -- A bunch of commands defined here -->
	
	<command name="item:expandtokens" type="Sitecore.Sandbox.Commands.ExpandTokens,Sitecore.Sandbox" />
	
	<! -- A bunch more defined here too -->
</configuration>

I then put all the pieces together using a patch include config file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getContentEditorWarnings>
        <processor type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.HasUnexpandedTokens, Sitecore.Sandbox"/>
      </getContentEditorWarnings>
    </pipelines>
    <tokens>
      <token>$name</token>
      <token>$id</token>
      <token>$parentid</token>
      <token>$parentname</token>
      <token>$date</token>
      <token>$time</token>
      <token>$now</token>
    </tokens>
  </sitecore>
</configuration>

Let’s create an item for testing:

expand-tokens-new-item

As you can see, the item’s Title field was populated automatically during item creation — the $name token lives on this item’s template’s Standard Values item in Sitecore and was expanded when we created our test item.

Let’s create the problem we are trying to solve by adding new tokens to our test item’s template’s Standard Values node:

expand-tokens-new-tokens

As you can see, these new tokens appear in fields in our test item. However, don’t fret — we now have a way to fix this issue. 🙂

expand-tokens-content-editor-warning

I clicked on the ‘Expand Tokens’ link, and saw the following:

expand-tokens-expanded-no-warning

Hopefully, this post has given you another weapon to add to your arsenal for solving the unexpanded tokens issue on existing items in Sitecore.

If you find another solution, please drop me a line — I would love to hear about it.

Content Manage Custom Standard Values Tokens in the Sitecore Client

A few days back, John West — Chief Technology Officer at Sitecore USA — blogged about adding custom tokens in a subclass of Sitecore.Data.MasterVariablesReplacer.

One thing that surprised me was how his solution did not use NVelocity, albeit I discovered why: the class Sitecore.Data.MasterVariablesReplacer does not use it, and as John states in this tweet, using NVelocity in his solution would have been overkill — only a finite number of tokens are defined, so why do this?

This kindled an idea — what if we could define such tokens in the Sitecore Client? How would one go about doing that?

This post shows how I did just that, and used NVelocity via a utility class I had built for my article discussing NVelocity

I first created two templates: one that defines the Standard Values variable token — I named this Variable — and the template for a parent Master Variables folder item — this has no fields on it, so I’ve omitted its screenshot:

variable-template

I then defined some Glass.Sitecore.Mapper Models for my Variable and Master Variables templates — if you’re not familiar with Glass, or are but aren’t using it, I strongly recommend you go to http://www.glass.lu/ and check it out!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

using Sitecore.Diagnostics;
using Sitecore.Reflection;

using Glass.Sitecore.Mapper.Configuration.Attributes;

namespace Sitecore.Sandbox.Model
{
    [SitecoreClass(TemplateId="{E14F91E4-7AF6-42EC-A9A8-E71E47598BA1}")]
    public class Variable
    {
        [SitecoreField(FieldId="{34DCAC45-E5B2-436A-8A09-89F1FB785F60}")]
        public virtual string Token { get; set; }

        [SitecoreField(FieldId = "{CD749583-B111-4E69-90F2-1772B5C96146}")]
        public virtual string Type { get; set; }

        [SitecoreField(FieldId = "{E3AB8CED-1602-4A7F-AC9B-B9031FCA2290}")]
        public virtual string PropertyName { get; set; }

        public object _TypeInstance;
        public object TypeInstance
        {
            get
            {
                bool shouldCreateNewInstance = !string.IsNullOrEmpty(Type) 
                                                && !string.IsNullOrEmpty(PropertyName) 
                                                && _TypeInstance == null;

                if (shouldCreateNewInstance)
                {
                    _TypeInstance = CreateObject(Type, PropertyName);
                }

                return _TypeInstance;
            }
        }

        private object CreateObject(string type, string propertyName)
        {
            try
            {
                return GetStaticPropertyValue(type, propertyName);
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }

            return null;
        }

        private object GetStaticPropertyValue(string type, string propertyName)
        {
            PropertyInfo propertyInfo = GetPropertyInfo(type, propertyName);
            return GetStaticPropertyValue(propertyInfo);
        }

        private PropertyInfo GetPropertyInfo(string type, string propertyName)
        {
            return GetType(type).GetProperty(propertyName);
        }

        private object GetStaticPropertyValue(PropertyInfo propertyInfo)
        {
            Assert.ArgumentNotNull(propertyInfo, "propertyInfo");
            return propertyInfo.GetValue(null, null);
        }

        private Type GetType(string type)
        {
            return ReflectionUtil.GetTypeInfo(type);
        }
    }
}

In my Variable model class, I added some logic that employs reflection to convert the defined type into an object we can use. This logic at the moment will only work with static properties, although could be extended for instance properties, and even methods on classes.

Here is the model for the Master Variables folder:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Glass.Sitecore.Mapper.Configuration.Attributes;

namespace Sitecore.Sandbox.Model
{
    [SitecoreClass(TemplateId = "{855A1D19-5475-43CB-B3E8-A4960A81FEFF}")]
    public class MasterVariables
    {
        [SitecoreChildren(IsLazy=true)]
        public virtual IEnumerable<Variable> Variables { get; set; }
    }
}

I then hooked up my Glass models in my Global.asax:

<%@Application Language='C#' Inherits="Sitecore.Web.Application" %>
<%@ Import Namespace="Glass.Sitecore.Mapper.Configuration.Attributes" %>
<script runat="server">
    protected void Application_Start(object sender, EventArgs e)
    {
        AttributeConfigurationLoader loader = new AttributeConfigurationLoader
        (
            new string[] { "Sitecore.Sandbox.Model, Sitecore.Sandbox" }
        );

        Glass.Sitecore.Mapper.Context context = new Glass.Sitecore.Mapper.Context(loader);
    }

    public void Application_End()
    {
    }

    public void Application_Error(object sender, EventArgs args)
    {
    }
</script>

Since my solution only works with static properties, I defined a wrapper class that accesses properties from Sitecore.Context for illustration:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Sitecore.Sandbox.Data
{
    public class SitecoreContextProperties
    {
        public static string DomainName
        {
            get
            {
                return Sitecore.Context.Domain.Name;
            }
        }

        public static string Username
        {
            get
            {
                return Sitecore.Context.User.Name;
            }
        }

        public static string DatabaseName
        {
            get
            {
                return Sitecore.Context.Database.Name;
            }
        }

        public static string CultureName
        {
            get
            {
                return Sitecore.Context.Culture.Name;
            }
        }

        public static string LanguageName
        {
            get
            {
                return Sitecore.Context.Language.Name;
            }
        }
    }
}

I then defined my subclass of Sitecore.Data.MasterVariablesReplacer. I let the “out of the box” tokens be expanded by the Sitecore.Data.MasterVariablesReplacer base class, and expand those defined in the Sitecore Client by using my token replacement utility class and content acquired from the master database via my Glass models:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Text;

using Glass.Sitecore.Mapper;

using Sitecore.Sandbox.Model;
using Sitecore.Sandbox.Utilities.StringUtilities.Base;
using Sitecore.Sandbox.Utilities.StringUtilities;
using Sitecore.Sandbox.Utilities.StringUtilities.DTO;

namespace Sitecore.Sandbox.Data
{
    public class ContentManagedMasterVariablesReplacer : MasterVariablesReplacer
    {
        private const string MasterVariablesDatabase = "master";

        public override string Replace(string text, Item targetItem)
        {
            return CreateNewTokenator().ReplaceTokens(base.Replace(text, targetItem));
        }

        public override void ReplaceField(Item item, Field field)
        {
            base.ReplaceField(item, field);
            field.Value = CreateNewTokenator().ReplaceTokens(field.Value);
        }

        private static ITokenator CreateNewTokenator()
        {
            return Utilities.StringUtilities.Tokenator.CreateNewTokenator(CreateTokenKeyValues());
        }

        private static IEnumerable<TokenKeyValue> CreateTokenKeyValues()
        {
            IEnumerable<Variable> variables = GetVariables();
            IList<TokenKeyValue> tokenKeyValues = new List<TokenKeyValue>();

            foreach (Variable variable in variables)
            {
                AddVariable(tokenKeyValues, variable);
            }
            
            return tokenKeyValues;
        }

        private static void AddVariable(IList<TokenKeyValue> tokenKeyValues, Variable variable)
        {
            Assert.ArgumentNotNull(variable, "variable");
            bool canAddVariable = !string.IsNullOrEmpty(variable.Token) && variable.TypeInstance != null;

            if (canAddVariable)
            {
                tokenKeyValues.Add(new TokenKeyValue(variable.Token, variable.TypeInstance));
            }
        }

        private static IEnumerable<Variable> GetVariables()
        {
            MasterVariables masterVariables = GetMasterVariables();

            if (masterVariables != null)
            {
                return masterVariables.Variables;
            }

            return new List<Variable>();
        }

        private static MasterVariables GetMasterVariables()
        {
            const string masterVariablesPath = "/sitecore/content/Settings/Master Variables"; // hardcoded here for illustration -- please don't hardcode paths!
            ISitecoreService sitecoreService = GetSitecoreService();
            return sitecoreService.GetItem<MasterVariables>(masterVariablesPath);
        }

        private static ISitecoreService GetSitecoreService()
        {
            return new SitecoreService(MasterVariablesDatabase);
        }
    }
}

At first, I tried to lazy instantiate my Tokenator instance in a property, but discovered that this class is only instantiated once — that would prevent newly added tokens from ever making their way into the ContentManagedMasterVariablesReplacer instance. This is why I call CreateNewTokenator() in each place where a Tokenator instance is needed.

I then wedged in my ContentManagedMasterVariablesReplacer class using a patch config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <setting name="MasterVariablesReplacer">
        <patch:attribute name="value">Sitecore.Sandbox.Data.ContentManagedMasterVariablesReplacer,Sitecore.Sandbox</patch:attribute>
      </setting>
    </settings>
  </sitecore>
</configuration>

Now, let’s see if this works.

We first need some variables:

culture-variable

ticks-variable

Next, we’ll need an item for testing. Let’s create a template with fields that will be populated using my Sitecore.Sandbox.Data.ContentManagedMasterVariablesReplacer class:

standard-values-test-page-template

Under /sitecore/content/Home, I created a new item based on this test template.

We can see that hardcoded tokens were populated from the base Sitecore.Data.MasterVariablesReplacer class:

test-item-hardcoded-tokens-populated

The content managed tokens were populated from the Sitecore.Sandbox.Data.ContentManagedMasterVariablesReplacer class:

test-item-content-managed-tokens-populated

That’s all there is to it.

Please keep in mind there could be potential performance issues with the above, especially when there are lots of Variable items — the code always creates an instance of the Tokenator in the ContentManagedMasterVariablesReplacer instance, which is pulling content from Sitecore each time, and we’re using reflection for each Variable. It would probably be best to enhance the above by leveraging Lucene in some way to increase its performance.

Further, these items should only be created/edited by advanced users or developers familiar with ASP.NET code — how else would one be able to populate the Type and Property Name fields in a Variable item?

Despite these, just imagine the big smiley your boss will send your way when you say new Standard Values variables no longer require any code changes coupled with a deployment. I hope that smiley puts a big smile on your face! 🙂