Home » 2013 » April

Monthly Archives: April 2013

Advertisements

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:

being-notified-about-new-child-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:

added-new-child-automatically-cloned

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:

no-notification-accepted-programmatically

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.

Advertisements

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:

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.

Reuse Sitecore Data Template Fields by Pulling Them Up Into a Base Template

How many times have you discovered that you need to reuse some fields defined in a data template, but don’t want to use that data template as a base template since it defines a bunch of fields that you don’t want to use?

What do you usually do in such a situation?

In the past, I would have created a new data template, moved the fields I would like to reuse underneath it, followed by setting that new template as a base template on the data template I moved the fields from.

I’ve spoken with other Sitecore developers on what they do in this situation, and one developer conveyed he duplicates the template, and delete fields that are not to be reused — thus creating a new base template. Though this might save time, I would advise against it — any code referencing fields by IDs will break using this paradigm for creating a new base template.

Martin Fowler — in his book
Refactoring: Improving the Design of Existing Code — defined the refactoring technique ‘Pull Up Field’ for moving fields from a class up into its base class — dubbed superclass for the Java inclined audience.

One should employ this refactoring technique when other subclasses of the base class are to reuse the fields being pulled up.

When I first encountered this refactoring technique in Fowler’s book, I pondered over how useful this would be in the Sitecore data template world, and decided to build two tools for pulling up fields into a base template. One tool will create a new base template, and the other will allow users to move fields to an existing base template.

