Home » Context Menu (Page 2)

Category Archives: Context Menu

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.

Put Things Into Context: Augmenting the Item Context Menu – Part 2

In part 1 of this article, I helped out a friend by walking him through how I added new Publishing menu options — using existing commands in Sitecore — to my Item context menu.

In this final part of the article, I will show you how I created my own custom command and pipeline to copy subitems from one Item in the content tree to another, and wired them up to a new Item context menu option within the ‘Copying’ fly-out menu of the context menu.

Overriding two methods — CommandState QueryState(CommandContext context) and void Execute(CommandContext context) — is paramount when creating a custom Sitecore.Shell.Framework.Commands.Command.

The QueryState() method determines how we should treat the menu option given the current passed context. In other words, should the menu option be enabled, disabled, or hidden? This method serves as a hook — it’s declared virtual — and returns CommandState.Enabled by default. All menu options are enabled by default unless overridden to do otherwise.

The Execute method contains the true “meat and potatoes” of a command’s logic — this method is the place where we do “something” to items passed to it via the CommandContext object. This method is declared abstract in the Command class, ultimately forcing subclasses to define their own logic — there is no default logic for this method.

Here is my command to copy subitems from one location in the content tree to another:

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

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

using Sitecore.Sandbox.Pipelines.Base;
using Sitecore.Sandbox.Pipelines.Utilities;

namespace Sitecore.Sandbox.Commands
{
    public class CopySubitemsTo : Command
    {
        private const string CopySubitemsPipeline = "uiCopySubitems";
        
        public override void Execute(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            CopySubitems(commandContext);
        }

        private void CopySubitems(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            IEnumerable<Item> subitems = GetSubitems(commandContext);
            StartPipeline(subitems);
        }

        public static IEnumerable<Item> GetSubitems(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            return GetSubitems(commandContext.Items);
        }

        public static IEnumerable<Item> GetSubitems(IEnumerable<Item> items)
        {
            Assert.ArgumentNotNull(items, "items");
            List<Item> list = new List<Item>();

            foreach (Item item in items)
            {
                list.AddRange(item.Children.ToArray());
            }

            return list;
        }

        private void StartPipeline(IEnumerable<Item> subitems)
        {
            Assert.ArgumentNotNull(subitems, "subitems");
            Assert.ArgumentCondition(subitems.Count() > 0, "subitems", "There must be at least one subitem in the collection to copy!");

            IPipelineLauncher pipelineLauncher = CreateNewPipelineLauncher();
            pipelineLauncher.StartPipeline(CopySubitemsPipeline, new CopyItemsArgs(), subitems.First().Database, subitems);
        }

        private IPipelineLauncher CreateNewPipelineLauncher()
        {
            return PipelineLauncher.CreateNewPipelineLauncher(Context.ClientPage);
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            int subitemsCount = CalculateSubitemsCount(commandContext);

            // if this item has no children, let's disable the menu option
            if (subitemsCount < 1)
            {
                return CommandState.Disabled;
            }

            return base.QueryState(commandContext);
        }

        private static int CalculateSubitemsCount(CommandContext commandContext)
        {
            int count = 0;

            foreach (Item item in commandContext.Items)
            {
                count += item.Children.Count;
            }

            return count;
        }
    }
}

My QueryState() method returns CommandState.Disabled if the current context item — although this does offer the ability to have multiple selected items — has no children. More complex logic could be written here, albeit I chose to keep it simple.

Many of the Commands within Sitecore.Shell.Framework.Commands that deal with Sitecore Items use the utility class Sitecore.Shell.Framework.Items. These commands delegate their core Execute() logic to static methods defined in this class.

Instead of creating my own Items utility class, I included my Comamnd’s logic in my Command class instead — I figure doing this makes it easier to illustrate it here (please Mike, stop writing so many classes :)). However, I would argue it should live in its own class.

The Sitecore.Shell.Framework.Commands.Item class also has a private Start() method that launches a pipeline and passes arguments to it. I decided to copy this code into a utility class that does just that, and used it above in my Command. Here is that class and its interface — the PipelineLauncher class:

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

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

namespace Sitecore.Sandbox.Pipelines.Base
{
    public interface IPipelineLauncher
    {
        void StartPipeline(string pipelineName, ClientPipelineArgs args, Database database, IEnumerable<Item> items);
    }
}
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;

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

using Sitecore.Sandbox.Pipelines.Base;

namespace Sitecore.Sandbox.Pipelines.Utilities
{
    public class PipelineLauncher : IPipelineLauncher
    {
        private const char PipeDelimiter = '|';

        private ClientPage ClientPage { get; set; }

        private PipelineLauncher(ClientPage clientPage)
        {
            SetClientPage(clientPage);
        }

        private void SetClientPage(ClientPage clientPage)
        {
            Assert.ArgumentNotNull(clientPage, "clientPage");
            ClientPage = clientPage;
        }

