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:
I right-clicked on my Home item to launch its context menu, and clicked ‘Insert from Template’:
I was presented with the “out of the box” ‘Insert from Template’ dialog, and selected a template:
Next I was prompted to select a sibling item to insert the new item after:
As you can see the new item now resides after the selected sibling:
If you have any thoughts on this, or other ideas around modifying the uiAddFromTemplate pipeline, please share in a comment below.