Home » Context Menu

Category Archives: Context Menu

Advertisements

Bucket and Unbucket Items via Custom Item Context Menu Options Using Sitecore PowerShell Extensions

Last Wednesday I was honored to present Sitecore PowerShell Extensions (SPE) at the Milwaukee Sitecore Meetup. My presentation was all about how easy it is to add, execute and reuse PowerShell scripts in SPE, and I showcased this using version 3.0 of SPE on Sitecore XP 8.

During the presentation, I demonstrated how one can go about adding custom Item Context Menu options using SPE, and did so with the following scripts which bucket and unbucket Sitecore Items:

 <#
    .NAME 
        Convert To Item Bucket

    .SYNOPSIS
        Converts the context item to an Item Bucket
     
    .NOTES
        Mike Reynolds
#>
 
$item = Get-Item .

if($item."__Is Bucket" -eq "1") {
   return 
}

Get-ChildItem . -Recurse | %{ $_.__Bucketable = "1" }
Invoke-ShellCommand -Name "item:bucket" -Item $item
Close-Window

The above PowerShell script basically checks to see if an Item is an Item Bucket and does nothing if it is. If the Item is not an Item Bucket, we make sure all sub-items are bucketable — we just tick the “__Bucketable” Checkbox field on them — and invoke the Sheer UI command for converting an Item into an Item Bucket — this is done via the Invoke-ShellCommand commandlet that ships with SPE — and then close the script execution dialog users are presented with when SPE executes a script in the Item Context Menu.

The following script does the exact opposite of the script above — it converts an Item Bucket back to a regular Sitecore Item using the Sheer UI command for unbucketing:

<#
    .NAME 
        Convert Item Bucket To A Regular Item

    .SYNOPSIS
        Converts an Item Bucket to a regular Item
     
    .NOTES
        Mike Reynolds
#>

$item = Get-Item .

if($item."__Is Bucket" -ne "1") {
   return 
}

Invoke-ShellCommand -Name "item:unbucket" -Item $item
Close-Window
 

I then saved the above scripts to a Context Menu integration point in a SPE module I created during the presentation using the SPE Integrated Scripting Environment (ISE) (to get to the ISE in SPE, go to Sitecore ==> Development Tools ==> PowerShell ISE in the Sitecore Start menu of the Sitecore Desktop):

Saved-context-menu-script

I’ve omitted screenshots on saving the “Convert Item Bucket To A Regular Item” script for brevity.

I then set rules in the “Show if rules are met or not defined” field on both Context Menu script Items — we only want the “Convert To Item Bucket” Context Menu option to show when the Item isn’t an Item Bucket, and the “Convert Item Bucket To A Regular Item” Context Menu option to show when the Item it is an Item Bucket:

The “Convert To Item Bucket” Context Menu item (this Item was saved to /sitecore/system/Modules/PowerShell/Script Library/SitecoreUG Module/Content Editor/Context Menu/Convert To Item Bucket in my Sitecore instance):

set-rules-convert-to-bucket

The “Convert Item Bucket To A Regular Item” Context Menu item:

set-rules-convert-to-unbucket

After saving the above, I navigated to an Item in my content tree that has sub-items, right-clicked on it, and clicked on the “Convert To Item Bucket” Context Menu option:

convert-to-bucket-right-click

I was then presented with a confirmation dialog:

convert-to-bucket-confirm

As you can see the Item is now an Item Bucket:

item-is-a-bucket

I right-clicked on the Item again, and clicked on the “Convert Item Bucket To A Regular Item” Context Menu option:

convert-to-unbucket-right-click

I was presented with another confirmation dialog:

convert-to-ubucket-confirm

As you can see the Item is no longer an Item Bucket:

no-longer-a-bucket

If you have any thoughts on this or ideas for other Context Menu PowerShell scripts for SPE, please drop a comment.

If you would like to watch the Milwaukee Sitecore Meetup presentation where I showed the above — you’ll also get to see some cool Sitecore PowerShell Extensions stuff from Adam Brauer, Senior Product Engineer at Active Commerce, in this presentation as well — have a look below:

Until next time, have a Sitecoretastic day!

Advertisements

Launch PowerShell Scripts in the Item Context Menu using Sitecore PowerShell Extensions

Last week during my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014 — a conference held in Utrecht, Netherlands — I demonstrated how to invoke PowerShell scripts from the Item context menu in Sitecore, and felt I should capture what I had shown in a blog post — yes, this is indeed that blog post. 😉

During that piece of my presentation, I shared the following PowerShell script to expands tokens in fields of a Sitecore item (if you want to learn more about tokens in Sitecore, please take a look at John West’s post about them, and also be aware that one can also invoke the Expand-Token PowerShell command that comes with Sitecore PowerShell Extensions to expand tokens on Sitecore items — this makes things a whole lot easier 😉 ):

$item = Get-Item .
$tokenReplacer = [Sitecore.Configuration.Factory]::GetMasterVariablesReplacer()
$item.Editing.BeginEdit()
$tokenReplacer.ReplaceItem($item)
$item.Editing.EndEdit()
Close-Window

