Home » Sorting

Category Archives: Sorting

Insert a New Item After a Sibling Item in Sitecore

Have you ever thought to yourself “wouldn’t it be nice to insert a new item in the Sitecore content tree at a specific place among its siblings without having to move the inserted item up or down multiple times to position it correctly?”

I’ve had this thought more than once, and decided to put something together to achieve this.

The following class consists of methods to be used in pipeline processors of the uiAddFromTemplate pipeline to make this happen:

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

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

namespace Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate
{
    public class InsertAfterItemOperations
    {
        private string SelectButtonText { get; set; }

        private string ModalIcon { get; set; }

        private string ModalTitle { get; set; }

        private string ModalInstructions { get; set; }

        public void StoreTemplateResult(ClientPipelineArgs args)
        {
            args.Parameters["Template"] = args.Result;
        }

        public void EnsureParentAndChildren(ClientPipelineArgs args)
        {
            AssertArguments(args);
            Item parent = GetParentItem(args);
            EnsureParentItem(parent, args);
            args.Parameters["HasChildren"] = parent.HasChildren.ToString();
            args.IsPostBack = false;
        }

        public void GetInsertAfterId(ClientPipelineArgs args)
        {
            AssertArguments(args);
            bool hasChildren = false;
            bool.TryParse(args.Parameters["HasChildren"], out hasChildren);
            if (!hasChildren)
            {
                SetCanAddItemFromTemplate(args);
                return;
            }

            Item parent = GetParentItem(args);
            EnsureParentItem(parent, args);
            if (!args.IsPostBack)
            {
                ItemListerOptions itemListerOptions = new ItemListerOptions
                {
                    ButtonText = SelectButtonText,
                    Icon = ModalIcon,
                    Title = ModalTitle,
                    Text = ModalInstructions,
                    Items = parent.Children.ToList()
                };

                SheerResponse.ShowModalDialog(itemListerOptions.ToUrlString().ToString(), true);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.Parameters["InsertAfterId"] = args.Result;
                SetCanAddItemFromTemplate(args);
                args.IsPostBack = false;
            }
            else
            {
                SetCanAddItemFromTemplate(args);
                args.IsPostBack = false;
            }
        }

        private void SetCanAddItemFromTemplate(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.Parameters["CanAddItemFromTemplate"] = bool.TrueString;
        }

        private static Item GetParentItem(ClientPipelineArgs args)
        {
            AssertArguments(args);
            Assert.ArgumentNotNullOrEmpty(args.Parameters["id"], "id");
            return GetItem(GetDatabase(args.Parameters["database"]), args.Parameters["id"], args.Parameters["language"]);
        }

        private static void EnsureParentItem(Item parent, ClientPipelineArgs args)
        {
            if (parent != null)
            {
                return;
            }

            SheerResponse.Alert("Parent item could not be located -- perhaps it was deleted.");
            args.AbortPipeline();
        }

        public void AddItemFromTemplate(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            bool canAddItemFromTemplate = false;
            bool.TryParse(args.Parameters["CanAddItemFromTemplate"], out canAddItemFromTemplate);
            if (canAddItemFromTemplate)
            {
                int index = args.Parameters["Template"].IndexOf(',');
                Assert.IsTrue(index >= 0, "Invalid return value from dialog");
                string path = StringUtil.Left(args.Parameters["Template"], index);
                string name = StringUtil.Mid(args.Parameters["Template"], index + 1);
                Database database = GetDatabase(args.Parameters["database"]);
                Item parent = GetItem(database, args.Parameters["id"], args.Parameters["language"]);
                if (parent == null)
                {
                    SheerResponse.Alert("Parent item not found.");
                    args.AbortPipeline();
                    return;
                }

                if (!parent.Access.CanCreate())
                {
                    SheerResponse.Alert("You do not have permission to create items here.");
                    args.AbortPipeline();
                    return;
                }

                Item item = database.GetItem(path);
                if (item == null)
                {
                    SheerResponse.Alert("Item not found.");
                    args.AbortPipeline();
                    return;
                }
                
                History.Template = item.ID.ToString();
                Item added = null;
                if (item.TemplateID == TemplateIDs.Template)
                {
                    Log.Audit(this, "Add from template: {0}", new string[] { AuditFormatter.FormatItem(item) });
                    TemplateItem template = item;
                    added = Context.Workflow.AddItem(name, template, parent);
                }
                else
                {
                    Log.Audit(this, "Add from branch: {0}", new string[] { AuditFormatter.FormatItem(item) });
                    BranchItem branch = item;
                    added = Context.Workflow.AddItem(name, branch, parent);
                }

                if (added == null)
                {
                    SheerResponse.Alert("Something went terribly wrong when adding the item.");
                    args.AbortPipeline();
                    return;
                }

                args.Parameters["AddedId"] = added.ID.ToString();
            }
        }

