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

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

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

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!


12 Comments

  1. […] creative juices flowing. Why not create new item context menu options — check out part 1 and part 2 of my post discussing how one would go about augmenting the item context menu, and also my last […]

  2. […] the core database. If you are unfamiliar with how this is done, please take a look at my first and second posts on augmenting the item context menu to see how this is […]

  3. […] 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 […]

  4. […] 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 […]

  5. […] 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 […]

  6. […] 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 […]

  7. Please this receive an Asp.Net Yellow Screen ot death, saying
    Server Error in ‘/’ Application.

    Value cannot be null.
    Parameter name: value

    Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

    Exception Details: System.ArgumentNullException: Value cannot be null.
    Parameter name: value

    Source Error:

    An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

    Stack Trace:

    [ArgumentNullException: Value cannot be null.
    Parameter name: value]
    Sitecore.Text.UrlString.Append(String key, String value) +466
    Sitecore.Shell.Framework.Pipelines.CopyItems.GetDestination(CopyItemsArgs args) +384

    [TargetInvocationException: Exception has been thrown by the target of an invocation.]
    System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor) +0
    System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments) +76
    System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) +211
    System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters) +35
    Sitecore.Nexus.Pipelines.NexusPipelineApi.Resume(PipelineArgs args, Pipeline pipeline) +398
    Sitecore.Pipelines.Pipeline.Start(PipelineArgs args, Boolean atomic) +327
    Sitecore.Web.UI.Sheer.ClientPage.RunPipelines() +271
    Sitecore.Web.UI.Sheer.ClientPage.OnPreRender(EventArgs e) +547
    Sitecore.Shell.Applications.ContentManager.ContentEditorPage.OnPreRender(EventArgs e) +25
    System.Web.UI.Control.PreRenderRecursiveInternal() +113
    System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +4297

    PLease Help

  8. Please, on debugging everything works, but when you watch closely, the ArgumentNullException is thrown from

    [ArgumentNullException: Value cannot be null.
    Parameter name: value]
    Sitecore.Text.UrlString.Append(String key, String value) +466
    Sitecore.Shell.Framework.Pipelines.CopyItems.GetDestination(CopyItemsArgs args) +384

    and i guess its from

    I pretty new to sitecore. Thanks.
    Regards

  9. This is exception is throw after clicking the context menu. A dialog is displayed with this message “Value cannot be null.
    Parameter name: value”

    Regards

Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.