Home » 2013 » February (Page 2)

Monthly Archives: February 2013

Advertisements

Dude, Where’s My Processor? Filling the Void in the SaveRichTextContent and LoadRichTextContent Sitecore Pipelines

Some of you might be aware that I frequently go through the Web.config of my local instance of Sitecore looking for opportunities to extend or customize class files referenced within it — I may have mentioned this in a previous post, and no doubt have told some Sitecore developers/enthusiasts in person I do this at least once per day. I must confess: I usually do this multiple times a day.

Last night, I was driven to explore something I have noticed in the Web.config of my v6.5 instance — my attention has been usurped many times by the saveRichTextContent and loadRichTextContent pipeline nodes being empty.

These two pipelines allow you to make changes to content within Rich Text fields before any save actions on your item in the Sitecore client.

I remembered that one of them did have a pipeline processor defined within it at one point. It was time to do some research.

After conducting some research — truth be told, I only googled a couple of times — I stumbled upon some release notes on SDN discussing the saveRichTextContent Web.config pipeline, and that this pipeline did contain a processor in it at one point — the Sitecore.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent.EmbedInParagraph processor — although I don’t remember what this processor did, and don’t have an older version of Sitecore.Client.dll to investigate. I could download an older version of Sitecore from SDN, but decided to leave that exercise for another snowy weekend.

I decided to explore whether the option to add custom processors to these pipelines still existed. I came up with an idea straight out of the 1990’s — having marquee tags animate content across my pages.

As an aside, back in the 1990’s, almost every webpage — all webpages were called homepages then — had at least one marquee. Most had multiple — it was the cool thing to do back then, asymptotic only to having an ‘Under Construction’ image on your homepage. Employing this practice today would be considered anathema.

I decided to reuse my concept of manipulator from my Manipulate Field Values in a Custom Sitecore Web Forms for Marketers DataProvider article, and created a new manipulator to wrap specified tags in marquee tags:

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

namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
    public interface IWrapHtmlTagsInTagManipulator : IManipulator<string>
    {
    }
}

I thought it would be a good idea to define a DTO for my manipulator to pass objects to it in a clean manner:

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

namespace Sitecore.Sandbox.Utilities.Manipulators.DTO
{
    public class WrapHtmlTagsInTagManipulatorSettings
    {
        public string WrapperTag { get; set; }
        public IEnumerable<string> TagsToWrap { get; set; }
    }
}

Next, I built my manipulator:

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

using Sitecore.Diagnostics;

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

using HtmlAgilityPack;

namespace Sitecore.Sandbox.Utilities.Manipulators
{
    public class WrapHtmlTagsInTagManipulator : IWrapHtmlTagsInTagManipulator
    {
        private WrapHtmlTagsInTagManipulatorSettings Settings { get; set; }

        private WrapHtmlTagsInTagManipulator(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            SetSettings(settings);
        }

        private void SetSettings(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            AssertSettings(settings);
            Settings = settings;
        }

        private static void AssertSettings(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            Assert.ArgumentNotNull(settings, "settings");
            Assert.ArgumentNotNullOrEmpty(settings.WrapperTag, "settings.WrapperTag");
            Assert.ArgumentNotNull(settings.TagsToWrap, "settings.TagsToWrap");
        }

        public string Manipulate(string html)
        {
            Assert.ArgumentNotNullOrEmpty(html, "html");
            HtmlNode documentNode = GetHtmlDocumentNode(html);

            foreach (string tagToWrap in Settings.TagsToWrap)
            {
                WrapTags(documentNode, tagToWrap);
            }

            return documentNode.InnerHtml;
        }

        private void WrapTags(HtmlNode documentNode, string tagToWrap)
        {
            HtmlNodeCollection htmlNodes = documentNode.SelectNodes(CreateNewDescendantsSelector(tagToWrap));

            foreach(HtmlNode htmlNode in htmlNodes)
            {
                WrapHtmlNodeIfApplicable(documentNode, htmlNode);
            }
        }

        private void WrapHtmlNodeIfApplicable(HtmlNode documentNode, HtmlNode htmlNode)
        {
            if (!AreEqualIgnoreCase(htmlNode.ParentNode.Name, Settings.WrapperTag))
            {
                WrapHtmlNode(documentNode, htmlNode, Settings.WrapperTag);
            }
        }

