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:
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:
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:
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:
The content managed tokens were populated from the Sitecore.Sandbox.Data.ContentManagedMasterVariablesReplacer class:
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! 🙂
[…] Content Manage Custom Standard Values Tokens in the Sitecore Client […]
[…] But would it not be nice if you could do your own custom tokens? I found some very nice posts about that subject: John West – ADD CUSTOM STANDARD VALUES TOKENS IN THE SITECORE ASP.NET CMS Mike Reynolds – Content Manage Custom Standard Values Tokens in the Sitecore Client […]