The following class defines methods that will be used by two new client pipelines I will define below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Data.Templates;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.Dialogs.ItemLister;
using Sitecore.Shell.Framework;
using Sitecore.Text;
using Sitecore.Web;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class PullUpFieldsIntoBaseTemplate
    {
        private const char IDDelimiter = '|';
        private const string BaseTemplateFieldName = "__Base template";
        
        public void EnsureTemplate(ClientPipelineArgs args)
        {
            Item item = GetItem(args);
            if (!IsTemplate(item))
            {
                CancelOperation(args, "The supplied item is not a template!");
            }
        }

        private static bool IsTemplate(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return TemplateManager.IsTemplate(item);
        }

        public void SelectFields(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!args.IsPostBack)
            {
                UrlString urlString = new UrlString(UIUtil.GetUri("control:TreeListExEditor"));
                UrlHandle urlHandle = new UrlHandle();
                urlHandle["title"] = "Select Fields";
                urlHandle["text"] = "Select the fields to pull up into a base template.";
                urlHandle["source"] = GetSelectFieldsDialogSource(args);
                urlHandle.Add(urlString);
                SheerResponse.ShowModalDialog(urlString.ToString(), "800px", "500px", string.Empty, true);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["fieldIds"] = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                CancelOperation(args);
            }
        }

        private static string GetSelectFieldsDialogSource(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(args.Parameters["database"], "args.Parameters[\"database\"]");
            Assert.ArgumentNotNullOrEmpty(args.Parameters["templateId"], "args.Parameters[\"templateId\"]");
            return string.Format
            (
                "AllowMultipleSelection=true&DatabaseName={0}&DataSource={1}&IncludeTemplatesForSelection=Template field",
                args.Parameters["database"],
                args.Parameters["templateId"]
            );
        }

        public void ChooseBaseTemplateName(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                SheerResponse.Input("Enter the name of the base template", string.Concat("New Base Template 1"));
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["baseTemplateName"] = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                CancelOperation(args);
            }
        }

        public void ChooseBaseTemplateLocation(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                Dialogs.BrowseItem
                (
                    "Select Base Template Location",
                    "Select the parent item under which the new base template will be created.",
                    args.Parameters["icon"],
                    "OK",
                    "/sitecore/templates",
                    args.Parameters["templateId"]
                );

                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["baseTemplateParentId"] = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                CancelOperation(args);
            }
        }

        public void CreateNewBaseTemplate(ClientPipelineArgs args)
        {
            Database database = GetDatabase(args.Parameters["database"]);
            Item baseTemplateParent = database.GetItem(args.Parameters["baseTemplateParentId"]);
            Item baseTemplate = baseTemplateParent.Add(args.Parameters["baseTemplateName"], new TemplateID(TemplateIDs.Template));
            SetBaseTemplateField(baseTemplate, TemplateIDs.StandardTemplate);
            args.Parameters["baseTemplateId"] = baseTemplate.ID.ToString();
        }

        public void ChooseExistingBaseTemplate(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                ItemListerOptions itemListerOptions = new ItemListerOptions
                {
                    ButtonText = "OK",
                    Icon = args.Parameters["icon"],
                    Title = "Select Base Template",
                    Text = "Select the base template for pulling up the fields."
                };

                TemplateItem templateItem = GetItem(args);
                itemListerOptions.Items = ExcludeStandardTemplate(templateItem.BaseTemplates).Select(baseTemplate => baseTemplate.InnerItem).ToList();
                itemListerOptions.AddTemplate(TemplateIDs.Template);
                SheerResponse.ShowModalDialog(itemListerOptions.ToUrlString().ToString(), true);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["baseTemplateId"] = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                CancelOperation(args);
            }
        }

        private static IEnumerable<TemplateItem> ExcludeStandardTemplate(IEnumerable<TemplateItem> baseTemplates)
        {
            if (baseTemplates != null && baseTemplates.Any())
            {
                return baseTemplates.Where(baseTemplate => baseTemplate.ID != TemplateIDs.StandardTemplate);
            }

            return baseTemplates;
        }

        public void EnsureCanCreateAtBaseTemplateLocation(ClientPipelineArgs args)
        {
            Item baseTemplateParent = GetItem(args.Parameters["database"], args.Parameters["baseTemplateParentId"]);
            if (!baseTemplateParent.Access.CanCreate())
            {
                CancelOperation(args, "You cannot create an item at the selected location!");
            }
        }

        public void Execute(ClientPipelineArgs args)
        {
            Database database = GetDatabase(args.Parameters["database"]);
            Item baseTemplate = database.GetItem(args.Parameters["baseTemplateId"]);
            SetBaseTemplateField(baseTemplate, TemplateIDs.StandardTemplate);
            IDictionary<SectionInformation, IList<Item>> sectionsWithFields = GetSectionsWithFields(database, args.Parameters["fieldIds"]);

            foreach (SectionInformation sectionInformation in sectionsWithFields.Keys)
            {
                IEnumerable<Item> fields = sectionsWithFields[sectionInformation];
                MoveFieldsToAppropriateSection(baseTemplate, sectionInformation, fields);
            }

            Item templateItem = database.GetItem(args.Parameters["templateId"]);
            ListString baseTemplateIDs = new ListString(templateItem[BaseTemplateFieldName], IDDelimiter);
            baseTemplateIDs.Add(baseTemplate.ID.ToString());
            SetBaseTemplateField(templateItem, baseTemplateIDs);
            Refresh(templateItem);
        }

        private static void MoveFieldsToAppropriateSection(Item baseTemplate, SectionInformation sectionInformation, IEnumerable<Item> fields)
        {
            TemplateItem templateItem = baseTemplate;
            TemplateSectionItem templateSectionItem = templateItem.GetSection(sectionInformation.Section.Name);

            if (templateSectionItem != null)
            {
                MoveFields(templateSectionItem, fields);
            }
            else
            {
                Item sectionItemCopy = sectionInformation.Section.CopyTo(baseTemplate, sectionInformation.Section.Name, ID.NewID, false);
                MoveFields(sectionItemCopy, fields);
            }

            if (!sectionInformation.Section.GetChildren().Any())
            {
                sectionInformation.Section.Delete();
            }
        }

        private static void MoveFields(TemplateSectionItem templateSectionItem, IEnumerable<Item> fields)
        {
            Assert.ArgumentNotNull(templateSectionItem, "templateSectionItem");
            Assert.ArgumentNotNull(fields, "fields");
            foreach (Item field in fields)
            {
                field.MoveTo(templateSectionItem.InnerItem);
            }
        }

        private static void SetBaseTemplateField(Item templateItem, object baseTemplateIds)
        {
            Assert.ArgumentNotNull(templateItem, "templateItem");
            templateItem.Editing.BeginEdit();
            templateItem[BaseTemplateFieldName] = EnsureDistinct(baseTemplateIds.ToString(), IDDelimiter);
            templateItem.Editing.EndEdit();
        }

        private static string EnsureDistinct(string strings, char delimiter)
        {
            return string.Join(delimiter.ToString(), EnsureDistinct(strings.ToString().Split(new[] { delimiter })));
        }

        private static IEnumerable<string> EnsureDistinct(IEnumerable<string> strings)
        {
            return strings.Distinct();
        }

        private static IDictionary<SectionInformation, IList<Item>> GetSectionsWithFields(Database database, string fieldIds)
        {
            IDictionary<SectionInformation, IList<Item>> sectionsWithFields = new Dictionary<SectionInformation, IList<Item>>();

            foreach (TemplateFieldItem field in GetFields(database, fieldIds))
            {
                SectionInformation sectionInformation = new SectionInformation(field.Section.InnerItem);
                if (sectionsWithFields.ContainsKey(sectionInformation))
                {
                    sectionsWithFields[sectionInformation].Add(field.InnerItem);
                }
                else
                {
                    sectionsWithFields.Add(sectionInformation, new List<Item>(new[] { field.InnerItem }));
                }
            }

            return sectionsWithFields;
        }

        private static IEnumerable<TemplateFieldItem> GetFields(Database database, string fieldIds)
        {
            ListString fieldIdsListString = new ListString(fieldIds, IDDelimiter);
            IList<TemplateFieldItem> fields = new List<TemplateFieldItem>();

            foreach (string fieldId in fieldIdsListString)
            {
                fields.Add(database.GetItem(fieldId));
            }

            return fields;
        }

        private static Item GetItem(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Item item = GetItem(args.Parameters["database"], args.Parameters["templateId"]);
            Assert.IsNotNull(item, "Item cannot be null!");
            return item;
        }

        private static Item GetItem(string databaseName, string itemId)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            Assert.ArgumentNotNullOrEmpty(itemId, "itemId");
            ID id = GetID(itemId);
            Assert.IsTrue(!ID.IsNullOrEmpty(id), "itemId is not valid!");
            Database database = GetDatabase(databaseName);
            Assert.IsNotNull(database, "Database is not valid!");
            return database.GetItem(id);
        }

        private static ID GetID(string itemId)
        {
            ID id;
            if (ID.TryParse(itemId, out id))
            {
                return id;
            }

            return ID.Null;
        }

        private static Database GetDatabase(string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            return Factory.GetDatabase(databaseName);
        }
        
        private static void CancelOperation(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            CancelOperation(args, "Operation has been cancelled!");
        }

        private static void CancelOperation(ClientPipelineArgs args, string alertMessage)
        {
            Assert.ArgumentNotNull(args, "args");

            if (!string.IsNullOrEmpty(alertMessage))
            {
                SheerResponse.Alert(alertMessage);
            }

            args.AbortPipeline();
        }

        private static void Refresh(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", item.ID), 1);
        }

        public class SectionInformation : IEquatable<SectionInformation>
        {
            public Item Section { get; set; }

            public SectionInformation(Item section)
            {
                Assert.ArgumentNotNull(section, "section");
                Section = section;
            }

            public override int GetHashCode()
            {
                return Section.ID.Guid.GetHashCode();
            }

            public override bool Equals(object other)
            {
                return Equals(other as SectionInformation);
            }

            public bool Equals(SectionInformation other)
            {
                return other != null 
                        && Section.ID.Guid == Section.ID.Guid;
            }
        }
    }
}

