Home » Commands (Page 2)

Category Archives: Commands

Delete All But This: Delete Sibling Items Using a Custom Item Context Menu Option in Sitecore

Every so often I find myself having to delete all Sitecore items in a folder except for one — the reason for this eludes me at the moment, but it does make an appearance once in a while (if this also happens to you, and you remember the context for why it happens, please share in a comment) — and it feels like it takes ages to delete all of these items: I have to step through all of these items in the Sitecore content tree, and delete each individually .

When this happens I usually say to myself “wouldn’t it be cool to have something like the ‘Close All But This’ feature found in Visual Studio?”:

close-all-but-this-vs

I always forget to write this idea down, but did remember it a couple of days ago, and decided to build something to save time when deleting all items in a folder except for one.

To delete all items in a folder, we need a way to get all sibling items, and exclude the item we don’t want to delete. I decided to create a custom pipeline to do this, and defined the following parameter object for it:

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

using Sitecore.Data.Items;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.GetSiblings
{
    public class GetSiblingsArgs : PipelineArgs
    {
        public Item Item { get; set; }

        public IEnumerable<Item> Siblings { get; set; } 
    }
}

Now that we have the parameter object defined, we need a class with methods that will compose our custom pipeline for grabbing sibling items in Sitecore. The following class does the trick:

using System.Linq;

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.GetSiblings
{
    public class GetSiblingsOperations
    {
        public void EnsureItem(GetSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (args.Item == null)
            {
                args.AbortPipeline();
            }
        }

        public void GetSiblings(GetSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Item, "args.Item");
            args.Siblings = (from sibling in args.Item.Parent.GetChildren()
                             where sibling.ID != args.Item.ID
                             select sibling).ToList();
        }
    }
}

The EnsureItem() method above just makes sure the item instance passed to it isn’t null, and aborts the pipeline if it is.

The GetSiblings() method gets all siblings items of the item — it just grabs all children of its parent, and excludes the item in question from the resulting collection using LINQ.

Now that we have a way to get sibling items, we need a way to delete them. I decided to build another custom pipeline to get sibling items for an item — by leveraging the pipeline created above — and delete them, and created the following parameter object for it:

using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings
{
    public class DeleteSiblingsArgs : ClientPipelineArgs
    {
        public Item Item { get; set; }

        public bool ShouldDelete { get; set; }

        public IEnumerable<Item> Siblings { get; set; } 
    }
}

The following class contains methods that will be used it our custom client pipeline to delete sibling items:

using System;
using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Pipelines.GetSiblings;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings
{
    public class DeleteSiblingsOperations
    {
        private string DeleteConfirmationMessage { get; set; }
        private string DeleteConfirmationWindowWidth { get; set; }
        private string DeleteConfirmationWindowHeight { get; set; }

        public void ConfirmDeleteAction(DeleteSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(DeleteConfirmationMessage, "DeleteConfirmationMessage");
            Assert.ArgumentNotNullOrEmpty(DeleteConfirmationWindowWidth, "DeleteConfirmationWindowWidth");
            Assert.ArgumentNotNullOrEmpty(DeleteConfirmationWindowHeight, "DeleteConfirmationWindowHeight");
            if (!args.IsPostBack)
            {
                SheerResponse.YesNoCancel(DeleteConfirmationMessage, DeleteConfirmationWindowWidth, DeleteConfirmationWindowHeight);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.ShouldDelete = AreEqualIgnoreCase(args.Result, "yes");
                args.IsPostBack = false;
            }
        }

        public void GetSiblingsIfConfirmed(DeleteSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!args.ShouldDelete)
            {
                args.AbortPipeline();
                return;
            }
            
            args.Siblings = GetSiblings(args.Item);
        }

        protected virtual IEnumerable<Item> GetSiblings(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            GetSiblingsArgs getSiblingsArgs = new GetSiblingsArgs { Item = item };
            CorePipeline.Run("getSiblings", getSiblingsArgs);
            return getSiblingsArgs.Siblings;
        }

        public void DeleteSiblings(DeleteSiblingsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Siblings, "args.Siblings");
            DeleteItems(args.Siblings);
        }

        protected virtual void DeleteItems(IEnumerable<Item> items)
        {
            Assert.ArgumentNotNull(items, "items");
            foreach (Item item in items)
            {
                item.Recycle();
            }
        }

        private static bool AreEqualIgnoreCase(string one, string two)
        {
            return string.Equals(one, two, StringComparison.CurrentCultureIgnoreCase);
        }
    }
}

The ConfirmDeleteAction() method — which is invoked first in the custom client pipeline — asks the user if he/she would like to delete all sibling items for the item in question.

The GetSiblingsIfConfirmed() method is then processed next in the pipeline sequence, and ascertains whether the user had clicked the ‘Yes’ button — this is a button in the YesNoCancel dialog that was presented to the user via the ConfirmDeleteAction() method.

If the user had clicked the ‘Yes’ button, sibling items are grabbed from Sitecore — this is done using the custom pipeline built above — and is set on Siblings property of the DeleteSiblingsArgs instance.

The DeleteSiblings() method is then processed next, and basically does as named: it deletes all items in the Siblings property of the DeleteSiblingsArgs instance.

Now that we are armed with our pipelines above, we need a way to call them from the Sitecore UI. The following command was built for that purpose:

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

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

using Sitecore.Sandbox.Pipelines.GetSiblings;
using Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings;

namespace Sitecore.Sandbox.Shell.Framework.Commands
{
    public class DeleteAllButThis : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            DeleteSiblings(context);
        }

        private static void DeleteSiblings(CommandContext context)
        {
            Context.ClientPage.Start("uiDeleteSiblings", new DeleteSiblingsArgs { Item = GetItem(context) });
        }

        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            if (!HasSiblings(GetItem(context)))
            {
                return CommandState.Hidden;
            }

            return CommandState.Enabled;
        }

        private static bool HasSiblings(Item item)
        {
            return GetSiblings(item).Any();
        }

        private static IEnumerable<Item> GetSiblings(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            GetSiblingsArgs getSiblingsArgs = new GetSiblingsArgs { Item = item };
            CorePipeline.Run("getSiblings", getSiblingsArgs);
            return getSiblingsArgs.Siblings;
        }

        private static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Items, "context.Items");
            return context.Items.FirstOrDefault();
        }
    }
}

The command above will only display if the context item has siblings — we invoke the pipeline defined towards the beginning of this post to get sibling items. If none are returned, the command is hidden.

When the command executes, we basically just pass the context item to our custom pipeline that deletes sibling items, and sit back and wait for them to be deleted (I just lean back, and put my feet up on my desk).

I then strung everything together using the following configuration include file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:DeleteAllButThis" type="Sitecore.Sandbox.Shell.Framework.Commands.DeleteAllButThis, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <getSiblings>
        <processor type="Sitecore.Sandbox.Pipelines.GetSiblings.GetSiblingsOperations, Sitecore.Sandbox" method="EnsureItem" />
        <processor type="Sitecore.Sandbox.Pipelines.GetSiblings.GetSiblingsOperations, Sitecore.Sandbox" method="GetSiblings" />
      </getSiblings>
    </pipelines>
    <processors>
      <uiDeleteSiblings>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings.DeleteSiblingsOperations, Sitecore.Sandbox" method="ConfirmDeleteAction">
          <DeleteConfirmationMessage>Are you sure you want to delete all sibling items and their descendants?</DeleteConfirmationMessage>
          <DeleteConfirmationWindowWidth>200</DeleteConfirmationWindowWidth>
          <DeleteConfirmationWindowHeight>200</DeleteConfirmationWindowHeight>
        </processor>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings.DeleteSiblingsOperations, Sitecore.Sandbox" method="GetSiblingsIfConfirmed"/>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.DeleteSiblings.DeleteSiblingsOperations, Sitecore.Sandbox" method="DeleteSiblings"/>
	    </uiDeleteSiblings>
    </processors>
  </sitecore>
</configuration>

I also had to wire the command above to the Sitecore UI by defining a context menu item in the core database. I’ve omitted how I’ve done this. If you would like to learn how to do this, check out my first and second posts on adding to the item context menu.

Let’s see this in action.

I first created some items to delete, and one item that I don’t want to delete:

stuff-to-delete

I then right-clicked on the item I don’t want to delete, and was presented with the new item context menu option:

delete-all-but-this-context-menu

I was then prompted with a confirmation dialog:

delete-all-but-this-confirmation-dialog

I clicked ‘Yes’, and then saw that all sibling items were deleted:

items-deleted

If you have any suggestions on making this better, please drop a comment.

Until next time, have a Sitecoretastic day!

Restart the Sitecore Client and Server Using Custom Pipelines

Last week Michael West asked me about creating shortcuts to restart the Sitecore client and server via this tweet, and I was immediately curious myself on what was needed to accomplish this.

If you are unfamiliar with restarting the Sitecore client and server, these are options that are presented to Sitecore users — typically developers — after installing a package into Sitecore:

install-package-end-wizard

Until last week, I never understood how either of these checkboxes worked, and uncovered the following code during my research:

restart-code-in-InstallPackageForm

Michael West had conveyed how he wished these two lines of code lived in pipelines, and that prompted me to write this article — basically encapsulate the logic above into two custom pipelines: one to restart the Sitecore client, and the other to restart the Sitecore server.

I decided to define the concept of a ‘Restarter’, an object that restarts something — this could be anything — and defined the following interface for such objects:

namespace Sitecore.Sandbox.Utilities.Restarters
{
    public interface IRestarter
    {
        void Restart();
    }
}

I then created the following IRestarter for the Sitecore client:

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Utilities.Restarters
{
    public class SitecoreClientRestarter : IRestarter
    {
        private ClientResponse ClientResponse { get; set; }

        public SitecoreClientRestarter()
            : this(Context.ClientPage)
        {
        }

        public SitecoreClientRestarter(ClientPage clientPage)
        {
            SetClientResponse(clientPage);
        }

        public SitecoreClientRestarter(ClientResponse clientResponse)
        {
            SetClientResponse(clientResponse);
        }

        private void SetClientResponse(ClientPage clientPage)
        {
            Assert.ArgumentNotNull(clientPage, "clientPage");
            SetClientResponse(clientPage.ClientResponse);
        }

        private void SetClientResponse(ClientResponse clientResponse)
        {
            Assert.ArgumentNotNull(clientResponse, "clientResponse");
            ClientResponse = clientResponse;
        }

        public void Restart()
        {
            ClientResponse.Broadcast(ClientResponse.SetLocation(string.Empty), "Shell");
        }
    }
}

The class above has three constructors. One constructor takes an instance of Sitecore.Web.UI.Sheer.ClientResponse — this lives in Sitecore.Kernel.dll — and another constructor takes in an instance of
Sitecore.Web.UI.Sheer.ClientPage — this also lives in Sitecore.Kernel.dll — which contains a property instance of Sitecore.Web.UI.Sheer.ClientResponse, and this instance is set on the ClientResponse property of the SitecoreClientRestarter class.

The third constructor — which is parameterless — calls the constructor that takes in a Sitecore.Web.UI.Sheer.ClientPage instance, and passes the ClientResponse instance set in Sitecore.Context.

I followed building the above class with another IRestarter — one that restarts the Sitecore server:

using Sitecore.Install;

namespace Sitecore.Sandbox.Utilities.Restarters
{
    public class SitecoreServerRestarter : IRestarter
    {
        public SitecoreServerRestarter()
        {
        }

        public void Restart()
        {
            Installer.RestartServer();
        }
    }
}

There really isn’t much happening in the class above. It just calls the static method RestartServer() — this method changes the timestamp on the Sitecore instance’s Web.config to trigger a web application restart — on Sitecore.Install.Installer in Sitecore.Kernel.dll.

Now we need to a way to use the IRestarter classes above. I built the following class to serve as a processor of custom pipelines I define later on in this post:

using Sitecore.Diagnostics;
using Sitecore.Pipelines;

using Sitecore.Sandbox.Utilities.Restarters;

namespace Sitecore.Sandbox.Pipelines.RestartRestarter
{
    public class RestartRestarterOperations
    {
        private IRestarter Restarter { get; set; }

        public void Process(PipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(Restarter, "Restarter");
            Restarter.Restart();
        }
    }
}

Through a pipeline processor configuration setting, we define the type of IRestarter — it’s magically created by Sitecore when the pipeline processor instance is created.

After some null checks, the Process() method invokes Restart() on the IRestarter instance, ultimately restarting whatever the IRestarter is set to restart.

I then needed a way to test the pipelines I define later on in this post. I built the following class to serve as commands that I added into the Sitecore ribbon:

using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Commands.Admin
{
    public class Restart : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Parameters, "context.Parameters");
            Assert.ArgumentNotNullOrEmpty(context.Parameters["pipeline"], "context.Parameters[\"pipeline\"]");
            CorePipeline.Run(context.Parameters["pipeline"], new PipelineArgs());
        }
    }
}

The command above expects a pipeline name to be supplied via the Parameters NameValueCollection instance set on the CommandContext instance passed to the Execute method() — I show later on in this post how I pass the name of the pipeline to the command.

If the pipeline name is given, we invoke the pipeline.

I then glued everything together using the following configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="admin:Restart" type="Sitecore.Sandbox.Commands.Admin.Restart, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <restartClient>
        <processor type="Sitecore.Sandbox.Pipelines.RestartRestarter.RestartRestarterOperations, Sitecore.Sandbox">
          <Restarter type="Sitecore.Sandbox.Utilities.Restarters.SitecoreClientRestarter, Sitecore.Sandbox"/>
        </processor>  
      </restartClient>
      <restartServer>
        <processor type="Sitecore.Sandbox.Pipelines.RestartRestarter.RestartRestarterOperations, Sitecore.Sandbox">
          <Restarter type="Sitecore.Sandbox.Utilities.Restarters.SitecoreServerRestarter, Sitecore.Sandbox"/>
        </processor>
      </restartServer>
    </pipelines>
  </sitecore>
</configuration>

I’m omitting how I created custom buttons in a custom ribbon in Sitecore to test this. If you want to learn about adding buttons to the Sitecore Ribbon, please read John West’s blog post on doing so.

However, I did do the following to pass the name of the pipeline we want to invoke in the custom command class defined above:

restart-client-button

I wish I could show you some screenshots on how this works. However, there really isn’t much visual to see here.

If you have any suggestions on how I could show this in action, or improve the code above, please share in a comment.

Choose Template Fields to Display in the Sitecore Content Editor

The other day I was going through search terms people had used to get to my blog, and discovered a few people made their way to my blog by searching for ‘sitecore hide sections in data template’.

I had built something like this in the past, but no longer remember how I had implemented that particular solution — not that I could show you that solution since it’s owned by a previous employer — and decided I would build another solution to accomplish this.

Before I move forward, I would like to point out that Sitecore MVP Andy Uzick wrote a blog post recently showing how to hide fields and sections in the content editor, though I did not have much luck with hiding sections in the way that he had done it — sections with no fields were still displaying for me in the content editor — and I decided to build a different solution altogether to make this work.

I thought it would be a good idea to let users turn this functionality on and off via a checkbox in the ribbon, and used some code from a previous post — in this post I had build an object to keep track of the state of a checkbox in the ribbon — as a model. In the spirit of that object, I defined the following interface:

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public interface IRegistrySettingToggle
    {
        bool IsOn();

        void TurnOn();

        void TurnOff();
    }
}

I then created the following abstract class which implements the interface above, and stores the state of the setting defined by the given key — the key of the setting and the “on” value are passed to it via a subclass — in the Sitecore registry.

using Sitecore.Diagnostics;

using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public abstract class RegistrySettingToggle : IRegistrySettingToggle
    {
        private string RegistrySettingKey { get; set; }

        private string RegistrySettingOnValue { get; set; }

        protected RegistrySettingToggle(string registrySettingKey, string registrySettingOnValue)
        {
            SetRegistrySettingKey(registrySettingKey);
            SetRegistrySettingOnValue(registrySettingOnValue);
        }

        private void SetRegistrySettingKey(string registrySettingKey)
        {
            Assert.ArgumentNotNullOrEmpty(registrySettingKey, "registrySettingKey");
            RegistrySettingKey = registrySettingKey;
        }

        private void SetRegistrySettingOnValue(string registrySettingOnValue)
        {
            Assert.ArgumentNotNullOrEmpty(registrySettingOnValue, "registrySettingOnValue");
            RegistrySettingOnValue = registrySettingOnValue;
        }

        public bool IsOn()
        {
            return Registry.GetString(RegistrySettingKey) == RegistrySettingOnValue;
        }

        public void TurnOn()
        {
            Registry.SetString(RegistrySettingKey, RegistrySettingOnValue);
        }

        public void TurnOff()
        {
            Registry.SetString(RegistrySettingKey, string.Empty);
        }
    }
}