        private static void WrapHtmlNode(HtmlNode documentNode, HtmlNode htmlNode, string wrapperTag)
        {
            HtmlNode wrapperHtmlNode = documentNode.OwnerDocument.CreateElement(wrapperTag);
            AddNewParent(wrapperHtmlNode, htmlNode);
        }

        private static void AddNewParent(HtmlNode newParentHtmlNode, HtmlNode htmlNode)
        {
            Assert.ArgumentNotNull(newParentHtmlNode, "newParentHtmlNode");
            Assert.ArgumentNotNull(htmlNode, "htmlNode");
            htmlNode.ParentNode.ReplaceChild(newParentHtmlNode, htmlNode);
            newParentHtmlNode.AppendChild(htmlNode);
        }

        private static bool AreEqualIgnoreCase(string stringOne, string stringTwo)
        {
            return string.Equals(stringOne, stringTwo, StringComparison.InvariantCultureIgnoreCase);
        }

        private static string CreateNewDescendantsSelector(string tag)
        {
            Assert.ArgumentNotNullOrEmpty(tag, "tag");
            return string.Format("//{0}", tag);
        }

        private HtmlNode GetHtmlDocumentNode(string html)
        {
            HtmlDocument htmlDocument = CreateNewHtmlDocument(html);
            return htmlDocument.DocumentNode;
        }

        private HtmlDocument CreateNewHtmlDocument(string html)
        {
            HtmlDocument htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(html);
            return htmlDocument;
        }

        public static IWrapHtmlTagsInTagManipulator CreateNewWrapHtmlTagsInTagManipulator(WrapHtmlTagsInTagManipulatorSettings settings)
        {
            return new WrapHtmlTagsInTagManipulator(settings);
        }
    }
}

My manipulator class above uses Html Agility Pack to find targeted html elements, and wrap them in newly created marquee tags — which are also created via Html Agility Pack.

I decided to create a base class to contain core logic that will be used across both of my pipeline processors:

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

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

namespace Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.Base
{
    public abstract class AddSomeMarqueesBase
    {
        private IWrapHtmlTagsInTagManipulator _HtmlManipulator;
        private IWrapHtmlTagsInTagManipulator HtmlManipulator
        {
            get
            {
                if(_HtmlManipulator == null)
                {
                    _HtmlManipulator = CreateNewWrapHtmlTagsInTagManipulator();
                }

                return _HtmlManipulator;
            }
        }

        private IWrapHtmlTagsInTagManipulator CreateNewWrapHtmlTagsInTagManipulator()
        {
            return WrapHtmlTagsInTagManipulator.CreateNewWrapHtmlTagsInTagManipulator(CreateNewWrapHtmlTagsInTagManipulatorSettings());
        }

        protected virtual WrapHtmlTagsInTagManipulatorSettings CreateNewWrapHtmlTagsInTagManipulatorSettings()
        {
            return new WrapHtmlTagsInTagManipulatorSettings
            {
                WrapperTag = "marquee",
                TagsToWrap = new string[] { "em", "img" }
            };
        }

        protected virtual string ManipulateHtml(string html)
        {
            if (!string.IsNullOrEmpty(html))
            {
                return HtmlManipulator.Manipulate(html);
            }

            return html;
        }
    }
}

This base class creates an instance of our manipulator class above, passing in the required DTO housing the wrapper tag and tags to wrap settings.

Honestly, while writing this article and looking at this code, I am not completely happy about how I implemented this base class. I should have added a constructor which takes in the manipulator instance — thus allowing subclasses to provide their own manipulators, especially if these subclasses need to use a different manipulator than the one used by default in the base class.

Further, it probably would have been prudent to put the html tags I defined in my DTO instance into a patch config file.

Next, I defined my loadRichTextContent pipeline processor:

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

using Sitecore.Shell.Controls.RichTextEditor.Pipelines.LoadRichTextContent;

using Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.Base;

namespace Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.LoadRichTextContent
{
    public class AddSomeMarquees : AddSomeMarqueesBase
    {
        public void Process(LoadRichTextContentArgs args)
        {
            args.Content = ManipulateHtml(args.Content);
        }
    }
}

Followed by my saveRichTextContentpipeline processor:

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

using Sitecore.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent;

using Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.Base;