        public void MoveAdded(ClientPipelineArgs args)
        {
            AssertArguments(args);
            Assert.ArgumentNotNullOrEmpty(args.Parameters["AddedId"], "AddedId");
            Item added = GetAddedItem(args);
            if (string.IsNullOrWhiteSpace(args.Parameters["InsertAfterId"]))
            {
                Items.MoveFirst(new [] { added });
            }
            
            SetSortorder(GetItemOrdering(added, args.Parameters["InsertAfterId"]));
        }

        private static void AssertArguments(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            Assert.ArgumentNotNullOrEmpty(args.Parameters["database"], "database");
            Assert.ArgumentNotNullOrEmpty(args.Parameters["language"], "language");
        }

        private static Item GetAddedItem(ClientPipelineArgs args)
        {
            AssertArguments(args);
            Assert.ArgumentNotNullOrEmpty(args.Parameters["AddedId"], "AddedId");
            return GetItem(GetDatabase(args.Parameters["database"]), args.Parameters["AddedId"], args.Parameters["language"]);
        }

        private static Item GetItem(Database database, string id, string language)
        {
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNullOrEmpty(id, "id");
            Assert.ArgumentNotNullOrEmpty(language, "language");
            return database.Items[id, Language.Parse(language)];
        }

        private static Database GetDatabase(string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            return Factory.GetDatabase(databaseName);
        }

        private static IList<Item> GetItemOrdering(Item added, string insertAfterId)
        {
            IList<Item> ordering = new List<Item>();
            foreach (Item child in added.Parent.GetChildren())
            {
                ordering.Add(child);
                bool shouldAddAfter = string.Equals(child.ID.ToString(), insertAfterId);
                if (shouldAddAfter)
                {
                    ordering.Add(added);
                }
            }

            return ordering;
        }

        private static void SetSortorder(IList<Item> items)
        {
            Assert.ArgumentNotNull(items, "items");
            for (int i = 0; i < items.Count; i++)
            {
                int sortorder = (i + 1) * 100;
                SetSortorder(items[i], sortorder);
            }
        }

        private static void SetSortorder(Item item, int sortorder)
        {
            Assert.ArgumentNotNull(item, "item");
            if (item.Access.CanWrite() && !item.Appearance.ReadOnly)
            {
                item.Editing.BeginEdit();
                item[FieldIDs.Sortorder] = sortorder.ToString();
                item.Editing.EndEdit();
            }
        }
    }
}

In the StoreTemplateResult method, we store the ID of the template selected in the ‘Insert from Template’ dialog. This dialog is launched by clicking the ‘Insert from Template’ menu option in the item context menu — an example of this can be seen in my test run near the bottom of this post.

The EnsureParentAndChildren method makes certain the parent item exists — we want to be sure another user did not delete it in another Sitecore session — and ascertains if the parent item has children.

Logic in the GetInsertAfterId method launches another dialog when the parent item does have children. This dialog prompts the user to select a sibling item to precede the new item. If the ‘Cancel’ button is clicked, the item will be inserted before all sibling items.

The AddItemFromTemplate method basically contains the same logic that can be found in the Execute method in the Sitecore.Shell.Framework.Pipelines.AddFromTemplate class in Sitecore.Kernel.dll, albeit with a few minor changes — I removed some of the nested if/else conditionals, and stored the ID of the newly created item, which is needed when reordering the sibling items (this is how we move the new item after the selected sibling item).

The MoveAdded method is where we reorder the siblings items with the newly created item so that the new item follows the selected sibling. If there is no selected sibling, we just move the new item to the first position.

I then put all of the above together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <uiAddFromTemplate>
        <processor mode="on" patch:after="processor[@type='Sitecore.Shell.Framework.Pipelines.AddFromTemplate,Sitecore.Kernel' and @method='GetTemplate']"
                   type="Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox" method="StoreTemplateResult"/>
        <processor mode="on" patch:after="processor[@type='Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox' and @method='StoreTemplateResult']"
                   type="Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox" method="EnsureParentAndChildren"/>
        <processor mode="on" patch:after="processor[@type='Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox' and @method='EnsureParentAndChildren']"
                   type="Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox" method="GetInsertAfterId">
          <SelectButtonText>Insert After</SelectButtonText>
          <ModalIcon>Applications/32x32/nav_up_right_blue.png</ModalIcon>
          <ModalTitle>Select Item to Insert After</ModalTitle>
          <ModalInstructions>Select the item you would like to insert after. If you would like to insert before the first item, just click 'Cancel'.</ModalInstructions>
        </processor>
        <processor mode="on" patch:instead="processor[@type='Sitecore.Shell.Framework.Pipelines.AddFromTemplate,Sitecore.Kernel' and @method='Execute']"
                   type="Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox" method="AddItemFromTemplate" />
        <processor mode="on" patch:after="processor[@type='Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox' and @method='AddItemFromTemplate']"
                   type="Sitecore.Sandbox.Shell.Framework.Pipelines.AddFromTemplate.InsertAfterItemOperations, Sitecore.Sandbox" method="MoveAdded" />
      </uiAddFromTemplate> 
    </processors>
  </sitecore>