I then built the following class to toggle the display settings for our displayable fields:

using System;

namespace Sitecore.Sandbox.Utilities.ClientSettings
{
    public class ShowDisplayableFieldsOnly : RegistrySettingToggle
    {
        private const string RegistrySettingKey = "/Current_User/Content Editor/Show Displayable Fields Only";
        private const string RegistrySettingOnValue = "on";

        private static volatile IRegistrySettingToggle current;
        private static object lockObject = new Object();

        public static IRegistrySettingToggle Current
        {
            get
            {
                if (current == null)
                {
                    lock (lockObject)
                    {
                        if (current == null)
                        {
                            current = new ShowDisplayableFieldsOnly();
                        }
                    }
                }

                return current;
            }
        }

        private ShowDisplayableFieldsOnly()
            : base(RegistrySettingKey, RegistrySettingOnValue)
        {
        }
    }
}

It passes its Sitecore registry key and “on” state value to the RegistrySettingToggle base class, and employs the Singleton pattern — I saw no reason for there to be multiple instances of this object floating around.

In order to use a checkbox in the ribbon, we have to create a new command for it:

using System.Linq;

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

using Sitecore.Sandbox.Utilities.ClientSettings;

namespace Sitecore.Sandbox.Commands
{
    public class ToggleDisplayableFieldsVisibility : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            ToggleDisplayableFields();
            Refresh(context);
        }

        private static void ToggleDisplayableFields()
        {
            IRegistrySettingToggle showDisplayableFieldsOnly = ShowDisplayableFieldsOnly.Current;
            if (!showDisplayableFieldsOnly.IsOn())
            {
                showDisplayableFieldsOnly.TurnOn();
            }
            else
            {
                showDisplayableFieldsOnly.TurnOff();
            }
        }

        public override CommandState QueryState(CommandContext context)
        {
            if (!ShowDisplayableFieldsOnly.Current.IsOn())
            {
                return CommandState.Enabled;
            }

            return CommandState.Down;
        }

        private static void Refresh(CommandContext context)
        {
            Refresh(GetItem(context));
        }

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

        private static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            return context.Items.FirstOrDefault();
        }
    }
}

The command above leverages the instance of the ShowDisplayableFieldsOnly class defined above to turn the displayable fields feature on and off.

I followed the creation of the command above with the definition of the ribbon checkbox in the core database:

show-displayable-fields-checkbox-defined

The command name above — which is set in the Click field — is defined in the patch configuration file towards the end of this post.

I then created the following data template with a TreelistEx field to store the displayable fields:

displayable-fields-template

The TreelistEx field above will pull in all sections and their fields into the TreelistEx dialog, but only allow the selection of template fields, as is dictated by the following parameters that I have mapped in its Source field:

DataSource=/sitecore/templates/Sample/Sample Item&IncludeTemplatesForSelection=Template field&IncludeTemplatesForDisplay=Template section,Template field&AllowMultipleSelection=no

I then set this as a base template in my sandbox solution’s Sample Item template:

set-displayable-fields-template-as-base

In order to remove fields, we need a <getContentEditorFields> pipeline processor. I built the following class for to serve as one:

using System;

using Sitecore.Configuration;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Data.Templates;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields;

using Sitecore.Sandbox.Utilities.ClientSettings;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields
{
    public class RemoveUndisplayableFields
    {
        public void Process(GetContentEditorFieldsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Item, "args.Item");
            Assert.ArgumentCondition(!string.IsNullOrWhiteSpace(DisplayableFieldsFieldName), "DisplayableFieldsFieldName", "DisplayableFieldsFieldName must be set in the configuration file!");
            if (!ShowDisplayableFieldsOnly.Current.IsOn())
            {
                return;
            }

            foreach (Editor.Section section in args.Sections)
            {
                AddDisplayableFields(args.Item[DisplayableFieldsFieldName], section);
            }
        }

        private void AddDisplayableFields(string displayableFieldIds, Editor.Section section)
        {
            Editor.Fields displayableFields = new Editor.Fields();
            foreach (Editor.Field field in section.Fields)
            {
                if (IsDisplayableField(displayableFieldIds, field))
                {
                    displayableFields.Add(field);
                }
            }

            section.Fields.Clear();
            section.Fields.AddRange(displayableFields);
        }

        private bool IsDisplayableField(string displayableFieldIds, Editor.Field field)
        {
            if (IsStandardValues(field.ItemField.Item))
            {
                return true;
            }

            if (IsDisplayableFieldsField(field.ItemField))
            {
                return false;
            }

            return IsStandardTemplateField(field.ItemField)
                    || string.IsNullOrWhiteSpace(displayableFieldIds)
                    || displayableFieldIds.Contains(field.ItemField.ID.ToString());
        }
        
        private bool IsDisplayableFieldsField(Field field)
        {
            return string.Equals(field.Name, DisplayableFieldsFieldName, StringComparison.CurrentCultureIgnoreCase);
        }

        private static bool IsStandardValues(Item item)
        {
            if (item.Template.StandardValues != null)
            {
                return item.Template.StandardValues.ID == item.ID;
            }

            return false;
        }

        private bool IsStandardTemplateField(Field field)
        {
            Assert.IsNotNull(StandardTemplate, "The Stardard Template could not be found.");
            return StandardTemplate.ContainsField(field.ID);
        }

        private static Template GetStandardTemplate()
        {
            return TemplateManager.GetTemplate(Settings.DefaultBaseTemplate, Context.ContentDatabase);
        }

        private Template _StandardTemplate;
        private Template StandardTemplate
        {
            get
            {
                if (_StandardTemplate == null)
                {
                    _StandardTemplate = GetStandardTemplate();
                }

                return _StandardTemplate;
            }
        }

        private string DisplayableFieldsFieldName { get; set; }
    }
}

The class above iterates over all fields for the supplied item, and adds only those that were selected in the Displayable Fields TreelistEx field, and also Standard Fields — we don’t want to remove these since they are shown/hidden by the Standard Fields feature in Sitecore — to a new Editor.Fields collection. This new collection is then set on the GetContentEditorFieldsArgs instance.

Plus, we don’t want to show the Displayable Fields TreelistEx field when the feature is turned on, and we are on an item. This field should only display when we are on the standard values item when the feature is turned on — this is how we will choose our displayable fields.

Now we have to handle sections without fields — especially after ripping them out via the pipeline processor above.

I built the following class to serve as a <renderContentEditor> pipeline processor to do this:

using System.Linq;

using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor
{
    public class FilterSectionsWithFields
    {
        public void Process(RenderContentEditorArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.Sections = GetSectionsWithFields(args.Sections);
        }

        private static Editor.Sections GetSectionsWithFields(Editor.Sections sections)
        {
            Assert.ArgumentNotNull(sections, "sections");
            Editor.Sections sectionsWithFields = new Editor.Sections();
            foreach (Editor.Section section in sections)
            {
                AddIfContainsFields(sectionsWithFields, section);
            }

            return sectionsWithFields;
        }

        private static void AddIfContainsFields(Editor.Sections sections, Editor.Section section)
        {
            Assert.ArgumentNotNull(sections, "sections");
            Assert.ArgumentNotNull(section, "section");
            if (!ContainsFields(section))
            {
                return;
            }

            sections.Add(section);
        }

        private static bool ContainsFields(Editor.Section section)
        {
            Assert.ArgumentNotNull(section, "section");
            return section.Fields != null && section.Fields.Any();
        }
    }
}

It basically builds a new collection of sections that contain at least one field, and sets it on the RenderContentEditorArgs instance being passed through the <renderContentEditor> pipeline.

In order for this to work, we must make this pipeline processor run before all other processors.

I tied all of the above together with the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="contenteditor:ToggleDisplayableFieldsVisibility" type="Sitecore.Sandbox.Commands.ToggleDisplayableFieldsVisibility, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <getContentEditorFields>
        <processor type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields.RemoveUndisplayableFields, Sitecore.Sandbox">
          <DisplayableFieldsFieldName>Displayable Fields</DisplayableFieldsFieldName>
        </processor>  
      </getContentEditorFields>
      <renderContentEditor>
        <processor patch:before="processor[@type='Sitecore.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.RenderSkinedContentEditor, Sitecore.Client']"
                   type="Sitecore.Sandbox.Shell.Applications.ContentEditor.Pipelines.RenderContentEditor.FilterSectionsWithFields, Sitecore.Sandbox"/>
      </renderContentEditor>
    </pipelines>
  </sitecore>