namespace Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent
{
    public class AddSomeMarquees : AddSomeMarqueesBase
    {
        public void Process(SaveRichTextContentArgs args)
        {
            args.Content = ManipulateHtml(args.Content);
        }
    }
}

Thereafter, I glued everything together via a patch config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <loadRichTextContent>
        <processor type="Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.LoadRichTextContent.AddSomeMarquees, Sitecore.Sandbox"/>
      </loadRichTextContent>
      <saveRichTextContent>
        <processor type="Sitecore.Sandbox.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent.AddSomeMarquees, Sitecore.Sandbox"/>
      </saveRichTextContent>
    </pipelines>
  </sitecore>
</configuration>

Time to see the fruits of my labor above.

I’ve added some content in a Rich Text field:

RTF-Design-Before-Marquees

Here’s the html in the Rich Text field:

RTF-Html-Before-Marquees

I clicked the ‘Accept’ button in the Rich Text dialog window, and then saw the targeted content come to life:

RTF-With-Marquees

I launched the dialog window again to investigate what the html now looks like:

RTF-Html-With-Marquees

Mission accomplished — we now have marquees! 🙂

I do want to point out I could not get my loadRichTextContent pipeline processor to run. I thought it would run when opening the Rich Text dialog, although I was wrong — it did not. I also tried to get it to run via the ‘Edit Html’ button, but to no avail.

If I am looking in the wrong place, or this is a known issue in Sitecore, please drop a comment and let me know.

Advertisements

Shoehorn Sitecore Web Forms for Marketers Field Values During a Custom Save Action

In a previous article, I discussed how one could remove Web Forms for Marketers (WFFM) field values before saving form data into the WFFM database — which ultimately make their way into the Form Reports for your form.

When I penned that article, my intentions were not to write an another highlighting the polar opposite action of inserting field values — I figured one could easily ascertain how to do this from that article.

However, last night I started to feel some guilt for not sharing how one could do this — there is one step in this process that isn’t completely intuitive — so I worked late into the night developing a solution of how one would go about doing this, and this article is the fruit of that effort.

Besides, how often does one get the opportunity to use the word shoehorn? If you aren’t familiar with what a shoehorn is, check out this article. I am using the word as an action verb in this post — meaning to insert values for fields in the WFFM database. 🙂

I first defined a utility class and its interfaces for shoehorning field values into a AdaptedResultList collection — a collection of WFFM fields:

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

namespace Sitecore.Sandbox.Utilities.Shoehorns.Base
{
    public interface IShoehorn<T, U>
    {
        U Shoehorn(T source);
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Form.Core.Client.Data.Submit;

using Sitecore.Sandbox.Utilities.Shoehorns.Base;

namespace Sitecore.Sandbox.Utilities.Shoehorns.Base
{
    public interface IWFFMFieldValuesShoehorn : IShoehorn<AdaptedResultList, AdaptedResultList>
    {
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Diagnostics;
using Sitecore.Form.Core.Client.Data.Submit;
using Sitecore.Form.Core.Controls.Data;

using Sitecore.Sandbox.Utilities.Shoehorns.Base;

namespace Sitecore.Sandbox.Utilities.Shoehorns
{
    public class WFFMFieldValuesShoehorn : IWFFMFieldValuesShoehorn
    {
        private IDictionary<string, string> _FieldsForShoehorning;
        private IDictionary<string, string> FieldsForShoehorning 
        { 
            get
            {
                if (_FieldsForShoehorning == null)
                {
                    _FieldsForShoehorning = new Dictionary<string, string>();
                }

                return _FieldsForShoehorning;
            }
        }

        private WFFMFieldValuesShoehorn(IEnumerable<KeyValuePair<string, string>> fieldsForShoehorning)
        {
            SetFieldsForShoehorning(fieldsForShoehorning);
        }

        private void SetFieldsForShoehorning(IEnumerable<KeyValuePair<string, string>> fieldsForShoehorning)
        {
            AssertFieldsForShoehorning(fieldsForShoehorning);
            
            foreach (var keyValuePair in fieldsForShoehorning)
            {
                AddToDictionaryIfPossible(keyValuePair);
            }
        }

