Home » Layouts » Swap Out Sitecore Layouts and Sublayouts Dynamically Based on a Theme

Swap Out Sitecore Layouts and Sublayouts Dynamically Based on a Theme

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

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:

theme-droplist

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:

theme-sublayouts-rendering-presentation-details

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:

blue-theme-files

I chose “Blue Theme” in the Theme droplist on my home item, published, and then navigated to my test page:

blue-theme-page

The “Inverse Blue Theme” sublayout lives in /layouts/Inverse Blue Theme from my website root:

inverse-blue-theme-files

I chose “Inverse Blue Theme” in the Theme droplist on my home item, published, and then reloaded my test page:

inverse-blue-theme-page

The “Green Theme” layout and sublayouts live in /layouts/Green Theme from my website root:

green-theme-files

I chose “Green Theme” in the Theme droplist on my home item, published, and then refreshed my test page:

green-theme-page

Do you know of or have an idea for another solution to accomplish the same? If so, please drop a comment.


1 Comment

  1. mursino says:

    This is really cool…

    We’ve done quite a bit of re-usable components on multi-site solutions and many times opt for site-specific CSS rather than this granular of control to swap out individual ASCXs. This can give much for fine-grained control, however the disadvantage may be lots of code-behind duplication. Instead sometimes we make our HTML mark-up very generic and add extra wrappers so a site theme is purely based on CSS where possible. Great solution!

Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.