</configuration>

Let’s try this out.

I navigated to the standard values item for my Sample Item data template, and selected the fields I want to display in the content editor:

selected-some-fields

I then went to an item that uses this data template, and turned on the displayable fields feature:

displayable-fields-on

As you can see, only the fields we had chosen display — along with standard fields since the Standard Fields checkbox is checked.

I then turned off the displayable fields feature:

displayable-fields-off

Now all fields for the item display.

I then turned the displayable fields feature back on, and turned off Standard Fields:

standard-fields-off-displayable-fields-on

Now only our selected fields display.

If you have any thoughts on this, or ideas around making this better, please share in a comment.

Expand Tokens on Sitecore Items Using a Custom Command in Sitecore PowerShell Extensions

During my Sitecore from the Command Line presentation at the Sitecore User Group – New England, I had shown attendees how they could go about adding a custom command into the Sitecore PowerShell Extensions module.

This blog post shows what I had presented — although the code in this post is an improved version over what I had presented at my talk. Many thanks to Sitecore MVP Adam Najmanowicz for helping me make this code better!

The following command will expand “out of the box” tokens in all fields of a supplied Sitecore item — check out Expand Tokens on Sitecore Items Using a Custom Command in Revolver where I discuss the problem commands like this address, and this article by Sitecore MVP Jens Mikkelsen which lists “out of the box” tokens available in Sitecore:

using System;
using System.Management.Automation;

using Sitecore.Configuration;

using Sitecore.Data;
using Sitecore.Data.Items;

using Cognifide.PowerShell.PowerShellIntegrations.Commandlets;

namespace CommandLineExtensions.PowerShell.Commandlets
{
    [Cmdlet("Expand", "Token")]
    [OutputType(new[] { typeof(Item) })]
    public class ExpandTokenCommand : BaseCommand
    {
        private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer();

        [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]
        public Item Item { get; set; }

        protected override void ProcessRecord()
        {
            Item.Editing.BeginEdit();
            try
            {
                TokenReplacer.ReplaceItem(Item);
                Item.Editing.EndEdit();
            }
            catch (Exception ex)
            {
                Item.Editing.CancelEdit();
                throw ex;
            }

            WriteItem(Item);
        }
    }
}

The command above subclasses Cognifide.PowerShell.PowerShellIntegrations.Commandlets.BaseCommand — the base class for most (if not all) commands in Sitecore PowerShell Extensions.

An item is passed to the command via a parameter, and is magically set on the Item property of the command class instance.

The ValueFromPipeline parameter being set to “true” on the Item property’s Parameter attribute will allow for chaining of this command with others so that items can be fed into it via a pipe bridging the commands together in PowerShell.

An instance of the Sitecore.Data.MasterVariablesReplacer class — which is created by the GetMasterVariablesReplacer() method of the Sitecore.Configuration.Factory class based on the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — is used to expand tokens on the supplied Sitecore item after the item was flagged for editing.

Once tokens have been expanded on the item — or not in the event an exception is encountered — the item is written to the Results window via the WriteItem method which is defined in the BaseCommand class.

I then had to wire up the custom command via a patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
		    <add Name="Custom Commandlets" type="*, CommandLineExtensions" />
      </commandlets>
    </powershell>
  </sitecore>                                                                                     
</configuration>

Let’s take this custom command for a spin.

I created a bunch of test items, and set tokens in their fields. I then selected the following page at random for testing:

page-one-unexpanded-tokens

I opened up the Integrated Scripting Environment of Sitecore PowerShell Extensions, typed in the following PowerShell code, and executed by pressing Ctrl-E:

spe-ise-expand-tokens-page-one

As you can see tokens were expanded on the Page One item:

page-one-expanded-tokens

How about expanding tokens on all descendants of the Home item? Let’s see an example of how we can do that.

I chose the following content item — a grandchild of the Home item — for testing:

inner-page-one-unexpanded-tokens

I switched back over to the Integrated Scripting Environment, wrote the following code for testing — the Get-ChildItem command with the -r parameter (this means do this recursively) will grab all descendants of the Home item, and pipe each item in the result set into the Expand-Token command — and clicked the Execute button:

spe-ise-expand-on-descendants

I then went back to the grandchild item of the Home page in the content tree, and saw that tokens were expanded in its fields:

spe-expanded-on-descendants

If you have any thoughts or comments on this, or ideas for new commands in Sitecore PowerShell Extensions, please share in a comment.

Until next time, have a scriptolicious day!

Expand Tokens on Sitecore Items Using a Custom Command in Revolver

On September 18, 2013, I presented Sitecore from the Command Line at the Sitecore User Group – New England.

During my presentation, I gave an example of creating a custom command in Revolver — the first scripting platform for Sitecore built by Alistair Deneys — and thought I would write something up for those who had missed the presentation, or wanted to revisit what I had shown.

One thing that plagues some Sitecore developers — if you disagree please leave a comment — is not having a nice way to expand tokens on items when tokens are added to Standard Values after items had been created previously.

Newly added tokens “bleed” into preexisting items’ fields, and I’ve seen developers perform crazy feats of acrobatic gymnastics to expand them — writing a standalone web form to recursive crawl the content tree to expand these is such an example (take a look at Empower Your Content Authors to Expand Standard Values Tokens in the Sitecore Client where I offer an alternative way to expand tokens on content items).

The following custom Revolver command will expand tokens on a supplied Sitecore item, and help out on the front of expanding newly added tokens on preexisting items:

using System;
using Sitecore.Configuration;
using System.Linq;

using Sitecore.Data;
using Sitecore.Data.Items;

using Revolver.Core;
using Revolver.Core.Commands;

namespace CommandLineExtensions.Revolver.Commands
{
    public class ExpandTokensCommand : BaseCommand
    {
        private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer();

        public override string Description()
        {
            return "Expand tokens on an item";
        }

        public override HelpDetails Help()
        {
            HelpDetails details = new HelpDetails
            {
                Description = Description(),
                Usage = "<cmd> [path]"
            };
            
            details.AddExample("<cmd>");
            details.AddExample("<cmd> /item1/item2");
            return details;
        }

        public override CommandResult Run(string[] args)
        {
            string path = string.Empty;
            if (args.Any())
            {
                path = args.FirstOrDefault();
            }

            using (new ContextSwitcher(Context, path))
            {
                if (!Context.LastGoodPath.EndsWith(path, StringComparison.CurrentCultureIgnoreCase))
                {
                    return new CommandResult
                    (
                        CommandStatus.Failure, 
                        string.Format("Failed to expand tokens on item {0}\nReason:\n\n An item does not exist at that location!", path)
                    );
                }

                CommandResult result;
                Item item = Context.CurrentItem;
                item.Editing.BeginEdit();
                try
                {
                    TokenReplacer.ReplaceItem(item);
                    result = new CommandResult(CommandStatus.Success, string.Concat("Expanded tokens on item ", Context.LastGoodPath));
                    item.Editing.EndEdit();
                }
                catch (Exception ex)
                {
                    item.Editing.CancelEdit();
                    result = new CommandResult(CommandStatus.Failure, string.Format("Failed to expand tokens on item {0}\nReason:\n\n{1}", path, ex));
                }

                return result;
            }
        }
    }
}

Tokens are expanded using an instance of the Sitecore.Data.MasterVariablesReplacer class — you can roll your own, and wire it up in the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — which is provided by Sitecore.Configuration.Factory.GetMasterVariablesReplacer().

All custom commands in Revolver must implement the Revolver.Core.ICommand interface. I subclassed Revolver.Core.Commands.BaseCommand — which does implement this interface — since it seemed like the right thing to do given that all “out of the box” commands I saw in Revolver were subclassing it, and then implemented the Description(), Help() and Run() abstract methods.

I then had to bind the custom command to a new name — I chose “et” for “Expand Tokens”:

@echooff
@stoponerror

bind CommandLineExtensions.Revolver.Commands.ExpandTokensCommand,CommandLineExtensions et

@echoon

Since it wouldn’t be efficient to type and run this bind script every time I want to use the “et” command, I added it into a startup script in the core database:

bind-custom-commands-script

I then had to create a user script for the startup script to run. I chose the Everyone role here for demonstration purposes:

bind-custom-commands-user-script

The above startup script will be invoked when Revolver is opened, and our custom command will be bound.

Let’s see all of the above in action.

I added some tokens in my home item:

home-item-unexpanded-tokens

I then opened up Revolver, navigated to /sitecore/content, and ran the custom command on the home item:

ran-et-revolver

As you can see the tokens were expanded:

home-item-expanded-tokens

You might be thinking “that’s wonderful Mike — except now I have to navigate to every item in my content tree using Revolver, and then run this custom command on it”.

Well, I do have a solution for this: a custom script that grabs an item and all of its descendants using a Sitecore query, and passes them to the custom command to expand tokens:

@echooff
@stoponerror

if ($1$ = \$1\$) (exit (Missing required parameter path))

@echoon

query -ns $1$/descendant-or-self::* et

I put this script in the core database, and named it “etr” for “Expand Tokens Recursively”:

etr-script

I navigated to a descendant of /sitecore/content/home, and see that it has some unexpanded tokens on it:

descendant-unexpanded-tokens

I went back to Revolver, and ran the “etr” command on the home item:

etr-revolver

As you can see tokens were expanded on the descendant item:

descendant-expanded-tokens

If you have any thoughts on this, or have ideas for other custom commands in Revolver, please share in a comment.

Navigate to Base Templates of a Template using a Sitecore Command

Have you ever said to yourself when looking at base templates of a template in its Content tab “wouldn’t it be great if I could easily navigate to one of these?”

the-problem-1

I have had this thought more than once despite having the ability to do this in a template’s Inheritance tab — you can do this by clicking one of the base template links listed:

inheritance-tab

For some reason I sometimes forget you have the ability to get to a base template of a template in the Inheritance tab — why I forget is no doubt a larger issue I should try to tackle, albeit I’ll leave that for another day — and decided to build something that will be more difficult for me to forget: launching a dialog via a new item context menu option, and selecting one of the base templates of a template in that dialog.

I decided to atomize functionality in my solution by building custom pipelines/processors wherever I felt doing so made sense.

I started off by building a custom pipeline that gets base templates for a template, and defined a data transfer object (DTO) class for it:

using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Pipelines;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GetBaseTemplatesArgs : PipelineArgs
    {
        public TemplateItem TemplateItem { get; set; }

        public bool IncludeAncestorBaseTemplates { get; set; }

        private List<TemplateItem> _BaseTemplates;
        public List<TemplateItem> BaseTemplates 
        {
            get
            {
                if (_BaseTemplates == null)
                {
                    _BaseTemplates = new List<TemplateItem>();
                }

                return _BaseTemplates;
            }
            set
            {
                _BaseTemplates = value;
            }
        }
    }
}

Client code must supply the template item that will be used as the starting point for gathering base templates, and can request all ancestor base templates — excluding the Standard Template as you will see below — by setting the IncludeAncestorBaseTemplates property to true.

I then created a class with a Process method that will serve as the only pipeline processor for my new pipeline:

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

using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GetBaseTemplates
    {
        public void Process(GetBaseTemplatesArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.TemplateItem, "args.TemplateItem");
            List<TemplateItem> baseTemplates = new List<TemplateItem>();
            GatherBaseTemplateItems(baseTemplates, args.TemplateItem, args.IncludeAncestorBaseTemplates);
            args.BaseTemplates = baseTemplates;
        }

        private static void GatherBaseTemplateItems(List<TemplateItem> baseTemplates, TemplateItem templateItem, bool includeAncestors)
        {
            if (includeAncestors)
            {
                foreach (TemplateItem baseTemplateItem in templateItem.BaseTemplates)
                {
                    GatherBaseTemplateItems(baseTemplates, baseTemplateItem, includeAncestors);
                }
            }

            if (!IsStandardTemplate(templateItem) && templateItem.BaseTemplates != null && templateItem.BaseTemplates.Any())
            {
                baseTemplates.AddRange(GetBaseTemplatesExcludeStandardTemplate(templateItem.BaseTemplates));
            }
        }

        private static IEnumerable<TemplateItem> GetBaseTemplatesExcludeStandardTemplate(TemplateItem templateItem)
        {
            if (templateItem == null)
            {
                return new List<TemplateItem>();
            }

            return GetBaseTemplatesExcludeStandardTemplate(templateItem.BaseTemplates);
        }

        private static IEnumerable<TemplateItem> GetBaseTemplatesExcludeStandardTemplate(IEnumerable<TemplateItem> baseTemplates)
        {
            if (baseTemplates != null && baseTemplates.Any())
            {
                return baseTemplates.Where(baseTemplate => !IsStandardTemplate(baseTemplate));
            }

            return baseTemplates;
        }

        private static bool IsStandardTemplate(TemplateItem templateItem)
        {
            return templateItem.ID == TemplateIDs.StandardTemplate;
        }
    }
}

Methods in the above class add base templates to a list when the templates are not the Standard Template — I thought it would be a rare occurrence for one to navigate to it, and decided not to include it in the collection.

Further, the method that gathers base templates is recursively executed when client code requests all ancestor base templates be include in the collection.

The next thing I built was functionality to prompt the user for a base template via a dialog, and track which base template was chosen. I decided to do this using a custom client processor, and built the following DTO for it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Web.UI.Sheer;
using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GotoBaseTemplateArgs : ClientPipelineArgs
    {
        public TemplateItem TemplateItem { get; set; }

        public string SelectedBaseTemplateId { get; set; }
    }
}

Just like the other DTO defined above, client code must suppy a template item. The SelectedBaseTemplateId property is set after a user selects a base template in the modal launched by the following class:

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

using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Applications.Dialogs.ItemLister;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GotoBaseTemplate
    {
        public string SelectTemplateButtonText { get; set; }

        public string ModalIcon { get; set; }

        public string ModalTitle { get; set; }

        public string ModalInstructions { get; set; }

        public void SelectBaseTemplate(GotoBaseTemplateArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.TemplateItem, "args.TemplateItem");
            Assert.ArgumentNotNullOrEmpty(SelectTemplateButtonText, "SelectTemplateButtonText");
            Assert.ArgumentNotNullOrEmpty(ModalIcon, "ModalIcon");
            Assert.ArgumentNotNullOrEmpty(ModalTitle, "ModalTitle");
            Assert.ArgumentNotNullOrEmpty(ModalInstructions, "ModalInstructions");
            
            if (!args.IsPostBack)
            {
                ItemListerOptions itemListerOptions = new ItemListerOptions
                {
                    ButtonText = SelectTemplateButtonText,
                    Icon = ModalIcon,
                    Title = ModalTitle,
                    Text = ModalInstructions
                };

                itemListerOptions.Items = GetBaseTemplateItemsForSelection(args.TemplateItem).Select(template => template.InnerItem).ToList();
                itemListerOptions.AddTemplate(TemplateIDs.Template);
                SheerResponse.ShowModalDialog(itemListerOptions.ToUrlString().ToString(), true);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.SelectedBaseTemplateId = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                args.AbortPipeline();
            }
        }

        private IEnumerable<TemplateItem> GetBaseTemplateItemsForSelection(TemplateItem templateItem)
        {
            GetBaseTemplatesArgs args = new GetBaseTemplatesArgs
            {
                TemplateItem = templateItem,
                IncludeAncestorBaseTemplates = true,
            };
            CorePipeline.Run("getBaseTemplates", args);
            return args.BaseTemplates;
        }

        public void Execute(GotoBaseTemplateArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(args.SelectedBaseTemplateId, "args.SelectedBaseTemplateId");
            Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", args.SelectedBaseTemplateId), 1);
        }
    }
}

The SelectBaseTemplate method above gives the user a list of base templates to choose from — this includes all ancestor base templates of a template minus the Standard Template.

The title, icon, helper text of the modal are supplied via the processor’s xml node in its configuration file — you’ll see this later on in this post.

Once a base template is chosen, its Id is then set in the SelectedBaseTemplateId property of the GotoBaseTemplateArgs instance.

The Execute method brings the user to the selected base template item in the Sitecore content tree.

Now we need a way to launch the code above.

I did this using a custom command that will be wired up to the item context menu:

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

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

using Sitecore.Sandbox.Shell.Framework.Pipelines;
using Sitecore.Web.UI.Sheer;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Commands
{
    public class GotoBaseTemplateCommand : Command
    {
        public override void Execute(CommandContext context)
        {
            Context.ClientPage.Start("gotoBaseTemplate", new GotoBaseTemplateArgs { TemplateItem = GetItem(context) });
        }

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

            return CommandState.Hidden;
        }

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

        private static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Items, "context.Items");
            return context.Items.FirstOrDefault();
        }

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

        private static IEnumerable<TemplateItem> GetBaseTemplates(TemplateItem templateItem)
        {
            Assert.ArgumentNotNull(templateItem, "templateItem");
            GetBaseTemplatesArgs args = new GetBaseTemplatesArgs 
            { 
                TemplateItem = templateItem, 
                IncludeAncestorBaseTemplates = false 
            };

            CorePipeline.Run("getBaseTemplates", args);
            return args.BaseTemplates;
        }
    }
}