        private void AddToDictionaryIfPossible(KeyValuePair<string, string> keyValuePair)
        {
            if (!FieldsForShoehorning.ContainsKey(keyValuePair.Key))
            {
                FieldsForShoehorning.Add(keyValuePair.Key, keyValuePair.Value);
            }
        }

        private static void AssertFieldsForShoehorning(IEnumerable<KeyValuePair<string, string>> fieldsForShoehorning)
        {
            Assert.ArgumentNotNull(fieldsForShoehorning, "fieldsForShoehorning");

            foreach (var keyValuePair in fieldsForShoehorning)
            {
                Assert.ArgumentNotNullOrEmpty(keyValuePair.Key, "keyValuePair.Key");
            }
        }

        public AdaptedResultList Shoehorn(AdaptedResultList fields)
        {
            if (!CanProcessFields(fields))
            {
                return fields;
            }

            List<AdaptedControlResult> adaptedControlResults = new List<AdaptedControlResult>();

            foreach(AdaptedControlResult field in fields)
            {
                adaptedControlResults.Add(GetShoehornedField(field));
            }

            return adaptedControlResults;
        }

        private static bool CanProcessFields(IEnumerable<ControlResult> fields)
        {
            return fields != null && fields.Any();
        }

        private AdaptedControlResult GetShoehornedField(AdaptedControlResult field)
        {
            string value = string.Empty;

            if (FieldsForShoehorning.TryGetValue(field.FieldName, out value))
            {
                return GetShoehornedField(field, value);
            }

            return field;
        }

        private static AdaptedControlResult GetShoehornedField(ControlResult field, string value)
        {
            return new AdaptedControlResult(CreateNewControlResultWithValue(field, value), true);
        }

        private static ControlResult CreateNewControlResultWithValue(ControlResult field, string value)
        {
            ControlResult controlResult = new ControlResult(field.FieldName, value, field.Parameters);
            controlResult.FieldID = field.FieldID;
            return controlResult;
        }

        public static IWFFMFieldValuesShoehorn CreateNewWFFMFieldValuesShoehorn(IEnumerable<KeyValuePair<string, string>> fieldsForShoehorning)
        {
            return new WFFMFieldValuesShoehorn(fieldsForShoehorning);
        }
    }
}

This class is similar to the utility class I’ve used in my article on ripping out field values, albeit we are inserting values that don’t necessarily have to be the empty string.

I then defined a new WFFM field type — an invisible field that serves as a placeholder in our Form Reports for a given form. This is the non-intuitive step I was referring to above:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;

using Sitecore.Diagnostics;

using Sitecore.Form.Web.UI.Controls;

namespace Sitecore.Sandbox.WFFM.Controls
{
    public class InvisibleField : SingleLineText
    {
        public InvisibleField()
        {
        }

        public InvisibleField(HtmlTextWriterTag tag) 
            : base(tag)
        {
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            HideControls(new Control[] { title, generalPanel });
        }

        private static void HideControls(IEnumerable<Control> controls)
        {
            Assert.ArgumentNotNull(controls, "controls");

            foreach (Control control in controls)
            {
                HideControl(control);
            }
        }

        private static void HideControl(Control control)
        {
            Assert.ArgumentNotNull(control, "control");
            control.Visible = false;
        }
    }
}

I had to register this new field in WFFM in the Sitecore Client under /sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types/Custom:

invisible-field

Next, I created a custom field action that processes WFFM fields — and shoehorns values into fields where appropriate.

In my example, I will be capturing users’ browser user agent strings and their ASP.NET session identifiers — I strongly recommend defining your field names in a patch config file, although I did not do such a step for this post:

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

using Sitecore.Data;
using Sitecore.Form.Core.Client.Data.Submit;
using Sitecore.Form.Submit;

using Sitecore.Sandbox.Utilities.Shoehorns;
using Sitecore.Sandbox.Utilities.Shoehorns.Base;

namespace Sitecore.Sandbox.WFFM.Actions
{
    public class ShoehornFieldValuesThenSaveToDatabase : SaveToDatabase
    {
        public override void Execute(ID formId, AdaptedResultList fields, object[] data)
        {
            base.Execute(formId, ShoehornFields(fields), data);
        }

        private AdaptedResultList ShoehornFields(AdaptedResultList fields)
        {
            IWFFMFieldValuesShoehorn shoehorn = WFFMFieldValuesShoehorn.CreateNewWFFMFieldValuesShoehorn(CreateNewFieldsForShoehorning());
            return shoehorn.Shoehorn(fields);
        }