</configuration>

Let’s test this out.

This is how my content tree looked before adding new items:

no-new-items

I right-clicked on my Home item to launch its context menu, and clicked ‘Insert from Template’:

item-context-menu-insert-from-template

I was presented with the “out of the box” ‘Insert from Template’ dialog, and selected a template:

insert-from-template-dialog

Next I was prompted to select a sibling item to insert the new item after:

selected-insert-after

As you can see the new item now resides after the selected sibling:

was-inserted-after

If you have any thoughts on this, or other ideas around modifying the uiAddFromTemplate pipeline, please share in a comment below.

Advertisement

You Can’t Move This! Experiments in Disabling Move Related Commands in the getQueryState Sitecore Pipeline

I was scavenging through my local sandbox instance’s Web.config the other day — yes I was looking for things to customize — and noticed the getQueryState pipeline — a pipeline that contains no “out of the box” pipeline processors:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
	<!-- Some stuff here -->
	<sitecore>
		<!-- Some more stuff here -->
		<pipelines>
			<!-- Even more stuff here -->
		<!--  Allows developers to programmatically disable or hide any button or panel in the Content Editor ribbons 
            without overriding the individual commands. 
            Processors must accept a single argument of type GetQueryStateArgs (namespace: Sitecore.Pipelines.GetQueryState)  -->
			<getQueryState>
			</getQueryState>
			<!-- Yeup, more stuff here -->
		</pipelines>
		<!-- wow, lots of stuff here too -->
	</sitecore>
	<!-- lots of stuff down here -->
</configuration>

The above abridged version of my Web.config contains an XML comment underscoring what this pipeline should be used for: disabling and/or hiding buttons in the Sitecore client.

Although I am still unclear around the practicality of using this pipeline overall — if you have an idea, please leave a comment — I thought it would be fun building one regardless, just to see how it works. Besides, I like to tinker with things — many of my previous posts corroborate this sentiment.

What I came up with is a getQueryState pipeline processor that disables buttons containing move related commands on a selected item in the content tree with a particular template. Sorting commands also fall under the umbrella of move related commands, so these are included in our set of commands to disable.

Here’s the pipeline processor I built:

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

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

namespace Sitecore.Sandbox.Pipelines.GetQueryState
{
    public class DisableMoveCommands
    {
        private static readonly IEnumerable<string> Commands = GetCommands();
        private static readonly IEnumerable<string> UnmovableTemplateIDs = GetUnmovableTemplateIDs();

        public void Process(GetQueryStateArgs args)
        {
            if (!CanProcessGetQueryStateArgs(args))
            {
                return;
            }

            bool shouldDisableCommand = IsUnmovableItem(GetCommandContextItem(args)) && IsMovableCommand(args.CommandName);
            if (shouldDisableCommand)
            {
                args.CommandState = CommandState.Disabled;
            }
        }

        private static bool CanProcessGetQueryStateArgs(GetQueryStateArgs args)
        {
            return args != null
                    && !string.IsNullOrEmpty(args.CommandName)
                    && args.CommandContext != null
                    && args.CommandContext.Items.Any();
        }

        private static Item GetCommandContextItem(GetQueryStateArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.CommandContext, "args.CommandContext");
            Assert.ArgumentNotNull(args.CommandContext.Items, "args.CommandContext.Items");
            return args.CommandContext.Items.FirstOrDefault();
        }

        private static bool IsUnmovableItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return IsUnmovableTemplateID(item.TemplateID);
        }

        private static bool IsUnmovableTemplateID(ID templateID)
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(templateID), "templateID", "templateID must be set!");
            return UnmovableTemplateIDs.Contains(templateID.ToString());
        }

        private static bool IsMovableCommand(string command)
        {
            Assert.ArgumentNotNullOrEmpty(command, "command");
            return Commands.Contains(command);
        }

        private static IEnumerable<string> GetCommands()
        {
            return GetStringCollection("moveCommandsToPrevent/command");
        }

        private static IEnumerable<string> GetUnmovableTemplateIDs()
        {
            return GetStringCollection("unmovableTemplates/id");
        }

        private static IEnumerable<string> GetStringCollection(string path)
        {
            Assert.ArgumentNotNullOrEmpty(path, "path");
            return Factory.GetStringSet(path);
        }
    }
}