The command above is visible only when the item is a template, and has base templates on it — we invoke the custom pipeline built above to get base templates.

When the command is invoked, we call our custom client processor to prompt the user for a base template to go to.

I then glued everything together using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <sitecore>
    <commands>
      <command name="item:GotoBaseTemplate" type="Sitecore.Sandbox.Commands.GotoBaseTemplateCommand, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <getBaseTemplates>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.GetBaseTemplates, Sitecore.Sandbox"/>
      </getBaseTemplates>
    </pipelines>
    <processors>
      <gotoBaseTemplate>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.GotoBaseTemplate, Sitecore.Sandbox" method="SelectBaseTemplate">
          <SelectTemplateButtonText>OK</SelectTemplateButtonText>
          <ModalIcon>Applications/32x32/nav_up_right_blue.png</ModalIcon>
          <ModalTitle>Select A Base Template</ModalTitle>
          <ModalInstructions>Select the base template you want to navigate to.</ModalInstructions>
        </processor>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.GotoBaseTemplate, Sitecore.Sandbox" method="Execute"/>
      </gotoBaseTemplate>
    </processors>
  </sitecore>
</configuration>

I’ve left out how I’ve added the command shown above to the item context menu in the core database. For more information on adding to the item context menu, please see part one and part two of my post showing how to do this.

Let’s see how we did.

I first created some templates for testing. The following template named ‘Meta’ uses two other test templates as base templates:

meta-template

I also created a ‘Base Page’ template which uses the ‘Meta’ template above:

base-page-template

Next I created ‘The Coolest Page Template Ever’ template — this uses the ‘Base Page’ template as its base template:

the-coolest-page-template-ever-template

I then right-clicked on ‘The Coolest Page Template Ever’ template to launch its context menu, and selected our new menu option:

context-menu-go-to-base-template

I was then presented with a dialog asking me to select the base template I want to navigate to:

base-template-lister-modal-1

I chose one of the base templates, and clicked ‘OK’:

base-template-lister-modal-2

I was then brought to the base template I had chosen:

brought-to-selected-base-template

If you have any thoughts on this, please leave a comment.

Delete An Item Across Multiple Databases in Sitecore

Have you ever thought “wouldn’t it be handy to have the ability to delete an item across multiple databases in Sitecore?” In other words, wouldn’t it be nice to not have to publish the parent of an item — with sub-items — after deleting it, just to remove it from a target database?

This particular thought has crossed my mind more than once, and I decided to do something about it. This post showcases what I’ve done.

I spent some time surfing through Sitecore.Kernel.dll and Sitecore.Client.dll in search of a dialog that allows users to select multiple options simultaneously but came up shorthanded — if you are aware of one, please leave a comment — so I had to roll my own:

<?xml version="1.0" encoding="utf-8" ?> 
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
	<DeleteInDatabases>
		<FormDialog ID="DeleteInDatabasesDialog" Icon="Business/32x32/data_delete.png" Header="Delete Item In Databases" 
		  Text="Select the databases where you want to delete the item." OKButton="Delete">
		  
		  <CodeBeside Type="Sitecore.Sandbox.Shell.Applications.Dialogs.DeleteInDatabasesForm,Sitecore.Sandbox"/>
		  <GridPanel Width="100%" Height="100%" Style="table-layout:fixed">
			<Border Padding="4" ID="Databases"/>
		  </GridPanel>
		</FormDialog>
	</DeleteInDatabases>
</control>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
using System.Web.UI.HtmlControls;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Web;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Pages;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Shell.Applications.Dialogs
{
    public class DeleteInDatabasesForm : DialogForm
    {
        private const string DatabaseCheckboxIDPrefix = "db_";

        protected Border Databases;

        private string _ItemId;
        protected string ItemId
        {
            get
            {
                if (string.IsNullOrWhiteSpace(_ItemId))
                {
                    _ItemId = WebUtil.GetQueryString("id");
                }

                return _ItemId;
            }
        }

        protected override void OnLoad(EventArgs e)
        {
            AddDatabaseCheckboxes();
            base.OnLoad(e);
        }

        private void AddDatabaseCheckboxes()
        {
            Databases.Controls.Clear();
            foreach (string database in GetDatabasesForSelection())
            {
                HtmlGenericControl checkbox = new HtmlGenericControl("input");
                Databases.Controls.Add(checkbox);
                checkbox.Attributes["type"] = "checkbox";
                checkbox.Attributes["value"] = database;
                string checkboxId = string.Concat(DatabaseCheckboxIDPrefix, database);
                checkbox.ID = checkboxId;
                HtmlGenericControl label = new HtmlGenericControl("label");
                Databases.Controls.Add(label);
                label.Attributes["for"] = checkboxId;
                label.InnerText = database;
                Databases.Controls.Add(new LiteralControl("<br>"));
            }
        }

        private static IEnumerable<string> GetDatabasesForSelection()
        {
            return WebUtil.GetQueryString("db").Split("|".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
        }

        protected override void OnOK(object sender, EventArgs args)
        {
            IEnumerable<string> selectedDatabases = GetSelectedDabases();
            if (!selectedDatabases.Any())
            {
                SheerResponse.Alert("Please select at least one database!");
                return;
            }

            DeleteItemInDatabases(selectedDatabases, ItemId);
            SheerResponse.Alert("The item has been deleted in all selected databases!");
            base.OnOK(sender, args);
        }

        private static IEnumerable<string> GetSelectedDabases()
        {
            IList<string> databases = new List<string>();
            foreach (string id in Context.ClientPage.ClientRequest.Form.Keys)
            {
                if (!string.IsNullOrWhiteSpace(id) && id.StartsWith(DatabaseCheckboxIDPrefix))
                {
                    databases.Add(id.Substring(3));
                }
            }

            return databases;
        }

        private static void DeleteItemInDatabases(IEnumerable<string> databases, string itemId)
        {
            foreach(string database in databases)
            {
                DeleteItemInDatabase(database, itemId);
            }
        }

        private static void DeleteItemInDatabase(string databaseName, string itemId)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            Assert.ArgumentNotNullOrEmpty(itemId, "itemId");
            Database database = Factory.GetDatabase(databaseName);
            Assert.IsNotNull(database, "Invalid database!");
            DeleteItem(database.GetItem(itemId));
        }

        private static void DeleteItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (Settings.RecycleBinActive)
            {
                item.Recycle();
            }
            else
            {
                item.Delete();
            }
        }
    }
}

The dialog above takes in an item’s ID — this is the ID of the item the user has chosen to delete across multiple databases — and a list of databases a user can choose from as checkboxes.

Ideally the item should exist in each database, albeit the code will throw an exception via an assertion in the case when client code supplies a database, the user selects it, and the item does not live in it.

If the user does not check off one checkbox, and clicks the ‘Delete’ button, an ‘Alert’ box will let the user know s/he must select at least one database.

When databases are selected, and the ‘Delete’ button is clicked, the item will be deleted — or put into the Recycle Bin — in all selected databases.