This above class defines methods that will be used as pipeline processor steps to prompt users for input — via dialogs — needed for pulling up fields to a new or existing base template.

I then defined commands to launch the pipelines I will define later on. I employed the Template method pattern here — each subclass must define its icon path, and the client pipeline it depends on after being clicked:

using System.Collections.Specialized;
using System.Linq;

using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Commands.Base
{
    public abstract class PullUpFieldsCommand : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Context.ClientPage.Start(GetPullUpFieldsClientPipelineName(), CreateNewParameters(GetItem(context)));
        }

        protected abstract string GetPullUpFieldsClientPipelineName();

        private NameValueCollection CreateNewParameters(Item templateItem)
        {
            return new NameValueCollection 
            {
                {"icon", GetIcon()},
                {"database", templateItem.Database.Name},
                {"templateId", templateItem.ID.ToString()}
            };
        }

        protected abstract string GetIcon();

        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            if (ShouldEnable(GetItem(context)))
            {
                return CommandState.Enabled;
            }

            return CommandState.Hidden;
        }

        protected static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Items, "context.Items");
            Assert.ArgumentCondition(context.Items.Any(), "context.Items", "At least one item must be present!");
            return context.Items.FirstOrDefault();
        }

        private static bool ShouldEnable(Item item)
        {
            return item != null && IsTemplate(item);
        }

        private static bool IsTemplate(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return TemplateManager.IsTemplate(item);
        }
    }
}

The command for pulling up fields into a new base template:

using Sitecore.Sandbox.Commands.Base;

namespace Sitecore.Sandbox.Commands
{
    public class PullUpFieldsIntoNewBaseTemplate : PullUpFieldsCommand
    {
        protected override string GetIcon()
        {
            return "Business/32x32/up_plus.png";
        }