The above pipeline processor checks to see if the selected item in the content tree has the unmovable template I defined, coupled with whether the current command is within the set of commands we are to disable. If both are cases are met, the pipeline processor will disable the context command.

I defined my unmovable template and commands to disable in a patch include config file, along with the getQueryState pipeline processor:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getQueryState>
        <processor type="Sitecore.Sandbox.Pipelines.GetQueryState.DisableMoveCommands, Sitecore.Sandbox" />
      </getQueryState>
    </pipelines>
    <unmovableTemplates>
      <id>{8ADD3F45-027C-49C5-A8FB-0406B8C8728D}</id>
    </unmovableTemplates>
    <moveCommandsToPrevent>
      <command>item:moveto</command>
      <command>item:cuttoclipboard</command>
      <command>item:moveup</command>
      <command>item:movedown</command>
      <command>item:movefirst</command>
      <command>item:movelast</command>
      <command>item:moveto</command>
    </moveCommandsToPrevent>
  </sitecore>
</configuration>

Looking at the context menu on my unmovable item — appropriately named “You Cant Move This” — you can see the move related buttons are disabled:

context-menu-move-commands-disabled

Plus, item level sorting commands are also disabled:

context-menu-sorting-commands-disabled

Move related buttons in the ribbon are also disabled:

move-commands-in-ribbon-disabled

There really wasn’t much to building this pipeline processor, and this pipeline is at your disposal if you ever find yourself in a situation where you might have to disable buttons in the Sitecore client for whatever reason.

However, as I mentioned above, I still don’t understand why one would want to use this pipeline. If you have an idea why, please let me know.

Get Your House in Order: Create Your Own Subitems Sorting Comparer

This morning, I was fishing around in the core database of my local instance to research my next article — yeah, I know it’s Christmas morning and I should be resting, but unwrapping hidden gems in Sitecore.Kernel beats opening up presents anyday — and discovered that one can easily add his/her own subitems sorting comparer.

After opening up .NET reflector and using Sitecore.Data.Comparers.UpdatedComparer as a model, I created the following Comparer within minutes:

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

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

namespace Sitecore.Sandbox.Data.Comparers
{
    public class ItemNameLengthComparer : ExtractedKeysComparer
    {
        protected override int CompareKeys(IKey keyOne, IKey keyTwo)
        {
            Assert.ArgumentNotNull(keyOne, "keyOne");
            Assert.ArgumentNotNull(keyTwo, "keyTwo");
            return IntegerCompareTo(keyOne.Key, keyTwo.Key);
        }

        protected override int DoCompare(Item itemOne, Item itemTwo)
        {
            return IntegerCompareTo(itemOne.Name.Length, itemTwo.Name.Length);
        }

        private static int IntegerCompareTo(object itemOneNameLength, object itemTwoNameLength)
        {
            return IntegerCompareTo((int)itemOneNameLength, (int)itemTwoNameLength);
        }

        private static int IntegerCompareTo(int itemOneNameLength, int itemTwoNameLength)
        {
            return itemOneNameLength.CompareTo(itemTwoNameLength);
        }

        public override IKey ExtractKey(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return new KeyObj 
            { 
                Item = item, 
                Key = item.Name.Length, 
                Sortorder = item.Appearance.Sortorder 
            };
        }
    }
}

All we have to do is override and add our own custom logic to the DoCompare(), CompareKeys() and ExtractKey() methods defined in the ExtractedKeysComparer base class.

The above Comparer will sort items based on the length of their names — items with shorter names will appear before their siblings with longer names.

Next, I created a new Child Sorting item in my master database — yes, I did say the master database since I learned the hard way (my sorting option wasn’t appearing) that these sorting comparers are to be defined in each database where they are used — for my Comparer:

New Child Sorting

Let’s see how this Comparer fares in the wild.

I first created a handful of test items with different item name lengths in a new parent folder under my Home node:

Test Subitems

I then opened up the Subitems Sorting dialog and saw my new subitems sorting option in the ‘Sorting’ dropdown:

Set Subitems Sort 1

Nervously, I selected my new subitems sorting option and clicked ‘OK’:

Set Subitems Sort 2

I then wiped my brow and exhaled with relief after seeing that it had worked as I intended:

Set Subitems Sort 3

The above is further evidence of how customizable the Sitecore client truly is. Remember, the Sitecore client was built using the same API/technologies we use each and everyday to build websites within it — thus empowering us to extend the client where we see fit.

Happy Holidays! 🙂