Field value interpolation in Sitecore
In a previous blog post we talked about branch templates and how they make a content author's life a little bit easier by reducing the creation process of complex tree structures to a mere click. But, unbeknownst to most, the post also introduced us to a new concept: tokens.
So, what is a token?
Remember that special item $name underneath the branch template?
Well that is a token and it serves as a placeholder for the final name of the item. This gives the content author the opportunity to choose a name that makes sense for the item instead of getting a hardcoded one.
To put it simply, tokens are Sitecore's answer to the question how can I
populate a field with a predefined value without hardcoding it in the standard
values of the template?. The technical geeky term for this is interpolation
, but I won't
bore you to death with the technicalities (but in case you are interested here
is a more detailed explanation).
Out of the box Sitecore provides support for a finite number of tokens. These
tokens do not require any configuration or setup, simply use one of the following
in any field and you should be good to go.
Token | Description |
---|---|
$name | Expands to the name of the new item entered by the content author. |
$id | Expands to the id of the new item. |
$parentid | Expands to the id of the parent of the new item. |
$parentname | Expands to the name of the parent of the new item. |
$date | Expands to the system date in yyyyMMdd format. |
$time | Expands to the system time in HHmmss format. |
$now | Expands to the system date and time in yyyyMMdd HHmmss format. |
By convention, tokens are case insensitive, so it doesn't matter if you use
$ParentName
or $PaRenTNaME
, Sitecore will know what to do with the token.
Wait... there is no token for the information I need, what now?
OK, so there are two ways we can do this:
- Overriding the
Sitecore.Data.MasterVariablesReplacer
and patching the settingMasterVariableReplacer
with the new class. - Adding a new processor to the
expandInitialFieldValue
pipeline.
Both are valid but, in my humble opinion, the latter it's more in line with the Sitecore way of doing things, whilst the former feels a little too hacky for my taste (replacing things have never been my style, I prefer composition when possible).
Having said that, I've seen many good and valid solutions online, some use the first approach, some prefer the second... but I wasn't really convinced by any of them, mainly because those using the first approach felt hacky and the others supported only one variable per processor. So I decided to come up with my own solution with openness and extensibility in mind.
The first step is creating the supporting classes and interfaces.
This set of classes and interfaces will provide the default structure and base
implementation of all variable replacers (a.k.a. interpolators).
First we start with the IVariableReplacer
interface.
/// <summary> | |
/// Replaces a variable with a specific value. | |
/// </summary> | |
public interface IVariableReplacer | |
{ | |
/// <summary> | |
/// Gets the supported variable. | |
/// </summary> | |
IEnumerable<string> Variables { get; } | |
/// <summary> | |
/// Replaces a value from text | |
/// </summary> | |
/// <param name="text">The text to be replaced.</param> | |
/// <param name="targetItem">The target item being updated.</param> | |
/// <returns>A string that represents the replaced text.</returns> | |
string Replace(string text, Item targetItem); | |
} |
The interface defines two members:
- a
Variables
property that defines the name of the variable and aliases to be replaced (e.g.$path
,$contentPath
, etc). - a
Replace(string, Item)
method that does the interpolation.
Next is the base class for all replacers.
/// <summary> | |
/// Base class for a variable replacer. | |
/// </summary> | |
public abstract class VariableReplacerBase : IVariableReplacer | |
{ | |
/// <inheritdoc /> | |
public IEnumerable<string> Variables { get; } = new List<string>(); | |
/// <summary> | |
/// Class constructor. | |
/// </summary> | |
/// <param name="variables">The variable and aliases to be replaced.</param> | |
protected VariableReplacerBase(IEnumerable<string> variables) | |
{ | |
Assert.ArgumentNotNull(variables, nameof(variables)); | |
Assert.IsTrue(variables.Any(), "At least one variable is required"); | |
Variables = variables.Select(variable => StringUtil.EnsurePrefix('$', variable)); | |
} | |
/// <inheritdoc /> | |
public abstract string Replace(string text, Item targetItem); | |
} |
The code speaks for itself, the only thing worth noting is the constructor that
sets the Variables
property with the variables coming from the inheriting class.
The second step is creating the processor.
This is easily achieved by extending the
Sitecore.Pipelines.ExpandInitialFieldValue.ExpandInitialFieldValueProcessor
class and overriding the void Process(ExpandInitialFieldValueArgs)
method.
/// <summary> | |
/// Custom variable replacer. | |
/// </summary> | |
public class ReplaceCustomVariables : ExpandInitialFieldValueProcessor | |
{ | |
/// <summary> | |
/// Gets the variables to be replaced. | |
/// </summary> | |
public List<IVariableReplacer> Replacers { get; } = new List<IVariableReplacer>(); | |
/// <inheritdoc /> | |
public override void Process(ExpandInitialFieldValueArgs args) | |
{ | |
Assert.ArgumentNotNull(args, nameof(args)); | |
foreach (var replacer in Replacers) | |
{ | |
if (replacer.Variables.Any(variable => args.SourceField.Value.Contains(variable))) | |
{ | |
args.Result = replacer.Replace(args.Result, args.TargetItem); | |
} | |
} | |
} | |
/// <summary> | |
/// Adds a new variable replacer. | |
/// </summary> | |
/// <param name="node">The xml node to be parsed.</param> | |
public void AddVariableReplacer(XmlNode node) | |
{ | |
Assert.ArgumentNotNull(node, nameof(node)); | |
var replacer = Factory.CreateObject(node, true) as IVariableReplacer; | |
Assert.IsNotNull(replacer, "The replacer couldn't be created"); | |
Replacers.Add(replacer); | |
} | |
} |
This class has a property Replacers
and a public method AddVariableReplacer
that will be used in the next steps to create and store IVariableReplacer
objects.
The third step is creating specific variable replacers.
For simplicity, I'm just adding a variable replacer that will expand the
variable $path
to the path of the item created by the content author.
/// <summary> | |
/// Replaces $path with the item path. | |
/// </summary> | |
public class PathVariableReplacer : VariableReplacerBase | |
{ | |
/// <summary> | |
/// Class constructor. | |
/// </summary> | |
public PathVariableReplacer() : base(new string[] { "path" }) { } | |
/// <inheritdoc /> | |
public override string Replace(string text, Item targetItem) | |
{ | |
Assert.ArgumentNotNull(text, nameof(text)); | |
Assert.ArgumentNotNull(targetItem, nameof(targetItem)); | |
foreach (var variable in Variables) | |
{ | |
text = text.Replace(variable, targetItem.Paths.FullPath); | |
} | |
return text; | |
} | |
} |
But honestly the options are endless, anything you can access via the Item
class
is available to the variable replacer object... so go nuts...
The last step is tying it all together via a config patch file.
We are targeting the expandInitialFieldValue
pipeline and because we want to be good
citizens, we add our replacer after Sitecore's own replace variable processor, that
way we can guarantee Sitecore's out of the box variables will be expanded before ours.
Add as many replacers as needed to the replacers
node. This maps to the Replacers
property discussed above.
<configuration xmlns:patch="https://www.sitecore.com/xmlconfig/"> | |
<sitecore> | |
<pipelines> | |
<expandInitialFieldValue> | |
<processor type="MyAssembly.Namespace.Pipelines.ReplaceCustomVariables, MyAssembly.Namespace" | |
patch:after="processor[@type='type=Sitecore.Pipelines.ExpandInitialFieldValue.ReplaceVariables, Sitecore.Kernel']"> | |
<replacers hint="raw:AddVariableReplacer"> | |
<replacer type="MyAssembly.Namespace.VariableReplacers.PathVariableReplacer, MyAssembly.Namespace" /> | |
</replacers> | |
</processor> | |
</expandInitialFieldValue> | |
</pipelines> | |
</sitecore> | |
</configuration> |
Show me an example, maybe?
After a quick build and release, the variable will be available to use in the
content editor, so go ahead and create a new template and use the $path
variable
in any field of the template's __Standard Values
as shown below.
Then go to any of your pages and add a new item with the newly created template. You should be able to see the end result in the editing panel.
And that's a wrap people... I hope this helps you and your team!
Happy Coding!