Now we need a way to launch this dialog. I figured it would make sense to have it be available from the item context menu — just as the ‘Delete’ menu option is available there “out of the box” — and built the following command for it:

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

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Text;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Commands
{
    public class DeleteInDatabases : Command
    {
        public override void Execute(CommandContext commandContext)
        {
            Context.ClientPage.Start(this, "ShowDialog", CreateNewClientPipelineArgs(GetItem(commandContext)));
        }

        private static ClientPipelineArgs CreateNewClientPipelineArgs(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            ClientPipelineArgs args = new ClientPipelineArgs();
            args.Parameters["ItemId"] = item.ID.ToString();
            args.Parameters["ParentId"] = item.ParentID.ToString();
            return args;
        }

        private void ShowDialog(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                SheerResponse.ShowModalDialog
                (
                    GetDialogUrl
                    (
                        GetDatabasesForItem(args.Parameters["ItemId"]), 
                        args.Parameters["ItemId"]
                    ),
                    "300px",
                    "500px",
                    string.Empty,
                    true
               );

               args.WaitForPostBack();
            }
            else
            {
                RefreshChildren(args.Parameters["ParentId"]);
            }
        }

        private void RefreshChildren(string parentId)
        {
            Assert.ArgumentNotNullOrEmpty(parentId, "parentId");
            Context.ClientPage.SendMessage(this, string.Format("item:refreshchildren(id={0})", parentId));
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            bool shouldEnable = Context.User.IsAdministrator
                                && IsInDatabasesOtherThanCurrentContent(GetItem(commandContext));

            if (shouldEnable)
            {
                return CommandState.Enabled;
            }

            return CommandState.Hidden;
        }

        private static bool IsInDatabasesOtherThanCurrentContent(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return GetDatabasesForItem(item.ID.ToString()).Count() > 1;
        }
        private static Item GetItem(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items");
            return commandContext.Items.FirstOrDefault();
        }

        private static IEnumerable<string> GetDatabasesForItemExcludingContentDB(string id)
        {
            return GetDatabasesForItem(id).Where(db => string.Equals(db, Context.ContentDatabase.Name, StringComparison.CurrentCultureIgnoreCase));
        }

        private static IEnumerable<string> GetDatabasesForItem(string id)
        {
            Assert.ArgumentNotNullOrEmpty(id, "id");
            return (from database in Factory.GetDatabases()
                    let itemInDatabase = database.GetItem(id)
                    where itemInDatabase != null
                    select database.Name).ToList();
        }

        private static string GetDialogUrl(IEnumerable<string> databases, string id)
        {
            Assert.ArgumentNotNullOrEmpty(id, "id");
            Assert.ArgumentNotNull(databases, "databases");
            Assert.ArgumentCondition(databases.Any(), "databases", "At least one database should be supplied!");
            UrlString urlString = new UrlString(UIUtil.GetUri("control:DeleteInDatabases"));
            urlString.Append("id", id);
            urlString.Append("db", string.Join("|", databases));
            return urlString.ToString();
        }
    }
}

The command is only visible when the item is in another database other than the context content database and the user is an admin.

When the item context menu option is clicked, the command passes a pipe delimited list of database names — only databases that contain the item — and the item’s ID to the dialog through its query string.

Once the item is deleted via the dialog, control is returned back to the command, and it then refreshes all siblings of the deleted item — this is done so the deleted item is removed from the content tree if the context content database was chosen in the dialog.

I then made this command available in Sitecore using a configuration include file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:DeleteInDatabases" type="Sitecore.Sandbox.Commands.DeleteInDatabases,Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

I’ve omitted the step on how I’ve wired this up to the item context menu in the core database. For more information on adding to the item context menu, please see part one and part two of my post showing how to do this.

Let’s see this in action.

I navigated to a test item that lives in the master and web databases, and launched its item context menu:

context-menu-delete-in-dbs

I clicked the ‘Delete in Databases’ menu option, and was presented with this dialog:

delete-in-db-1

I got excited and forgot to select a database before clicking the ‘Delete’ button:

delete-in-db-2

I then selected all databases, and clicked ‘Delete’:

delete-in-db-3

When the dialog closed, we can see that our test item is gone:

item-vanished

Rest assured, it’s in the Recycle Bin:

delete-in-db-4

It was also deleted in the web database as well — I’ve omitted screenshots of this since they would be identical to the last two screenshots above.

If you have any thoughts on this, or recommendations on making it better, please share in a comment.

Until next time, have a Sitecoretastic day!

Replace Proxies With Clones in the Sitecore CMS

The other day I stumbled upon a thread in the Sitecore Developer Network (SDN) forums that briefly touched upon replacing proxies with clones, and I wondered whether anyone had built any sort of tool that creates clones for items being proxied — basically a tool that would automate creating clones from proxies — and removes the proxies once the clones are in place.

Since I am not aware of such a tool — not to mention that I’m hooked on programming and just love coding — I decided to create one.

The following command is my attempt at such a tool:

using System;
using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Data.Proxies;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Commands
{
    public class TransformProxyToClones : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            TransformProxyItemToClones(GetContextItem(context));
        }

        private static void TransformProxyItemToClones(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(!CanTransform(item))
            {
                return;
            }

            string proxyType = GetProxyType(item);
            Item source = GetItem(GetSourceItemFieldValue(item));
            Item target = GetItem(GetTargetItemFieldValue(item));
            
            if (AreEqualIgnoreCase(proxyType, "Entire sub-tree"))
            {
                DeleteItem(item);
                CloneEntireSubtree(source, target);
            }
            else if (AreEqualIgnoreCase(proxyType, "Root item only"))
            {
                DeleteItem(item);
                CloneRootOnly(source, target);
            }
        }

        private static void CloneEntireSubtree(Item source, Item destination)
        {
            Clone(source, destination, true);
        }
        
        private static void CloneRootOnly(Item root, Item destination)
        {
            Clone(root, destination, false);
        }

        private static Item Clone(Item cloneSource, Item cloneDestination, bool deep)
        {
            Assert.ArgumentNotNull(cloneSource, "cloneSource");
            Assert.ArgumentNotNull(cloneDestination, "cloneDestination");
            return cloneSource.CloneTo(cloneDestination, deep);
        }

        private static void DeleteItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (Settings.RecycleBinActive)
            {
                item.Recycle();
            }
            else
            {
                item.Delete();
            }
        }

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

            return CommandState.Hidden;
        }

        private static Item GetContextItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            return context.Items.FirstOrDefault();
        }

        private static bool CanTransform(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return IsProxy(item)
                    && IsSourceDatabaseFieldEmpty(item)
                    && !string.IsNullOrWhiteSpace(GetProxyType(item))
                    && !string.IsNullOrWhiteSpace(GetSourceItemFieldValue(item))
                    && !string.IsNullOrWhiteSpace(GetTargetItemFieldValue(item));
        }

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

        private static string GetProxyType(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item["Proxy type"];
        }

        private static bool IsSourceDatabaseFieldEmpty(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return string.IsNullOrWhiteSpace(item["Source database"]);
        }

        private static string GetSourceItemFieldValue(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item["Source item"];
        }

        private static string GetTargetItemFieldValue(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item["Target item"];
        }
        
        private static Item GetItem(string path)
        {
            Assert.ArgumentNotNullOrEmpty(path, "path");
            return Context.ContentDatabase.GetItem(path);
        }

        private static bool AreEqualIgnoreCase(string one, string two)
        {
            return string.Equals(one, two, StringComparison.CurrentCultureIgnoreCase);
        }
    }
}

The above command is only visible for proxy items having both source and target items set, and the proxy is for the context content database.

When the command is invoked, the source item — conjoined with its descendants if its sub-tree is also being proxied — is cloned to the target item, after the proxy definition item is deleted.

I registered the above command in Sitecore using an include configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:transformproxytoclones" type="Sitecore.Sandbox.Commands.TransformProxyToClones, Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

I also wired this up in the core database for the item context menu (I’ve omitted a screenshot on how this is done; If you would like to see how this is done, please see part 1 and part 2 of my post showing how to add to the item context menu).

Let’s take this for a drive.

I created a bunch of items for testing:

proxy-test-tree-created-items

I then created a proxy for these test items:

proxy-item-sub-tree

I then right-clicked on our test proxy item to launch its context menu, and then clicked on the “Transform Proxy To Clones” menu option:

transform-proxy-to-clones-context

The proxy item was removed, and we now have clones:

proxy-gone-now-clones

If you can think of any other ideas around proxies or clones, or know of other tools that create clones from proxies, please leave a comment.

Show Clones of An Item In the Sitecore CMS

“Out of the box”, there is no specific way to see all clones for a given item — at least I haven’t found one yet.

One could see these by looking at referrers of an item:

navigate-links-dropdown-clones

Unfortunately, referrers in this dropdown aren’t just reserved for clones — all referrers of the item will display in this dropdown. An example would include an item referencing the item in a Droplink field.

In a previous post, I provided a solution for auto-cloning new subitems to clones of their parents. That solution leveraged an instance of the ItemClonesGatherer utility class I defined in that post to return a collection of clones for a given item.

Yesterday, I realized this class could be reused for a feature to show a listing of clones for an item in Sitecore, and this post showcases that solution.

I had to come up with a medium for displaying a list of clones of an item. I decided I would display these in a new content editor tab.

I recalled reading an article by Sitecore MVP Mark Stiles on adding new Editor tabs in Sitecore.