        protected override string GetPullUpFieldsClientPipelineName()
        {
            return "uiPullUpFieldsIntoNewBaseTemplate";
        }
    }
}

The command for pulling up fields into an existing base template — it will be visible only when the template has base templates, not including the Standard Template:

using System.Linq;

using Sitecore.Sandbox.Commands.Base;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Commands
{
    public class PullUpFieldsIntoExistingBaseTemplate : PullUpFieldsCommand
    {
        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            bool shouldEnabled = base.QueryState(context) == CommandState.Enabled 
                                    && DoesTemplateHaveBaseTemplates(context);
            if (shouldEnabled)
            {
                return CommandState.Enabled;
            }

            return CommandState.Hidden;
        }

        private static bool DoesTemplateHaveBaseTemplates(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            TemplateItem templateItem = GetItem(context);
            return templateItem.BaseTemplates.Count() > 1 
                    || (templateItem.BaseTemplates.Any() 
                            && templateItem.BaseTemplates.FirstOrDefault().ID != TemplateIDs.StandardTemplate);
        }

        protected override string GetIcon()
        {
            return "Business/32x32/up_minus.png";
        }

        protected override string GetPullUpFieldsClientPipelineName()
        {
            return "uiPullUpFieldsIntoExistingBaseTemplate";
        }
    }
}

I’ve omitted how I mapped these to context menu buttons in the core database. If you are unfamiliar with how this is done, please take a look at my first and second posts on augmenting the item context menu to see how this is done.

I registered my commands, and glued methods in the PullUpFieldsIntoBaseTemplate class together into two client pipelines in a configuration include file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:pullupfieldsintonewbasetemplate" type="Sitecore.Sandbox.Commands.PullUpFieldsIntoNewBaseTemplate,Sitecore.Sandbox"/>
      <command name="item:pullupfieldsintoexistingbasetemplate" type="Sitecore.Sandbox.Commands.PullUpFieldsIntoExistingBaseTemplate,Sitecore.Sandbox"/>
    </commands>
    <processors>
      <uiPullUpFieldsIntoNewBaseTemplate>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="EnsureTemplate"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="SelectFields"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="ChooseBaseTemplateName"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="ChooseBaseTemplateLocation"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="EnsureCanCreateAtBaseTemplateLocation"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="CreateNewBaseTemplate"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="Execute"/>
      </uiPullUpFieldsIntoNewBaseTemplate>
      <uiPullUpFieldsIntoExistingBaseTemplate>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="EnsureTemplate"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="SelectFields"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="ChooseExistingBaseTemplate"/>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.PullUpFieldsIntoBaseTemplate,Sitecore.Sandbox" method="Execute"/>
      </uiPullUpFieldsIntoExistingBaseTemplate>
    </processors>
  </sitecore>
</configuration>

The uiPullUpFieldsIntoNewBaseTemplate client pipeline contains processor steps to present the user with dialogs for pulling up fields into a new base template that is created during one of those steps.

The uiPullUpFieldsIntoExistingBaseTemplate pipeline prompts users to select a base template from the list of base templates set on the template.

Let’s see this in action.

I created a template with some fields for testing:

pull-up-fields-landing-page

I right-clicked the on my Landing Page template to launch its context menu, and clicked on the ‘Pull Up Fields Into New Base Template’ context menu option:

pull-up-new-base-template-context-menu

The following dialog was presented to me for selecting fields to pull up:

I clicked the ‘OK’ button, and was prompted to input a name for a new base template. I entered ‘Page Base’:

pull-up-page-base

After clicking ‘OK’, I was asked to select the location where I want to store my new base template:

pull-up-base-template-folder

Not too long after clicking ‘OK’ again, I see that my new base template was created, and the Title field was moved into it:

pull-up-page-base-created

Further, I see that my Landing Page template now has Page Base as one of its base templates:

pull-up-page-base-set-base-template

I right-clicked again, and was presented with an additional context menu option to move fields into an existing base template — I clicked on this option:

pull-up-existing-base-template-context-menu

I then selected the remaining field under the Page Data section:

pull-up-selected-text-field

After clicking ‘OK’, a dialog presented me with a list of base templates to choose from — in this case there was only one base template to choose:

pull-up-selected-page-base-template

After clicking ‘OK’, I see that the Text field was moved into the Page Base template, and the Page Data section under my Landing Page template was removed:

pull-up-text-field

I hope the above tools will save you time in pulling up fields into base templates.

If you can think of other development tools that would be useful in the Sitecore client, please drop a comment.

Addendum

I have put a polished version of the above onto the Sitecore Marketplace.