        public void StartPipeline(string pipelineName, ClientPipelineArgs args, Database database, IEnumerable<Item> items)
        {
            Assert.ArgumentNotNullOrEmpty(pipelineName, "pipelineName");
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNull(items, "items");
            
            ListString listString = new ListString(PipeDelimiter);
            foreach (Item item in items)
            {
                listString.Add(item.ID.ToString());
            }

            NameValueCollection nameValueCollection = new NameValueCollection();
            nameValueCollection.Add("database", database.Name);
            nameValueCollection.Add("items", listString.ToString());

            args.Parameters = nameValueCollection;
            ClientPage.Start(pipelineName, args);
        }

        public static IPipelineLauncher CreateNewPipelineLauncher(ClientPage clientPage)
        {
            return new PipelineLauncher(clientPage);
        }
    }
}

Next, I created a pipeline that does virtually all of the heavy lifting of the command — it is truly the “man behind the curtain”.

Instead of writing all of copying logic to do this from scratch — not a difficult feat to accomplish — I decided to subclass the CopyTo pipeline used by the ‘Copy To’ command. This pipeline already copies multiples items from one location in the content tree to another. The only thing I needed to change was the url of my new dialog (I define a new dialog down below).

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

using Sitecore.Configuration;
using Sitecore.Shell.Framework.Pipelines;

namespace Sitecore.Sandbox.Pipelines.UICopySubitems
{
    public class CopySubitems : CopyItems
    {
        private const string DialogUrlSettingName = "Pipelines.UICopySubitems.CopySubitems.DialogUrl";
        private static readonly string DialogUrl = Settings.GetSetting(DialogUrlSettingName);

        protected override string GetDialogUrl()
        {
            return DialogUrl;
        }
    }
}

This is the definition for my new dialog. This is really just a “copy and paste” job of /sitecore/shell/Applications/Dialogs/CopyTo/CopyTo.xml with some changed copy — I changed ‘Copy Item’ to ‘Copy Subitems’.

This dialog uses the same logic as the ‘Copy Item To’ dialog. I saved this xml into a file named /sitecore/shell/Applications/Dialogs/CopySubitemsTo/CopySubitemsTo.xml.

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
  <CopyTo>
    <FormDialog Icon="Core3/24x24/Copy_To_Folder.png" Header="Copy Subitems To" 
      Text="Select the location where you want to copy the subitems to." OKButton="Copy">

      <CodeBeside Type="Sitecore.Shell.Applications.Dialogs.CopyTo.CopyToForm,Sitecore.Client"/>

      <DataContext ID="DataContext" Root="/"/>

      <GridPanel Width="100%" Height="100%" Style="table-layout:fixed">
        <Scrollbox Height="100%" Class="scScrollbox scFixSize scFixSize4 scInsetBorder" Background="white" Padding="0px" GridPanel.Height="100%">
          <TreeviewEx ID="Treeview" DataContext="DataContext" Click="SelectTreeNode" ContextMenu='Treeview.GetContextMenu("contextmenu")' />
        </Scrollbox>

        <Border Padding="4px 0px 4px 0px">
          <GridPanel Width="100%" Columns="2">

            <Border Padding="0px 4px 0px 0px">
              <Literal Text="Name:"/>
            </Border>

            <Edit ID="Filename" Width="100%" GridPanel.Width="100%"/>
          </GridPanel>
        </Border>

      </GridPanel>

    </FormDialog>
  </CopyTo>
</control>

Now, we have to wedge in my new pipeline into the Web.config. I did this by defining my new pipeline in a file named /App_Config/Include/CopySubitems.config. I also added a setting for my dialog url — this is being acquired above in my pipeline class. I vehemently loathe hardcoding things :).

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<processors>
			<uiCopySubitems>
				<processor mode="on" type="Sitecore.Sandbox.Pipelines.UICopySubitems.CopySubitems,Sitecore.Sandbox" method="GetDestination"/>
				<processor mode="on" type="Sitecore.Sandbox.Pipelines.UICopySubitems.CopySubitems,Sitecore.Sandbox" method="CheckDestination"/>
				<processor mode="on" type="Sitecore.Sandbox.Pipelines.UICopySubitems.CopySubitems,Sitecore.Sandbox" method="CheckLanguage"/>
				<processor mode="on" type="Sitecore.Sandbox.Pipelines.UICopySubitems.CopySubitems,Sitecore.Sandbox" method="Execute"/>
			</uiCopySubitems>
		</processors>
		<settings>
			<setting name="Pipelines.UICopySubitems.CopySubitems.DialogUrl" value="/sitecore/shell/Applications/Dialogs/Copy Subitems to.aspx" />
		</settings>
	</sitecore>
</configuration>

Next, I had to define my new command in /App_Config/Commands.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

<!-- Some Stuff Defined Here -->