        private IEnumerable<KeyValuePair<string, string>> CreateNewFieldsForShoehorning()
        {
            return new KeyValuePair<string, string>[] 
            { 
                new KeyValuePair<string, string>("User Agent", HttpContext.Current.Request.UserAgent),
                new KeyValuePair<string, string>("Session ID", HttpContext.Current.Session.SessionID)
            };
        }
    }
}

I then registered my new Save to Database action in Sitecore under /sitecore/system/Modules/Web Forms for Marketers/Settings/Actions/Save Actions:

shoehorn-save-to-db

Let’s build a form. I created another random form. This one contains some invisible fields:

another-random-form

Next, I mapped my custom Save to Database action to my new form:

another-random-form-add-save-to-db-action

I also created a new page item to hold my WFFM form, and navigated to that page to fill in my form:

another-random-form-filled

I clicked the submit button and got a pleasant confirmation message:

another-random-form-confirmation

Thereafter, I pulled up the Form Reports for this form, and see that field values were shoehorned into it:

another-random-form-reports

That’s all there is to it.

What is the utility in all of this? Well, you might want to use this approach when sending information off to a third-party via a web service where that third-party application returns some kind of transaction identifier. Having this identifier in your Form Reports could help in troubleshoot issues that had arisen during that transaction — third-party payment processors come to mind.

Kick-start Glass.Sitecore.Mapper in a Sitecore Initialize Pipeline

In my previous post, I used Glass.Sitecore.Mapper to grab content out of Sitecore for use in expanding tokens set in Standard Values fields.

While writing the code for that article, I recalled Alex Shyba asking whether it were possible to move Glass initialization code out of the Global.asax and into an initialize pipeline — Mike Edwards, the developer of Glass, illustrates how one initializes Glass in the Global.asax on github.

I do remember Mike saying it were possible, although I am uncertain whether anyone has done this.

As a follow up to Alex’s tweet, I’ve decided to do just that — create an initialize pipeline that will load up Glass models. I’ve also moved the model namespaces and assembly definitions into a patch config file along with defining the initialize pipeline.

Here is the pipeline I’ve written to do this:

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

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;

using Glass.Sitecore.Mapper.Configuration.Attributes;

namespace Sitecore.Sandbox.Pipelines.Loader
{
    public class InitializeGlassMapper
    {
        public void Process(PipelineArgs args)
        {
            CreateContextIfApplicable(GetModelTypes());
        }

        private static void CreateContextIfApplicable(IEnumerable<string> modelTypes)
        {
            if (CanCreateContext(modelTypes))
            {
                CreateContext(CreateNewAttributeConfigurationLoader(modelTypes));
            }
        }

        private static bool CanCreateContext(IEnumerable<string> modelTypes)
        {
            return modelTypes != null && modelTypes.Count() > 0;
        }

        private static AttributeConfigurationLoader CreateNewAttributeConfigurationLoader(IEnumerable<string> modelTypes)
        {
            Assert.ArgumentNotNull(modelTypes, "modelTypes");
            Assert.ArgumentCondition(modelTypes.Count() > 0, "modelTypes", "modelTypes collection must contain at least one string!");
            return new AttributeConfigurationLoader(modelTypes.ToArray());
        }

        private static void CreateContext(AttributeConfigurationLoader loader)
        {
            Assert.ArgumentNotNull(loader, "loader");
            Glass.Sitecore.Mapper.Context context = new Glass.Sitecore.Mapper.Context(loader);
        }

        private static IEnumerable<string> GetModelTypes()
        {
            return Factory.GetStringSet("glassMapperModels/type");
        }
    }
}

I’ve defined my new initialize pipeline in a patch config, coupled with Glass model namespace/assembly pairs:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="Sitecore.Sandbox.Pipelines.Loader.InitializeGlassMapper, Sitecore.Sandbox" />
      </initialize>
    </pipelines>
    <glassMapperModels>
      <type>Sitecore.Sandbox.Model, Sitecore.Sandbox</type>
    </glassMapperModels>
  </sitecore>
</configuration>

On the testing front, I validated what I developed for my previous post still works — it still works like a charm! 🙂

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! 🙂