The script above calls Sitecore.Configuration.Factory.GetMasterVariablesReplacer() for an instance of the MasterVariablesReplacer class — which is defined and can be overridden in the “MasterVariablesReplacer” setting in your Sitecore instance’s Web.config — and passes the context item — this is denote by a period — to the MasterVariablesReplacer instance’s ReplaceItem() method after the item has been put into editing mode.

Once the Item has been processed, it is taken out of editing mode.

So how do we save this script so that we can use it in the Item context menu? The following screenshot walks you through the steps to do just that:

item-context-menu-powershell-ise

The script is saved to an Item created by the dialog above:

expand-tokens-item

Let’s test this out!

I selected an Item with unexpanded tokens:

home-tokens-to-expand

I then launched its Item context menu, and clicked the option we created to ‘Expand Tokens’:

home-item-context-menu-expand-tokens

As you can see the tokens were expanded:

home-tokens-expanded

If you have any questions or thoughts on this, please drop a comment.

Until next time, have a scriptolicious day 😉

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!

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.

Expand Tokens on Sitecore Items Using a PowerShell Function in Sitecore PowerShell Extensions

During my Sitecore from the Command Line presentation at the Sitecore User Group – New England, I had briefly showcased a custom PowerShell function that expands Sitecore tokens in fields of a supplied item, and how I had saved this function into the Functions section of the Script Library — /sitecore/system/Modules/PowerShell/Script Library/Functions — of the Sitecore PowerShell Extensions module. This blog post captures what I had shown.

This is the custom function I had shown — albeit I changed its name to adhere to the naming convention in PowerShell for functions and commands (Verb-SingularNoun):

function Expand-SitecoreToken {
	<#
        .SYNOPSIS
             Expand tokens on the supplied item
              
        .EXAMPLE
            Expand tokens on the home item.
             
            PS master:\> Get-Item "/sitecore/content/home" | Expand-SitecoreToken
    #>
	[CmdletBinding()]
    param( 
		[ValidateNotNull()]
		[Parameter(ValueFromPipeline=$True)]
        [Sitecore.Data.Items.Item]$item
    )
	
    $item.Editing.BeginEdit()
    
    Try
    {
        $tokenReplacer = [Sitecore.Configuration.Factory]::GetMasterVariablesReplacer()
        $tokenReplacer.ReplaceItem($item)
        $result = $item.Editing.EndEdit()
        "Expanded tokens on item " + $item.Paths.Path
    }
    Catch [system.Exception]
    {
        $item.Editing.CancelEdit()
        "Failed to expand tokens on item"
        "Reason: " + $error
    }
}

The function above calls Sitecore.Configuration.Factory.GetMasterVariablesReplacer() for an instance of the MasterVariablesReplacer class — which is defined and can be overridden in the “MasterVariablesReplacer” setting in your Sitecore instance’s Web.config — and passes the item supplied to the function to the MasterVariablesReplacer instance’s ReplaceItem() method after the item has been put into editing mode.

Once tokens have been expanded, a confirmation message is sent to the Results window.

If an exception is caught, we display it — the exception is captured in the $error global variable.

I saved the above function into the Script Library of my copy of the Sitecore PowerShell Extensions module:

spe-save-function

An item was created in the Script Library to house the function:

Expand-SitecoreToken-item

Let’s try it out.

Let’s expand tokens on the Home item:

spe-home-unexpanded-tokens

In the Integrated Scripting Environment of the Sitecore PowerShell Extensions module, I typed in the following code:

Execute-Script "master:/system/Modules/PowerShell/Script Library/Functions/Expand-SitecoreToken"
Get-Item . | Expand-SitecoreToken

You can consider the Execute-Script “master:/system/Modules/PowerShell/Script Library/Functions/Expand-SitecoreToken” line of code to be comparable to a javascript “script” tag — it will execute the script thus defining the function so we can execute it.

I then ran that code above:

excuted-Expand-SitecoreToken-on-home

Once the script finished running, I went back over to the Content Editor, and saw that tokens were expanded on the Home item:

spe-function-home-tokens-expanded

You might be thinking “Mike, I really don’t want to be bothered with expanding these tokens, and would rather have our Content Editors/Authors do it. is there something we can set up to make that happen?”

You bet. 🙂

In the Sitecore PowerShell Extension module, you can save PowerShell into the Script Library to be executed via an item context menu option click. All you have to do is save it into the Script Library under the Content Editor Context Menu item:

spe-context-menu-option

The script is then saved in a new item created under Content Editor Context Menu item in the Script Library:

spe-content-menu-option-item

Let’s see it in action.

I chose the following page at random to expand tokens:

spe-inner-page-three-unexpanded-tokens

I right-clicked on the item to launch it’s context menu, opened up scripts, and saw a new “Expand Tokens” option:

spe-new-context-menu-option

I clicked it, and was given a dialog with a status bar:

spe-context-menu-expanding-tokens

I refreshed the item, and saw that all tokens were expanded:

spe-context-menu-tokens-expanded

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

Until next time, have a scriptabulous day!

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.