<command name="item:copysubitemsto" type="Sitecore.Sandbox.Commands.CopySubitemsTo, Sitecore.Sandbox" />

<!-- More Stuff Defined Here -->

</configuration>

As I had done in part 1 of this article, I created a new menu item — using the /sitecore/templates/System/Menus/Menu item template. I created a new menu item named ‘Copy Subitems To’ underneath /sitecore/content/Applications/Content Editor/Context Menues/Default/Copying in my Item context menu in the core database:

Copy Subitems To Menu Item

Oh look, I found some subitems I would like to copy somewhere else:

Subitems To Copy

After right-clicking on the parent of my subitems and hovering over ‘Copying’, my beautiful new menu item displays:

'Copy Subitems To Context Menu

I then clicked ‘Copy Subitems To’, and this dialog window appeared. I then selected a destination for my copied subitems, and clicked the ‘Copy’ button:

Copy Subitems To Dialog

And — voilà! — my subitems were copied to my selected destination:

Subitems Copies One Level

I had more fun with this later on by copying nested levels of subitems — it will copy all subitems beyond just one level below.

There are a few moving parts here, but nothing too overwhelming.

I hope you try out building your own custom Item context menu option. Trust me, you will have lots of fun building one. I know I did!

Until next time, happy coding!

Put Things Into Context: Augmenting the Item Context Menu – Part 1

Last week, I asked Lewis Goulden — a friend and former colleague — to name one thing in Sitecore he would like to learn or know more about. His answer was knowing how to add menu options to the Item context menu — particularly the ‘Publish Now’ and ‘Publish Item’ menu options found in the Publish ribbon.

I remembered reading a few blog articles in the past articulating how one would accomplish this. I needed to dig to find any of these articles.

After googling — yes googling is a verb in the English language — a few times, I found an article by John West discussing how to do this and another by Aboo Bolaky. I had also discovered this on pages 259-260 in John West’s book Professional Sitecore Development.

Instead of passing these on to Lewis and wishing him the best of luck in his endeavours augmenting his context menu — definitely not a cool thing to do to a friend — I decided to capture how this is done in this post, and share this with you as well.

Plus, this article sets the cornerstone for part two: augmenting the item context menu using a custom command and pipeline.

Here’s what the context menu looks like unadulterated:

Item Context Menu

My basic goal is to add a new ‘Publishing’ fly-out menu item containing ‘Publish Now’ and ‘Publish Item’ submenu options betwixt the ‘Insert’ and ‘Cut’ menu options.

In other words, I need to go find the ‘Publish Now’ and ‘Publish Item’ menu items in the core database and add these to the Item context menu.

Publishing Ribbon

So, I had to switch over to the core database and fish around to find out the command names of the ‘Publish Now’ and ‘Publish Item’ menu items.

First, I looked at the Publish ribbon (/sitecore/content/Applications/Content Editor/Ribbons/Ribbons/Default/Publish):

Publish Ribbon

The Publish ribbon helped me hone in closer to where I should continue to look to find the publishing menu items.

Next, I went to the Publish strip:

Publish Strip

The Publish strip pretty much tells me I have to keep on digging. Now, I have to go take a look at the Publish chunk.

In the Publish chunk, I finally found the ‘Publish Now’ menu button:

publish now button

I made sure I copied its click field value — we’ll be needing it when we create our own ‘Publish Now’ menu item.

I then navigated to /sitecore/content/Applications/Content Editor/Menues/Publish to snag the command name of the ‘Publish Item’ menu option:

Publish Item

Now, I have all I need to create my own ‘Publishing’ fly-out menu item and its ‘Publish Now’ and ‘Publish Item’ submenu options.

I then went to /sitecore/content/Applications/Content Editor/Context Menues/Default and added a new Item named ‘Divider’ — using the /sitecore/templates/System/Menus/Menu divider template — after the Insert menu option, and created my ‘Publishing’ fly-out menu item using the /sitecore/templates/System/Menus/Menu item template.

Thereafter, I created my ‘Publish Item’ menu item using the same template as its parent ‘Publishing’. I then put the command name for the ‘Publish Item’ menu option found previously into my item’s Message field:

Publish Item Context Menu Button

Next, I created my ‘Publish Now’ menu item using the same template as its parent and sibling. I put in the ‘Publish Now’ menu item’s Message field the command name for the ‘Publish Now’ menu option found during my expedition above:

Publish Now Context Menu Button

Now, let’s take this for a test drive. I switched back to the master database and navigated to my Home node. I then right-clicked:

Publishing Context Menu 1

As you can see, the ‘Publishing’ fly-out menu appears with its publishing submenu options. Trust me, both menu items do work. 🙂

All of this without any code whatsoever!

In part 2 of this article, I will step through building a custom command coupled with a custom pipeline that compose the logic for a custom menu item for the Item context menu that copies all subitems of an ancestor item to a selected destination.

Happy coding and have a Sitecorelicious day! 🙂