As Mark Stiles had done in his post, I created a stand-alone ASP.NET web form (/sitecore modules/Shell/ShowClones/default.aspx). This web form will display the content of our new “Show Clones” tab:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Sitecore650rev120706.sitecore_modules.Shell.ShowClones.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Show Clones</title>
    <style type="text/css">
        h1
        {
            color: #072d6b;
            font-size: 14pt;
            margin-bottom: 5px;
        }
        a
        {
            text-decoration: none;
            font-size: 8pt;
            font-family: Tahoma;
        }
        a:hover
        {
            text-decoration: underline;
        }
        #clones
        {
            margin: 10px;
        }
        #clone_listing
        {
            list-style-type: none;
            margin: 0 0 0 15px;
        }
        #clone_listing li
        {
            margin: 0;
        }
        #clone_listing li + li
        {
            margin-top: 5px;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
        <div id="clones">
            <h1>Clones</h1>
            <asp:MultiView ID="mvClones" ActiveViewIndex="0" runat="server">
                <asp:View ID="vClones" runat="server">
                    <asp:Repeater ID="rptClones" runat="server">
                        <HeaderTemplate>
                            <ol id="clone_listing">
                        </HeaderTemplate>
                        <ItemTemplate>
                            <li>
                                <asp:HyperLink 
                                    ID="hlClone" 
                                    NavigateUrl="#"
                                    onclick='<%# string.Format("parent.scForm.invoke(\"item:load(id={0})\"); return false;", Eval("ID").ToString()) %>'
                                    Text='<%# Eval("Paths.FullPath") %>'
                                    runat="server" />
                            </li>
                        </ItemTemplate>
                        <FooterTemplate>
                            </ol>
                        </FooterTemplate>
                    </asp:Repeater>
                </asp:View>
                <asp:View ID="vNoClones" runat="server">
                    <script type="text/javascript">
                        parent.scContent.closeEditorTab('ShowClones');
                    </script>
                </asp:View>
            </asp:MultiView>
        </div>
    </form>
</body>
</html>

The web form’s code-behind:

using System;
using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Web;

using Sitecore.Sandbox.Utilities.Gatherers.Base;
using Sitecore.Sandbox.Utilities.Gatherers;

namespace Sitecore650rev120706.sitecore_modules.Shell.ShowClones
{
    public partial class Default : System.Web.UI.Page
    {
        private static readonly IItemsGatherer ClonesGatherer = ItemClonesGatherer.CreateNewItemClonesGatherer();

        protected void Page_Load(object sender, EventArgs e)
        {
            ShowClones();
        }

        private void ShowClones()
        {
            BindRepeater();
            ToggleViews();
        }

        private void BindRepeater()
        {
            rptClones.DataSource = GetClones();
            rptClones.DataBind();
        }

        private void ToggleViews()
        {
            if (rptClones.Items.Count > 0)
            {
                mvClones.SetActiveView(vClones);
            }
            else
            {
                mvClones.SetActiveView(vNoClones);
            }
        }

        private IEnumerable<Item> GetClones()
        {
            Item item = GetItem();
            if(item == null)
            {
                return new List<Item>();
            }

            ClonesGatherer.Source = item;
            return ClonesGatherer.Gather();
        }

        private Item GetItem()
        {
            Item item = null;

            try
            {
                item = Sitecore.Context.ContentDatabase.GetItem(GetID(), GetLanguage(), GetVersion());
            }
            catch(Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }

            return item;
        }

        private ID GetID()
        {
            Sitecore.Data.ID id;
            if(Sitecore.Data.ID.TryParse(WebUtil.GetQueryString("id"), out id))
            {
                return id;
            }

            return Sitecore.Data.ID.Null;
        }

        private Language GetLanguage()
        {
            Language language;
            if(Language.TryParse(WebUtil.GetQueryString("la"), out language))
            {
                return language;
            }

            return Sitecore.Context.Language;
        }

        private Sitecore.Data.Version GetVersion()
        {
            Sitecore.Data.Version version;
            if (Sitecore.Data.Version.TryParse(WebUtil.GetQueryString("vs"), out version))
            {
                return version;
            }

            return Sitecore.Data.Version.Latest;
        }
    }
}

The code-behind above uses an instance of the ItemClonesGatherer class to get all clones for the passed item. If clones are found, these are bound to a repeater to display them as links.

If there are no clones, the Multiview in the web form is toggled to render JavaScript to close the tab — the tab should not be open if the item does not have any clones.

After revisiting Mark’s article, and realized I needed a different solution: one that will allow me to add a new tab on the fly.

Such a solution should only allow the tab to open when an item has clones, and it should not be be associated with any templates — it must be template oblivious.

I stumbled upon a command in one of the Sitecore DLLs — which one it was is evading me at the moment — and noticed it was using an instance of Sitecore.Web.UI.Framework.Scripts.ShowEditorTab. I decided to take a chance on using an instance of this object, hoping it might open up a new content editor tab on the fly.

Using that command as a model, I came up with the following command:

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

using Sitecore.Sandbox.Utilities.Gatherers.Base;
using Sitecore.Sandbox.Utilities.Gatherers;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Resources;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Text;
using Sitecore.Web;
using Sitecore.Web.UI.Framework.Scripts;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Commands
{
    public class ShowClones : Command
    {
        private static readonly IItemsGatherer ClonesGatherer = ItemClonesGatherer.CreateNewItemClonesGatherer();

        public override void Execute(CommandContext commandContext)
        {
            ShowClonesEditorTab(GetItem(commandContext));
        }

        private static void ShowClonesEditorTab(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            const string command = "contenteditor:pagedesigner";
            const string id = "ShowClones";

            bool shouldClickEditorTab = IsEditorTabOpen(command);

            if (IsEditorTabOpen(command))
            {
                ClickOpenEditorTab(id);
                return;
            }
            
            OpenEditorTab(CreateNewShowClonesEditorTab(item, command, id));
        }

        private static bool IsEditorTabOpen(string command)
        {
            Assert.ArgumentNotNullOrEmpty(command, "command");
            return WebUtil.GetFormValue("scEditorTabs").Contains(command);
        }

        private static void ClickOpenEditorTab(string id)
        {
            Assert.ArgumentNotNullOrEmpty(id, "id");
            SheerResponse.Eval(string.Format("scContent.onEditorTabClick(null, null, '{0}')", id));
        }

        private static void OpenEditorTab(ShowEditorTab tab)
        {
            Assert.ArgumentNotNull(tab, "tab");
            SheerResponse.Eval(tab.ToString());
        }

        private static ShowEditorTab CreateNewShowClonesEditorTab(Item item, string command, string id)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNullOrEmpty(command, "command");
            Assert.ArgumentNotNullOrEmpty(id, "id");

            UrlString urlString = new UrlString("/sitecore modules/shell/showclones/default.aspx");
            item.Uri.AddToUrlString(urlString);
            UIUtil.AddContentDatabaseParameter(urlString);
            return new ShowEditorTab
            {
                Command = command,
                Header = Translate.Text("Show Clones"),
                Icon = Images.GetThemedImageSource("Network/32x32/link_view.png"),
                Url = urlString.ToString(),
                Id = id,
                Closeable = true,
                Activate = true
            };
        }

        public override CommandState QueryState(CommandContext context)
        {
            if (!HasClones(GetItem(context)))
            {
                return CommandState.Hidden;
            }

            return CommandState.Enabled;
        }

        private static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            return context.Items.FirstOrDefault();
        }

        private static bool HasClones(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return GetClones(item).Any();
        }

        private static IEnumerable<Item> GetClones(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            ClonesGatherer.Source = item;
            return ClonesGatherer.Gather();
        }
    }
}

The command above uses an instance of ItemClonesGatherer to get all clones for the item in the content tree, and ensures it is visible when the item has clones. The logic hides the command when the item does not have clones.

When the command is invoked, it will open a new “Show Clones” tab, or set focus on the “Show Clones” tab if it’s already present.

I then registered the command above in a patch include configuration file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:showclones" type="Sitecore.Sandbox.Commands.ShowClones,Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

Now, we need to lock and load this command in Sitecore. I switched over to the core database, and added a new item context menu option:

show-clones-command

Let’s see this in action.

I first created a bunch of clones:

clones-in-content-tree

On my source item, I right-clicked, and was presented with a new context menu option to “Show Clones”:

show-clones-context-menu

After clicking the “Show Clones” context menu option, a new “Show Clones” tab appeared:

show-clones-tab

I then clicked on one of the links in the “Show Clones” tab, and was brought to its associated clone:

was-brough-to-clone

If you have other ideas around using clones in Sitecore, or if you know of another way of listing clones of an item, please leave 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.