To answer your question: branch templates are the way to go, here. However, branch templates, alone will not give you all you need. I have actually written a rule to provide this "branch preset" functionality, and it is currently in use on a Production site.
Branch Presets
The term Branch presets refers to creating a branch template with the purpose of storing a reusable presentation configuration, complete with datasources that may or may not live within the branch itself.
What Sitecore Does and Does not Provide
Sitecore only provides support for using branch templates to create the item hierarchy. This means that you can use Sitecore OOTB tools alone to create your datasource items. However, Sitecore does not "relink" your datasources for you. What does this mean? Consider the following example:
Branches
|--News # Has 'News Detail' rendering
|--Components
|--News Details # Datasource of 'News Detail' rendering
In this example, if we have only OOTB functionality, when we add an instance of this branch template, we will get the following result:
Content
|--News # 'News Detail' rendering datasource: branches/news/components/news details
|--Components
|--News Details # NOT datasource of 'News Detail' rendering
Notice that the problem here is that the News Detail
rendering is still pointing at the News Details
component in the Branch definition, not the instance of the branch. As such, we need to write some logic of our own to relink any datasources that live within the branch, itself.
Relinking Datasources
As before, in order for our branch presets to work, we need to write some logic to "relink" the datasources for any renderings on items in the branch that point to other items in the same branch. These datasources should be relinked to point at the items in the branch instance that correspond to the datasource items in the branch definition.
Triggering our Custom Logic
Triggering our custom logic is not as straightforward as you might think. Unfortunately, the item:added
event has technically been deprecated and the item:created
event is raised before create item's "master" (the reference to its branch template) has been set. We also don't want to pollute our item:saved
and item:saving
events, since these can have a big impact on performance. Fortunately, however, Sitecore's new item provider pipelines give us an excellent mechanism by which to trigger our logic: the <addFromTemplate>
pipeline.
The <addFromTemplate>
pipeline differs from the <uiAddFromTemplate>
pipeline in that the <addFromTemplate>
pipeline is called whenever an item is created, regardless of whether or not is created in the UI or where from in the UI.
Note that the <addFromTemplate>
pipeline only runs once when adding an item from a branch template rather than running once for each item in the branch. As such, we will want to recurse over the sub-items of our "item" (branch instance root) being added and relink the datasources for each.
Executing our Logic
So we now know where, what and why, but we now need to figure out "how". Anyone who knows me at all will know what I'm about to suggest: the Rules Engine!
The Rules Engine is an awesome mechanism that provides not only a highly extensible and flexible solution, but also one that is maintainable and low-effort to implement and add to. Sales-pitch, done - here is what you want to do:
You are going to want to first write some conditions to check to see if the item was created from a branch template. Be sure not to add conditions to only call your logic in the content
section of the tree. If you do, you will not be able to support branch templates that include instances of other branch templates.
The next thing that you will want to do is write an action to relink the branch datasources for item and its descendants. You can use string comparison on the relative paths from the datasource instance root to the relative paths from the datasource definition root to figure out which item linked to which. The only down-side is that this doesn't support items with duplicate names (which you really should try to avoid, anyway).
Enough Chat! Let's Code!
Full disclosure, all of the code, below, will work, but this is a pretty complex feature and I can't put the entire CSPROJ into this solution. As such, I am highlighting the important parts and providing you a link to a working repo that includes this rule. The repo also includes a TDS project that has all of the items necessary to run the solution. I most recently testing this in an 8.1u2 solution.
Configuration: Calling the Trigger
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<group name="itemProvider" groupName="itemProvider">
<pipelines>
<addFromTemplate>
<processor mode="on" type="Delphic.Platform.Sc.Rules.Processors.AddFromTemplate.AddFromTemplateRulesProcessor, Delphic.Platform.Sc.Rules">
<ruleFolderId>92F757EA-FB27-466C-937A-D44755BBBE7D</ruleFolderId>
</processor>
</addFromTemplate>
</pipelines>
</group>
</pipelines>
</sitecore>
</configuration>
Trigger: AddFromTemplate
Processor
using System;
using Delphic.Platform.Sc.Rules.Contexts;
using Sitecore;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.ItemProvider.AddFromTemplate;
using Sitecore.Web.UI.Sheer;
namespace Delphic.Platform.Sc.Rules.Processors.AddFromTemplate
{
public class AddFromTemplateRulesProcessor : AddFromTemplateProcessor
{
#region Public Properties
/// <summary>
/// Gets or sets the rule folder id.
/// </summary>
[CanBeNull]
[UsedImplicitly]
public string RuleFolderId { get; set; }
#endregion
#region Public Methods and Operators
/// <summary>
/// Processes the specified upload arguments.
/// </summary>
/// <param name="args">
/// The arguments.
/// </param>
public override void Process([NotNull] AddFromTemplateArgs args)
{
// this is managed in configuration (runIfAborted=true would have to be set to override the value)
if (args.Aborted)
{
return;
}
Assert.IsNotNull(args.FallbackProvider, "FallbackProvider is null");
try
{
var item = args.FallbackProvider.AddFromTemplate(args.ItemName, args.TemplateId, args.Destination, args.NewId);
if (item == null)
{
return;
}
args.ProcessorItem = args.Result = item;
}
catch (Exception ex)
{
Log.Error("AddFromTemplateRulesProcessor failed. Removing partially created item if it exists.", ex, this);
var item = args.Destination.Database.GetItem(args.NewId);
item?.Delete();
throw;
}
ID id;
if (string.IsNullOrWhiteSpace(RuleFolderId)
|| !Settings.Rules.ItemEventHandlers.RulesSupported(args.Destination.Database)
|| !ID.TryParse(RuleFolderId, out id))
{
return;
}
var ruleContext = new PipelineArgsRuleContext<AddFromTemplateArgs>(args);
RuleManager.RunRules(ruleContext, id);
}
#endregion
}
}
Note that I route all of my rules through a RuleManager
class to centralize logging/error-handling logic. (Credit to Matt Gramolini and Juozas "Jimbo" Baltika for their contributions to this structure and the code).
Action 1: Relink Datasources for Item:
using System.Runtime.InteropServices;
using Delphic.Platform.Sc.Rules.Actions.Base;
using Delphic.Platform.Sc.Rules.Utils;
using Sitecore;
using Sitecore.Rules;
namespace Delphic.Platform.Sc.Rules.Actions.BranchDatasources
{
[UsedImplicitly]
[Guid("4B439446-C6FD-4635-8282-147CA80A4CA5")]
public sealed class RelinkItemBranchDatasources<TRuleContext> : RuleAction<TRuleContext>
where TRuleContext : RuleContext
{
/// <summary>
/// The apply rule.
/// </summary>
/// <param name="ruleContext">
/// The rule context.
/// </param>
protected override void ApplyRule(TRuleContext ruleContext)
{
BranchDatasourceUtils.RelinkDatasourcesInBranchInstance(ruleContext.Item);
}
}
}
Action 2: Relink Datasources for Descendants:
using System.Runtime.InteropServices;
using Delphic.Platform.Sc.Rules.Actions.Base;
using Delphic.Platform.Sc.Rules.Utils;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Rules;
namespace Delphic.Platform.Sc.Rules.Actions.BranchDatasources
{
[UsedImplicitly]
[Guid("8E804F7A-77FF-4074-9BAC-7EEE0CFA89C6")]
public sealed class RelinkDescendantsAndSelfBranchDatasources<TRuleContext> : RuleAction<TRuleContext>
where TRuleContext : RuleContext
{
/// <summary>
/// The apply rule.
/// </summary>
/// <param name="ruleContext">
/// The rule context.
/// </param>
protected override void ApplyRule(TRuleContext ruleContext)
{
BranchDatasourceUtils.RelinkDatasourcesInBranchInstance(ruleContext.Item, true);
}
}
}
BranchDatasourceUtils:
using System;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Layouts;
using Sitecore.StringExtensions;
namespace Delphic.Platform.Sc.Rules.Utils
{
public class BranchDatasourceUtils
{
/// <summary>
/// Utility method for relinking branch datasources
/// </summary>
public static void RelinkDatasourcesInBranchInstance(Item item, bool descendants = false)
{
RelinkDatasourcesForItemInBranchInstance(item, item);
if (!descendants)
{
return;
}
foreach (var descendant in item.Axes.GetDescendants())
{
RelinkDatasourcesForItemInBranchInstance(descendant, item);
}
}
/// <summary>
/// Utility method for relinking datasources for an item within a branch instance
/// </summary>
/// <remarks>
/// Adapted from original code, written by Kam Figy:
/// https://github.com/kamsar/BranchPresets/blob/master/BranchPresets/AddFromBranchPreset.cs
/// </remarks>
public static void RelinkDatasourcesForItemInBranchInstance(Item item, Item instanceRoot)
{
Action<RenderingDefinition> relinkRenderingDatasource =
rendering =>
RelinkRenderingDatasourceForItemInBranch(item, instanceRoot, rendering);
LayoutUtils.ApplyActionToAllRenderings(item, relinkRenderingDatasource);
}
/// <summary>
/// Utility method for relinking the datasource for the supplied rendering on an item in the branch instance
/// </summary>
/// <remarks>
/// Adapted from original code, written by Kam Figy:
/// https://github.com/kamsar/BranchPresets/blob/master/BranchPresets/AddFromBranchPreset.cs
/// </remarks>
/// <param name="item">Item that contains the rendering</param>
/// <param name="instanceRoot">Root item of the branch instance</param>
/// <param name="rendering">Rendering to be relinked</param>
public static void RelinkRenderingDatasourceForItemInBranch(Item item, Item instanceRoot, RenderingDefinition rendering)
{
var branchBasePath = item.Branch.InnerItem.Paths.FullPath;
if (string.IsNullOrWhiteSpace(rendering.Datasource))
{
return;
}
var database = item.Database;
// note: queries and multiple item datasources are not supported
var renderingTargetItem = database.GetItem(rendering.Datasource);
Assert.IsNotNull(
renderingTargetItem,
"Error while expanding branch template rendering datasources: data source {0} was not resolvable."
.FormatWith(rendering.Datasource));
// if there was no valid target item OR the target item is not a child of the branch template we skip out
if (renderingTargetItem == null ||
!renderingTargetItem.Paths.FullPath.StartsWith(branchBasePath, StringComparison.OrdinalIgnoreCase))
{
return;
}
// get the path relative to the branch item
var relativeRenderingPath =
renderingTargetItem.Paths.FullPath.Substring(branchBasePath.Length);
// replace $name Sitecore tokens in path
relativeRenderingPath = relativeRenderingPath
.Replace("$name", instanceRoot.Name);
var newTargetPath = instanceRoot.Parent.Paths.FullPath + relativeRenderingPath;
var newTargetItem = database.GetItem(newTargetPath);
// if the target item was a valid under branch item, but the same relative path does not exist under the branch instance
// we set the datasource to something invalid to avoid any potential unintentional edits of a shared data source item
rendering.Datasource = newTargetItem?.ID.ToString() ?? "INVALID_BRANCH_SUBITEM_ID";
}
}
}
LayoutUtils:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Layouts;
using Sitecore.SecurityModel;
namespace Delphic.Platform.Sc.Rules.Utils
{
/// <summary>
/// Layout Utilities class
/// </summary>
/// <remarks>
/// Adapted from original code, written by Kam Figy:
/// https://github.com/kamsar/BranchPresets/blob/master/BranchPresets/AddFromBranchPreset.cs
/// </remarks>
public static class LayoutUtils
{
/// <summary>
/// Invokes Action on all Renderings on item
/// </summary>
public static void ApplyActionToAllRenderings(Item item, Action<RenderingDefinition> action)
{
ApplyActionToAllSharedRenderings(item, action);
ApplyActionToAllFinalRenderings(item, action);
}
/// <summary>
/// Invokes Action on all Shared Renderings on item
/// </summary>
public static void ApplyActionToAllSharedRenderings(Item item, Action<RenderingDefinition> action)
{
ApplyActionToLayoutField(item, FieldIDs.LayoutField, action);
}
/// <summary>
/// Invokes Action on all Final Renderings on item
/// </summary>
public static void ApplyActionToAllFinalRenderings(Item item, Action<RenderingDefinition> action)
{
ApplyActionToLayoutField(item, FieldIDs.FinalLayoutField, action);
}
/// <summary>
/// Invokes Action on all Final Renderings on item
/// </summary>
private static void ApplyActionToLayoutField(Item item, ID fieldId, Action<RenderingDefinition> action)
{
var currentLayoutXml = LayoutField.GetFieldValue(item.Fields[fieldId]);
if (string.IsNullOrEmpty(currentLayoutXml))
{
return;
}
var newXml = ApplyActionToLayoutXml(currentLayoutXml, action);
if (newXml == null)
{
return;
}
using (new SecurityDisabler())
{
using (new EditContext(item))
{
// NOTE: when dealing with layouts its important to get and set the field value with LayoutField.Get/SetFieldValue()
// if you fail to do this you will not process layout deltas correctly and may instead override all fields (breaking full inheritance),
// or attempt to get the layout definition for a delta value, which will result in your wiping the layout details when they get saved.
LayoutField.SetFieldValue(item.Fields[fieldId], newXml);
}
}
}
private static string ApplyActionToLayoutXml(string xml, Action<RenderingDefinition> action)
{
var layout = LayoutDefinition.Parse(xml);
// normalize the output in case of any minor XML differences (spaces, etc)
xml = layout.ToXml();
// loop over devices in the rendering
for (var deviceIndex = layout.Devices.Count - 1; deviceIndex >= 0; deviceIndex--)
{
var device = layout.Devices[deviceIndex] as DeviceDefinition;
if (device == null)
{
continue;
}
// loop over renderings within the device
for (var renderingIndex = device.Renderings.Count - 1; renderingIndex >= 0; renderingIndex--)
{
var rendering = device.Renderings[renderingIndex] as RenderingDefinition;
if (rendering == null)
{
continue;
}
// run the action on the rendering
action(rendering);
}
}
var layoutXml = layout.ToXml();
// save a modified layout value if necessary
return layoutXml != xml ? layoutXml : null;
}
}
}