Automagically Clone New Child Items to Clones of Parent Items In the Sitecore CMS
“Out of the box”, content authors are prompted via a content editor warning to take action on clones of a source item when a new subitem is added under the source item:
Content authors have a choice to either clone or not clone the new subitem, and such an action must be repeated on all clones of the source item — such might be a daunting task, especially when there are multiple clones for a given source item.
In a recent project, I had a requirement to automatically clone newly added subitems under clones of their parents, and remove any content editor warnings by programmatically accepting the Sitecore notifications driving these warnings.
Although I cannot show you that solution, I did wake up from a sound sleep early this morning with another solution to this problem — it was quite a dream — and this post captures that idea.
In a previous post, I built a utility object that gathers things, and decided to reuse this basic concept. Here, I define a contract for what constitutes a general gatherer:
using System.Collections.Generic; namespace Sitecore.Sandbox.Utilities.Gatherers.Base { public interface IGatherer<T, U> { T Source { get; set; } IEnumerable<U> Gather(); } }
In this post, we will be gathering clones — these are Sitecore items:
using Sitecore.Data.Items; namespace Sitecore.Sandbox.Utilities.Gatherers.Base { public interface IItemsGatherer : IGatherer<Item, Item> { } }
The following gatherer grabs all clones for a given item as they are cataloged in an instance of the LinkDatabase — in this solution we are using Globals.LinkDatabase as the default instance:
using System.Collections.Generic; using System.Linq; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Links; using Sitecore.Sandbox.Utilities.Gatherers.Base; namespace Sitecore.Sandbox.Utilities.Gatherers { public class ItemClonesGatherer : IItemsGatherer { private LinkDatabase LinkDatabase { get; set; } private Item _Source; public Item Source { get { return _Source; } set { Assert.ArgumentNotNull(value, "Source"); _Source = value; } } private ItemClonesGatherer() : this(GetDefaultLinkDatabase()) { } private ItemClonesGatherer(LinkDatabase linkDatabase) { SetLinkDatabase(linkDatabase); } private ItemClonesGatherer(LinkDatabase linkDatabase, Item source) { SetLinkDatabase(linkDatabase); SetSource(source); } private void SetLinkDatabase(LinkDatabase linkDatabase) { Assert.ArgumentNotNull(linkDatabase, "linkDatabase"); LinkDatabase = linkDatabase; } private void SetSource(Item source) { Source = source; } public IEnumerable<Item> Gather() { return (from itemLink in GetReferrers() where IsClonedItem(itemLink) select itemLink.GetSourceItem()).ToList(); } private IEnumerable<ItemLink> GetReferrers() { Assert.ArgumentNotNull(Source, "Source"); return LinkDatabase.GetReferrers(Source); } private static bool IsClonedItem(ItemLink itemLink) { return IsSourceField(itemLink) && !IsSourceItemNull(itemLink); } private static bool IsSourceField(ItemLink itemLink) { Assert.ArgumentNotNull(itemLink, "itemLink"); return itemLink.SourceFieldID == FieldIDs.Source; } private static bool IsSourceItemNull(ItemLink itemLink) { Assert.ArgumentNotNull(itemLink, "itemLink"); return itemLink.GetSourceItem() == null; } private static LinkDatabase GetDefaultLinkDatabase() { return Globals.LinkDatabase; } public static IItemsGatherer CreateNewItemClonesGatherer() { return new ItemClonesGatherer(); } public static IItemsGatherer CreateNewItemClonesGatherer(LinkDatabase linkDatabase) { return new ItemClonesGatherer(linkDatabase); } public static IItemsGatherer CreateNewItemClonesGatherer(LinkDatabase linkDatabase, Item source) { return new ItemClonesGatherer(linkDatabase, source); } } }
Next, we need a way to remove the content editor warning I alluded to above. The way to do this is to accept the notification that is triggered on the source item’s clones.
I decided to use the decorator pattern to accomplish this:
using Sitecore.Data.Clones; using Sitecore.Data.Items; using Sitecore.Diagnostics; namespace Sitecore.Sandbox.Data.Clones { public class AcceptAsIsNotification : Notification { private Notification InnerNotification { get; set; } private AcceptAsIsNotification(Notification innerNotification) { SetInnerNotification(innerNotification); SetProperties(); } private void SetInnerNotification(Notification innerNotification) { Assert.ArgumentNotNull(innerNotification, "innerNotification"); InnerNotification = innerNotification; } private void SetProperties() { ID = InnerNotification.ID; Processed = InnerNotification.Processed; Uri = InnerNotification.Uri; } public override void Accept(Item item) { base.Accept(item); } public override Notification Clone() { return InnerNotification.Clone(); } public static Notification CreateNewAcceptAsIsNotification(Notification innerNotification) { return new AcceptAsIsNotification(innerNotification); } } }
An instance of the above AcceptAsIsNotification class would wrap an instance of a notification, and accept the wrapped notification without any other action.
Notifications we care about in this solution are instances of Sitecore.Data.Clones.ChildCreatedNotification — in Sitecore.Kernel — for a given clone parent, and this notification clones a subitem during acceptance, a behavior we do not want to leverage since we have already cloned the subitem.
I then added an item:added event handler to use an instance of our gatherer defined above to get all clones of the newly added subitem’s parent; clone the subitem under these clones; and accept all instances of Sitecore.Data.Clones.ChildCreatedNotification on the parent item’s clones:
using System; using System.Collections.Generic; using System.Linq; using Sitecore.Data; using Sitecore.Data.Clones; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Events; using Sitecore.Sandbox.Data.Clones; using Sitecore.Sandbox.Utilities.Gatherers; using Sitecore.Sandbox.Utilities.Gatherers.Base; namespace Sitecore.Sandbox.Events.Handlers { public class AutomagicallyCloneChildItemEventHandler { private static readonly IItemsGatherer ClonesGatherer = ItemClonesGatherer.CreateNewItemClonesGatherer(); public void OnItemAdded(object sender, EventArgs args) { CloneItemIfApplicable(GetItem(args)); } private static void CloneItemIfApplicable(Item item) { if (item == null) { return; } ChildCreatedNotification childCreatedNotification = CreateNewChildCreatedNotification(); childCreatedNotification.ChildId = item.ID; IEnumerable<Item> clones = GetClones(item.Parent); foreach (Item clone in clones) { Item clonedChild = item.CloneTo(clone); RemoveChildCreatedNotifications(Context.ContentDatabase, clone); } } private static void RemoveChildCreatedNotifications(Database database, Item clone) { Assert.ArgumentNotNull(database, "database"); Assert.ArgumentNotNull(clone, "clone"); foreach (Notification notification in GetChildCreatedNotifications(database, clone)) { AcceptNotificationAsIs(notification, clone); } } private static void AcceptNotificationAsIs(Notification notification, Item item) { Assert.ArgumentNotNull(notification, "notification"); Assert.ArgumentNotNull(item, "item"); Notification acceptAsIsNotification = AcceptAsIsNotification.CreateNewAcceptAsIsNotification(notification); acceptAsIsNotification.Accept(item); } private static IEnumerable<Notification> GetChildCreatedNotifications(Database database, Item clone) { Assert.ArgumentNotNull(database, "database"); Assert.ArgumentNotNull(clone, "clone"); return GetNotifications(database, clone).Where(notification => notification.GetType() == typeof(ChildCreatedNotification)).ToList(); } private static IEnumerable<Notification> GetNotifications(Database database, Item clone) { Assert.ArgumentNotNull(database, "database"); Assert.ArgumentNotNull(clone, "clone"); return database.NotificationProvider.GetNotifications(clone); } private static IEnumerable<Item> GetClones(Item item) { ClonesGatherer.Source = item; return ClonesGatherer.Gather(); } private static Item GetItem(EventArgs args) { return Event.ExtractParameter(args, 0) as Item; } private static ChildCreatedNotification CreateNewChildCreatedNotification() { return new ChildCreatedNotification(); } } }
I then plugged in the above in a patch include configuration file:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <sitecore> <events> <event name="item:added"> <handler type="Sitecore.Sandbox.Events.Handlers.AutomagicallyCloneChildItemEventHandler, Sitecore.Sandbox" method="OnItemAdded"/> </event> </events> </sitecore> </configuration>
Let’s try this out, and see how we did.
I created a new child item under my source item:
As you can see, clones were added under all clones of the source item.
Plus, the notification about a new child item being added under the source item is not present:
This notification was automatically accepted in code via an instance of AcceptAsIsNotification within the item:added event handler defined above.
If you can think of any other interesting ways to leverage Sitecore’s item cloning capabilities, please leave a comment.
Swap Out Sitecore Layouts and Sublayouts Dynamically Based on a Theme
Out of the box, cloned items in Sitecore will retain the presentation components of their source items, and teasing these out could potentially lead to a mess.
I recently worked on a project where I had to architect a solution to avoid such a mess — this solution would ensure that items cloned across different websites in the same Sitecore instance could have a different look and feel from their source items.
Though I can’t show you what I did specifically on that project, I did go about creating a different solution just for you — yes, you — and this post showcases the fruit of that endeavor.
This is the basic idea of the solution:
For every sublayout (or the layout) set on an item, try to find a variant of that sublayout (or the layout) in a subfolder with the name of the selected theme. Otherwise, use the original.
For example, if /layouts/sublayouts/MySublayout.ascx is mapped to a sublayout set on the item and our selected theme is named “My Cool Theme”, try to find and use /layouts/sublayouts/My Cool Theme/MySublayout.ascx on the file system. If it does not exist, just use /layouts/sublayouts/MySublayout.ascx.
To start, I created some “theme” items. These items represent folders on my file system, and these folders house a specific layout and sublayouts pertinent to the theme. I set the parent folder of these items to be the source of the Theme droplist on my home page item:
In this solution, I defined objects as detectors — objects that ascertain particular conditions, depending on encapsulated logic within the detector class for the given source. This is the contract for all detectors in my solution:
namespace Sitecore.Sandbox.Utilities.Detectors.Base { public interface IDetector<T> { T Source { get; set; } bool Detect(); } }
In this solution, I am leveraging some string detectors:
namespace Sitecore.Sandbox.Utilities.Detectors.Base { public interface IStringDetector : IDetector<string> { } }
The first string detector I created was one to ascertain whether a file exists on the file system:
using System.IO; using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.Detectors.Base; namespace Sitecore.Sandbox.Utilities.Detectors { public class FileExistsDetector : IStringDetector { private string _Source; public string Source { get { return _Source; } set { Assert.ArgumentNotNull(value, "Source"); _Source = value; } } private FileExistsDetector() { } private FileExistsDetector(string source) { SetSource(source); } private void SetSource(string source) { Source = source; } public bool Detect() { Assert.ArgumentNotNull(Source, "Source"); return File.Exists(Source); } public static IStringDetector CreateNewFileExistsDetector() { return new FileExistsDetector(); } public static IStringDetector CreateNewFileExistsDetector(string source) { return new FileExistsDetector(source); } } }
The detector above is not web server aware — paths containing forward slashes will not be found. This led to the creation of the following decorator to convert server to file system paths:
using System.IO; using System.Web; using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.Detectors.Base; namespace Sitecore.Sandbox.Utilities.Detectors { public class ServerFileExistsDetector : IStringDetector { private HttpServerUtility HttpServerUtility { get; set; } private IStringDetector Detector { get; set; } public string Source { get { return Detector.Source; } set { Assert.ArgumentNotNull(value, "Source"); Detector.Source = HttpServerUtility.MapPath(value); } } private ServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector) { SetHttpServerUtility(httpServerUtility); SetDetector(detector); } private ServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source) { SetHttpServerUtility(httpServerUtility); SetDetector(detector); SetSource(source); } private void SetHttpServerUtility(HttpServerUtility httpServerUtility) { Assert.ArgumentNotNull(httpServerUtility, "httpServerUtility"); HttpServerUtility = httpServerUtility; } private void SetDetector(IStringDetector detector) { Assert.ArgumentNotNull(detector, "detector"); Detector = detector; } private void SetSource(string source) { Source = source; } public bool Detect() { Assert.ArgumentNotNull(Source, "Source"); return Detector.Detect(); } public static IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector) { return new ServerFileExistsDetector(httpServerUtility, detector); } public static IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source) { return new ServerFileExistsDetector(httpServerUtility, detector, source); } } }
I thought it would be best to control the instantiation of the string detectors above into a factory class:
using System.Web; namespace Sitecore.Sandbox.Utilities.Detectors.Base { public interface IStringDetectorFactory { IStringDetector CreateNewFileExistsDetector(); IStringDetector CreateNewFileExistsDetector(string source); IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility); IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector); IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source); } }
using System.Web; using Sitecore.Sandbox.Utilities.Detectors.Base; namespace Sitecore.Sandbox.Utilities.Detectors { public class StringDetectorFactory : IStringDetectorFactory { private StringDetectorFactory() { } public IStringDetector CreateNewFileExistsDetector() { return FileExistsDetector.CreateNewFileExistsDetector(); } public IStringDetector CreateNewFileExistsDetector(string source) { return FileExistsDetector.CreateNewFileExistsDetector(source); } public IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility) { return ServerFileExistsDetector.CreateNewServerFileExistsDetector(httpServerUtility, CreateNewFileExistsDetector()); } public IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector) { return ServerFileExistsDetector.CreateNewServerFileExistsDetector(httpServerUtility, detector); } public IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source) { return ServerFileExistsDetector.CreateNewServerFileExistsDetector(httpServerUtility, detector, source); } public static IStringDetectorFactory CreateNewStringDetectorFactory() { return new StringDetectorFactory(); } } }
I then needed a way to ascertain if an item is a sublayout — we shouldn’t be messing with presentation components that are not sublayouts (not including layouts which is handled in a different place):
using Sitecore.Data.Items; namespace Sitecore.Sandbox.Utilities.Detectors.Base { public interface IItemDetector : IDetector<Item> { } }
using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.Detectors.Base; namespace Sitecore.Sandbox.Utilities.Detectors { public class SublayoutDetector : IItemDetector { private Item _Source; public Item Source { get { return _Source; } set { Assert.ArgumentNotNull(value, "Source"); _Source = value; } } private SublayoutDetector() { } private SublayoutDetector(Item source) { SetSource(source); } private void SetSource(Item source) { Source = source; } public bool Detect() { Assert.ArgumentNotNull(Source, "Source"); return Source.TemplateID == TemplateIDs.Sublayout; } public static IItemDetector CreateNewSublayoutDetector() { return new SublayoutDetector(); } public static IItemDetector CreateNewSublayoutDetector(Item source) { return new SublayoutDetector(source); } } }
I then reused the concept of manipulators — objects that transform an instance of an object into a different instance — from a prior blog post. I forget the post I used manipulators in, so I have decided to re-post the base contract for all manipulator classes:
namespace Sitecore.Sandbox.Utilities.Manipulators.Base { public interface IManipulator<T> { T Manipulate(T source); } }
Yes, we will be manipulating strings. 🙂
namespace Sitecore.Sandbox.Utilities.Manipulators.Base { public interface IStringManipulator : IManipulator<string> { } }
I also defined another interface containing an accessor and mutator for a string representing the selected theme:
namespace Sitecore.Sandbox.Utilities.Manipulators.Base { public interface IThemeFolder { string ThemeFolder { get; set; } } }
I created a class that will wedge in a theme folder name into a file path — it is inserted after the last forward slash in the file path — and returns the resulting string:
namespace Sitecore.Sandbox.Utilities.Manipulators.Base { public interface IThemeFilePathManipulator : IStringManipulator, IThemeFolder { } }
using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.Manipulators.Base; namespace Sitecore.Sandbox.Utilities.Manipulators { public class ThemeFilePathManipulator : IThemeFilePathManipulator { private const string Slash = "/"; private const int NotFoundIndex = -1; private string _ThemeFolder; public string ThemeFolder { get { return _ThemeFolder; } set { _ThemeFolder = value; } } private ThemeFilePathManipulator() { } private ThemeFilePathManipulator(string themeFolder) { SetThemeFolder(themeFolder); } public string Manipulate(string source) { Assert.ArgumentNotNullOrEmpty(source, "source"); int lastIndex = source.LastIndexOf(Slash); bool canManipulate = !string.IsNullOrEmpty(ThemeFolder) && lastIndex > NotFoundIndex; if (canManipulate) { return source.Insert(lastIndex + 1, string.Concat(ThemeFolder, Slash)); } return source; } private void SetThemeFolder(string themeFolder) { ThemeFolder = themeFolder; } public static IThemeFilePathManipulator CreateNewThemeFilePathManipulator() { return new ThemeFilePathManipulator(); } public static IThemeFilePathManipulator CreateNewThemeFilePathManipulator(string themeFolder) { return new ThemeFilePathManipulator(themeFolder); } } }
But shouldn’t we see if the file exists, and then return the path if it does, or the original path if it does not? This question lead to the creation of the following decorator class to do just that:
using Sitecore.Diagnostics; using Sitecore.Sandbox.Utilities.Detectors.Base; using Sitecore.Sandbox.Utilities.Manipulators.Base; namespace Sitecore.Sandbox.Utilities.Manipulators { public class ThemeFilePathIfExistsManipulator : IThemeFilePathManipulator { private IThemeFilePathManipulator InnerManipulator { get; set; } private IStringDetector FileExistsDetector { get; set; } private string _ThemeFolder; public string ThemeFolder { get { return InnerManipulator.ThemeFolder; } set { InnerManipulator.ThemeFolder = value; } } private ThemeFilePathIfExistsManipulator(IThemeFilePathManipulator innerManipulator, IStringDetector fileExistsDetector) { SetInnerManipulator(innerManipulator); SetFileExistsDetector(fileExistsDetector); } private void SetInnerManipulator(IThemeFilePathManipulator innerManipulator) { Assert.ArgumentNotNull(innerManipulator, "innerManipulator"); InnerManipulator = innerManipulator; } private void SetFileExistsDetector(IStringDetector fileExistsDetector) { Assert.ArgumentNotNull(fileExistsDetector, "fileExistsDetector"); FileExistsDetector = fileExistsDetector; } public string Manipulate(string source) { Assert.ArgumentNotNullOrEmpty(source, "source"); string filePath = InnerManipulator.Manipulate(source); if(!DoesFileExist(filePath)) { return source; } return filePath; } private bool DoesFileExist(string filePath) { FileExistsDetector.Source = filePath; return FileExistsDetector.Detect(); } private void SetThemeFolder(string themeFolder) { ThemeFolder = themeFolder; } public static IThemeFilePathManipulator CreateNewThemeFilePathManipulator(IThemeFilePathManipulator innerManipulator, IStringDetector fileExistsDetector) { return new ThemeFilePathIfExistsManipulator(innerManipulator, fileExistsDetector); } } }
Next, I defined a contract for manipulators that transform instances of Sitecore.Layouts.RenderingReference:
using Sitecore.Layouts; namespace Sitecore.Sandbox.Utilities.Manipulators.Base { public interface IRenderingReferenceManipulator : IManipulator<RenderingReference> { } }
I then created a class to manipulate instances of Sitecore.Layouts.RenderingReference by swapping out their sublayouts with new instances based on the path returned by the IThemeFilePathManipulator instance:
namespace Sitecore.Sandbox.Utilities.Manipulators.Base { public interface IThemeRenderingReferenceManipulator : IRenderingReferenceManipulator, IThemeFolder { } }
using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Layouts; using Sitecore.Web.UI.WebControls; using Sitecore.Sandbox.Utilities.Detectors.Base; using Sitecore.Sandbox.Utilities.Manipulators.Base; namespace Sitecore.Sandbox.Utilities.Manipulators { public class ThemeRenderingReferenceManipulator : IThemeRenderingReferenceManipulator { private IItemDetector SublayoutDetector { get; set; } private IThemeFilePathManipulator ThemeFilePathManipulator { get; set; } public string ThemeFolder { get { return ThemeFilePathManipulator.ThemeFolder; } set { ThemeFilePathManipulator.ThemeFolder = value; } } private ThemeRenderingReferenceManipulator() : this(CreateDefaultThemeFilePathManipulator()) { } private ThemeRenderingReferenceManipulator(IThemeFilePathManipulator themeFilePathManipulator) : this(CreateDefaultSublayoutDetector(), themeFilePathManipulator) { } private ThemeRenderingReferenceManipulator(IItemDetector sublayoutDetector, IThemeFilePathManipulator themeFilePathManipulator) { SetSublayoutDetector(sublayoutDetector); SetThemeFilePathManipulator(themeFilePathManipulator); } private void SetSublayoutDetector(IItemDetector sublayoutDetector) { Assert.ArgumentNotNull(sublayoutDetector, "sublayoutDetector"); SublayoutDetector = sublayoutDetector; } private void SetThemeFilePathManipulator(IThemeFilePathManipulator themeFilePathManipulator) { Assert.ArgumentNotNull(themeFilePathManipulator, "themeFilePathManipulator"); ThemeFilePathManipulator = themeFilePathManipulator; } public RenderingReference Manipulate(RenderingReference source) { Assert.ArgumentNotNull(source, "source"); Assert.ArgumentNotNull(source.RenderingItem, "source.RenderingItem"); if (!IsSublayout(source.RenderingItem.InnerItem)) { return source; } RenderingReference renderingReference = source; Sublayout sublayout = source.GetControl() as Sublayout; if (sublayout == null) { return source; } renderingReference.SetControl(CreateNewSublayout(sublayout, ThemeFilePathManipulator.Manipulate(sublayout.Path))); return renderingReference; } private bool IsSublayout(Item item) { Assert.ArgumentNotNull(item, "item"); SublayoutDetector.Source = item; return SublayoutDetector.Detect(); } private static Sublayout CreateNewSublayout(Sublayout sublayout, string path) { Assert.ArgumentNotNull(sublayout, "sublayout"); Assert.ArgumentNotNullOrEmpty(path, "path"); return new Sublayout { ID = sublayout.ID, Path = path, Background = sublayout.Background, Border = sublayout.Border, Bordered = sublayout.Bordered, Cacheable = sublayout.Cacheable, CacheKey = sublayout.CacheKey, CacheTimeout = sublayout.CacheTimeout, Clear = sublayout.Clear, CssStyle = sublayout.CssStyle, Cursor = sublayout.Cursor, Database = sublayout.Database, DataSource = sublayout.DataSource, DisableDebug = sublayout.DisableDebug, DisableDots = sublayout.DisableDots, DisableSecurity = sublayout.DisableSecurity, Float = sublayout.Float, Foreground = sublayout.Foreground, LiveDisplay = sublayout.LiveDisplay, LoginRendering = sublayout.LoginRendering, Margin = sublayout.Margin, Padding = sublayout.Padding, Parameters = sublayout.Parameters, RenderAs = sublayout.RenderAs, RenderingID = sublayout.RenderingID, RenderingName = sublayout.RenderingName, VaryByData = sublayout.VaryByData, VaryByDevice = sublayout.VaryByDevice, VaryByLogin = sublayout.VaryByLogin, VaryByParm = sublayout.VaryByParm, VaryByQueryString = sublayout.VaryByQueryString, VaryByUser = sublayout.VaryByUser }; } private static IThemeFilePathManipulator CreateDefaultThemeFilePathManipulator() { return Manipulators.ThemeFilePathManipulator.CreateNewThemeFilePathManipulator(); } private static IItemDetector CreateDefaultSublayoutDetector() { return Utilities.Detectors.SublayoutDetector.CreateNewSublayoutDetector(); } public static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator() { return new ThemeRenderingReferenceManipulator(); } public static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator(IThemeFilePathManipulator themeFilePathManipulator) { return new ThemeRenderingReferenceManipulator(themeFilePathManipulator); } public static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator(IItemDetector sublayoutDetector, IThemeFilePathManipulator themeFilePathManipulator) { return new ThemeRenderingReferenceManipulator(sublayoutDetector, themeFilePathManipulator); } } }
I created a new httpRequestBegin pipeline processor to use a theme’s layout if found on the file system — such is done by delegating to instances of classes defined above:
using System; using System.Web; using Sitecore.Configuration; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Pipelines.HttpRequest; using Sitecore.Sandbox.Utilities.Manipulators; using Sitecore.Sandbox.Utilities.Manipulators.Base; using Sitecore.Sandbox.Utilities.Detectors; using Sitecore.Sandbox.Utilities.Detectors.Base; namespace Sitecore.Sandbox.Pipelines.HttpRequest { public class ThemeLayoutResolver : HttpRequestProcessor { public override void Process(HttpRequestArgs args) { if (!CanProcess()) { return; } string themeFilePath = GetThemeFilePath(); bool swapOutFilePath = !AreEqualIgnoreCase(Context.Page.FilePath, themeFilePath); if (swapOutFilePath) { Context.Page.FilePath = themeFilePath; } } private static bool CanProcess() { return Context.Database != null && !IsCore(Context.Database); } private static bool IsCore(Database database) { return database.Name == Constants.CoreDatabaseName; } private static string GetThemeFilePath() { IThemeFilePathManipulator themeFilePathManipulator = CreateNewThemeFilePathManipulator(); themeFilePathManipulator.ThemeFolder = GetTheme(); return themeFilePathManipulator.Manipulate(Context.Page.FilePath); } private static bool DoesFileExist(string filePath) { Assert.ArgumentNotNullOrEmpty(filePath, "filePath"); IStringDetector fileExistsDetector = CreateNewServerFileExistsDetector(); fileExistsDetector.Source = filePath; return fileExistsDetector.Detect(); } private static IThemeFilePathManipulator CreateNewThemeFilePathManipulator() { return ThemeFilePathIfExistsManipulator.CreateNewThemeFilePathManipulator ( ThemeFilePathManipulator.CreateNewThemeFilePathManipulator(), CreateNewServerFileExistsDetector() ); } private static IStringDetector CreateNewServerFileExistsDetector() { IStringDetectorFactory factory = StringDetectorFactory.CreateNewStringDetectorFactory(); return factory.CreateNewServerFileExistsDetector(HttpContext.Current.Server); } private static string GetTheme() { Item home = GetHomeItem(); return home["Theme"]; } private static Item GetHomeItem() { string startPath = Factory.GetSite(Sitecore.Context.GetSiteName()).StartPath; return Context.Database.GetItem(startPath); } private static bool AreEqualIgnoreCase(string stringOne, string stringTwo) { return string.Equals(stringOne, stringTwo, StringComparison.CurrentCultureIgnoreCase); } } }
The above code should not run if the context database is the core database — this could mean we are in the Sitecore shell, or on the Sitecore login page (I painfully discovered this earlier today after doing a QA drop, and had to act fast to fix it).
I then created a RenderLayout pipeline processor to swap out sublayouts for themed sublayouts if they exist on the file system:
using System.Collections.Generic; using System.Linq; using System.Web; using Sitecore.Configuration; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Layouts; using Sitecore.Pipelines; using Sitecore.Pipelines.InsertRenderings; using Sitecore.Pipelines.RenderLayout; using Sitecore.Sandbox.Utilities.Manipulators; using Sitecore.Sandbox.Utilities.Manipulators.Base; using Sitecore.Sandbox.Utilities.Detectors; using Sitecore.Sandbox.Utilities.Detectors.Base; namespace Sitecore.Sandbox.Pipelines.RenderLayout { public class InsertThemeRenderings : RenderLayoutProcessor { public override void Process(RenderLayoutArgs args) { Assert.ArgumentNotNull(args, "args"); if (Context.Item == null) { return; } using (new ProfileSection("Insert renderings into page.")) { IEnumerable<RenderingReference> themeRenderingReferences = GetThemeRenderingReferences(GetRenderingReferences()); foreach (RenderingReference renderingReference in themeRenderingReferences) { Context.Page.AddRendering(renderingReference); } } } private static IEnumerable<RenderingReference> GetRenderingReferences() { InsertRenderingsArgs insertRenderingsArgs = new InsertRenderingsArgs(); CorePipeline.Run("insertRenderings", insertRenderingsArgs); return insertRenderingsArgs.Renderings.ToList(); } private static IEnumerable<RenderingReference> GetThemeRenderingReferences(IEnumerable<RenderingReference> renderingReferences) { Assert.ArgumentNotNull(renderingReferences, "renderingReferences"); IList<RenderingReference> themeRenderingReferences = new List<RenderingReference>(); IThemeRenderingReferenceManipulator themeRenderingReferenceManipulator = CreateNewRenderingReferenceManipulator(); foreach (RenderingReference renderingReference in renderingReferences) { themeRenderingReferences.Add(themeRenderingReferenceManipulator.Manipulate(renderingReference)); } return themeRenderingReferences; } private static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator() { IThemeRenderingReferenceManipulator themeRenderingReferenceManipulator = ThemeRenderingReferenceManipulator.CreateNewRenderingReferenceManipulator(CreateNewThemeFilePathManipulator()); themeRenderingReferenceManipulator.ThemeFolder = GetTheme(); return themeRenderingReferenceManipulator; } private static IThemeFilePathManipulator CreateNewThemeFilePathManipulator() { return ThemeFilePathIfExistsManipulator.CreateNewThemeFilePathManipulator ( ThemeFilePathManipulator.CreateNewThemeFilePathManipulator(), CreateNewServerFileExistsDetector() ); } private static IStringDetector CreateNewServerFileExistsDetector() { IStringDetectorFactory factory = StringDetectorFactory.CreateNewStringDetectorFactory(); return factory.CreateNewServerFileExistsDetector(HttpContext.Current.Server); } private static string GetTheme() { Item home = GetHomeItem(); return home["Theme"]; } private static Item GetHomeItem() { string startPath = Factory.GetSite(Sitecore.Context.GetSiteName()).StartPath; return Context.Database.GetItem(startPath); } } }
I baked everything together in a patch include configuration file:
<?xml version="1.0" encoding="utf-8" ?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <renderLayout> <processor patch:instead="processor[@type='Sitecore.Pipelines.RenderLayout.InsertRenderings, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.RenderLayout.InsertThemeRenderings, Sitecore.Sandbox" /> </renderLayout> <httpRequestBegin> <processor patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.HttpRequest.ThemeLayoutResolver, Sitecore.Sandbox"/> </httpRequestBegin> </pipelines> </sitecore> </configuration>
Here are the “out of the box” presentation components mapped to my test page:
The above layout and sublayouts live in /layouts from my website root.
For my themes, I duplicated the layout and some of the sublayouts above into themed folders, and added some css for color — I’ve omitted this for brevity (come on Mike, this post is already wicked long :)).
The “Blue Theme” layout and sublayout live in /layouts/Blue Theme from my website root:
I chose “Blue Theme” in the Theme droplist on my home item, published, and then navigated to my test page:
The “Inverse Blue Theme” sublayout lives in /layouts/Inverse Blue Theme from my website root:
I chose “Inverse Blue Theme” in the Theme droplist on my home item, published, and then reloaded my test page:
The “Green Theme” layout and sublayouts live in /layouts/Green Theme from my website root:
I chose “Green Theme” in the Theme droplist on my home item, published, and then refreshed my test page:
Do you know of or have an idea for another solution to accomplish the same? If so, please drop a comment.