Home » Search results for 'Command'

Search Results for: Command

Bucket Items in Sitecore using a Custom Commandlet in Sitecore PowerShell Extensions

Last Wednesday I had the privilege to present Sitecore PowerShell Extensions (SPE) at the Milwaukee Sitecore Meetup. During my presentation, I demonstrated how easy it is to add, execute and reuse PowerShell scripts in SPE, and I showcased version 3.0 of SPE on Sitecore XP 8.

Unfortunately, I ran out of time before showing how one can go about creating a custom commandlet in SPE, and hope to make it up to everyone by sharing the commandlet I wrote for the presentation in this post.

I wrote the following commandlet to convert an Item into an Item Bucket in Sitecore:

using System;
using System.Management.Automation;

using Sitecore.Data.Items;
using Sitecore.Shell.Framework.Commands;

using Cognifide.PowerShell.Commandlets;
using Cognifide.PowerShell.Commandlets.Interactive.Messages;

namespace Sitecore.Sandbox.SPE.Commandlets.Buckets
{
    [Cmdlet(VerbsData.ConvertTo, "Bucket"), OutputType(new Type[] { typeof(Item) })]
    public class ConvertToBucketCommand : BaseItemCommand
    {
        protected override void ProcessItem(Item item)
        {
            try
            {
                PutMessage(new ShellCommandInItemContextMessage(item, "item:bucket"));   
            }
            catch (Exception exception)
            {
                WriteError(new ErrorRecord(exception, "sitecore_new_bucket_error", ErrorCategory.NotSpecified, Item));
            }

            WriteItem(Item);
        }
    }
}

The above commandlet implements the ProcessItem() method — this method is declared abstract in one of the ancestor classes of the class above — and leverages the framework of SPE to invoke a Sheer UI command to bucket the Item passed to the method — one of the ancestor classes of this class passes the Item to be processed.

The above highlights how in SPE we are employing the Template method pattern for many “out of the box” commandlets. This involves inheriting from an abstract base class — Cognifide.PowerShell.Commandlets.BaseItemCommand in Cognifide.PowerShell.dll (this assembly comes with the SPE module) is an example of one of these base classes — and implementing methods that are defined as abstract. The parent or an ancestor class will do the brunt of the work behind the scenes, and use your method implementation for specifics.

As a side note, we also provide method hooks as well — these are virtual methods defined on a base or ancestor class — which you can override to change how they work to meet your particular needs.

I then wired the above up using a Sitecore include configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
        <add Name="Custom Bucket Commandlets" type="*, Sitecore.Sandbox.SPE" />
      </commandlets>
    </powershell>
  </sitecore>
</configuration>

I deployed the above to my Sitecore instance; loaded up the Integrated Scripting Environment (ISE) in SPE; and saw that my commandlet was registered using the Control-Space shortcut key:

convert-to-bucket-ise-control-space

Let’s take this for a spin. Let’s convert the Home Item into an Item Bucket:

home-before-bucket

Here’s my script to do that:

ise-convert-home-bucket

I clicked the execute button, and then got this confirmation dialog:

ise-convert-home-bucket-confirm

I then clicked the “Ok” button and was immediately presented with this dialog:

ise-convert-home-bucket-processing

As you can see it worked! The Home Item in my content tree is now an Item Bucket:

home-after-bucket

If you have any thoughts on this or ideas for other custom commandlets for SPE, please share in a comment.

If you would like to watch the Milwaukee Sitecore Meetup presentation where I showcased Sitecore PowerShell Extensions — and as a bonus you’ll also get to see some real-life application of SPE from Adam Brauer, Senior Product Engineer at Active Commerce, in this presentation as well — it has been recorded for posterity, and you can watch it here:

Until next time, stay curious, keep experimenting, and let’s keep on sharing all the Sitecore things!

Accept All Notifications on Clones of an Item using a Custom Command in Sitecore

As I was walking along a beach near my apartment tonight, I thought “wouldn’t it be nifty to have a button in the Sitecore ribbon to accept all notifications on clones of an Item instead of having to accept these manually on each clone?”

I immediately returned home, and whipped up the following command class:

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

using Sitecore.Data.Clones;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Shell.Framework.Commands
{
    public class AcceptAllNotificationsOnClones : Command
    {
        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            IEnumerable<Item> clones = GetClonesWithNotifications(GetItem(context));
            if(!clones.Any())
            {
                return CommandState.Hidden;
            }

            return CommandState.Enabled;
        }

        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Item item = GetItem(context);
            IEnumerable<Item> clones = GetClonesWithNotifications(item);
            if(!clones.Any())
            {
                return;
            }

            foreach (Item clone in clones)
            {
                AcceptAllNotifications(item.Database.NotificationProvider, clone);
            }
        }

        protected virtual Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            return context.Items.FirstOrDefault();
        }

        protected virtual IEnumerable<Item> GetClonesWithNotifications(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            IEnumerable<Item> clones = item.GetClones();
            if(!clones.Any())
            {
                return new List<Item>();
            }
            
            IEnumerable<Item> clonesWithNotifications = GetClonesWithNotifications(item.Database.NotificationProvider, clones);
            if(!clonesWithNotifications.Any())
            {
                return new List<Item>();
            }

            return clonesWithNotifications;
        }

        protected virtual IEnumerable<Item> GetClonesWithNotifications(NotificationProvider notificationProvider, IEnumerable<Item> clones)
        {
            Assert.ArgumentNotNull(notificationProvider, "notificationProvider");
            Assert.ArgumentNotNull(clones, "clones");
            return (from clone in clones
                    let notifications = notificationProvider.GetNotifications(clone)
                    where notifications.Any()
                    select clone).ToList();
        }

        protected virtual void AcceptAllNotifications(NotificationProvider notificationProvider, Item clone)
        {
            Assert.ArgumentNotNull(notificationProvider, "notificationProvider");
            Assert.ArgumentNotNull(clone, "clone");
            foreach (Notification notification in notificationProvider.GetNotifications(clone))
            {
                notification.Accept(clone);
            }
        }
    }
}

The code in the command above ensures the command is only visible when the selected Item in the Sitecore content tree has clones, and those clones have notifications — this visibility logic is contained in the QueryState() method.

When the command is invoked — this happens through the Execute() method — all clones with notifications of the selected Item are retrieved, and iterated over — each are passed to the AcceptAllNotifications() method which contains logic to accept all notifications on them via the Accept() method on a NotificationProvider instance: this NotificationProvider instance comes from the source Item’s Database property.

I then registered the above command class in Sitecore using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:AcceptAllNotificationsOnClones" type="Sitecore.Sandbox.Shell.Framework.Commands.AcceptAllNotificationsOnClones, Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

We need a way to invoke this command. I created a new button to go into the ‘Item Clones’ chunk in the ribbon:

accept-notifications-on-clones-button-core

Let’s take this for a test drive!

I first created some clones:

created-clones

I then changed a field value on one of those clones:

changed-field-value-on-clone

On the clone’s source Item, I changed the same field’s value with something completely different, and added a new child item — the new button appeared after saving the Item:

new-child-item-field-value-change

Now, the clone has notifications on it:

notifications-on-clone

I went back to the source Item, clicked the ‘Accept Notifications On Clones’ button in the ribbon, and navigated back to the clone:

notifications-accepted

As you can see, the notifications were accepted.

If you have any thoughts on this, please share in a comment.

Build a Custom Command in Sitecore PowerShell Extensions

In my Sitecore PowerShell Extensions presentation at the Sitecore User Group Conference 2014, I showed the audience how easy it is to build custom commands for Sitecore PowerShell Extensions, and thought it would be a good idea to distill what I had shown into a blog post for future reference. This blog post embodies that endeavor.

During my presentation, I shared an example of using the template method pattern for two commands using the following base class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;

using Sitecore.Data.Items;

using Cognifide.PowerShell.PowerShellIntegrations.Commandlets;

namespace Sitecore.Sandbox.SPE.Commandlets.Data
{
    public abstract class EditItemCommand : BaseCommand
    {
        protected override void ProcessRecord()
        {
            ProcessItem(Item);
            if (!Recurse.IsPresent)
            {
                return;
            }
            
            ProcessItems(Item.Children, true);
        }

        private void ProcessItems(IEnumerable<Item> items, bool recursive)
        {
            foreach (Item item in items)
            {
                ProcessItem(item);
                if (recursive && item.Children.Any())
                {
                    ProcessItems(item.Children, recursive);
                }
            }
        }

        private void ProcessItem(Item item)
        {
            item.Editing.BeginEdit();
            try
            {
                EditItem(item);
                item.Editing.EndEdit();
            }
            catch (Exception exception)
            {
                item.Editing.CancelEdit();
                throw exception;
            }

            WriteItem(item);
        }

        protected abstract void EditItem(Item item);

        [Parameter(ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
        public Item Item { get; set; }

        [Parameter]
        public SwitchParameter Recurse { get; set; }
    }
}

The class above defines the basic algorithm for editing an Item — the editing part occurs in the EditItem() method which must be defined by subclasses — and all of its descendants when the Recurse switch is supplied to the command. When the Recursive switch is supplied, recursion is employed to process all descendants of the Item once editing of the supplied Item is complete.

The following subclass of the EditItemCommand class above protects a supplied Item in its implementation of the EditItem() method:

using System;
using System.Management.Automation;

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.SPE.Commandlets.Data
{
    [OutputType(new Type[] { typeof(Sitecore.Data.Items.Item) }), Cmdlet("Protect", "Item")]
    public class ProtectItemCommand : EditItemCommand
    {
        protected override void EditItem(Item item)
        {
            item.Appearance.ReadOnly = true;
        }
    }
}

Conversely, the following subclass of the EditItemCommand class unprotects the passed Item in its EditItem() method implementation:

using System;
using System.Management.Automation;

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.SPE.Commandlets.Data
{
    [OutputType(new Type[] { typeof(Sitecore.Data.Items.Item) }), Cmdlet("Unprotect", "Item")]
    public class UnprotectItemCommand : EditItemCommand
    {
        protected override void EditItem(Item item)
        {
            item.Appearance.ReadOnly = false;
        }
    }
}

The verb and noun for each command is defined in the Cmdlet class attribute set on each command class declaration.

I then registered all of the above in Sitecore using the following configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
            <add Name="Sitecore Sandbox Commandlets" type="*, Sitecore.Sandbox" />
      </commandlets>
    </powershell>
  </sitecore>                                                                                    
</configuration>

Since everything looks copacetic — you got to love a developer’s optimism 😉 — I built and deployed all of the above to my Sitecore instance.

Let’s take this for a spin!

I selected my home Item knowing it is not protected:

not-protected-home

I then looked to see if it had an unprotected descendant, and found the following item:

not-protected-page-three

I then ran a script on the home Item using our new command to protect an item, and supplied the Recurse switch to protect all descendants:

protect-item-command-powershell-ise

As you can see, the home Item is now protected:

protected-home

Its descendant is also protected:

protected-page-three

Let’s now unprotect them. I ran a script on the home Item using our new command to unprotect an item, and supplied the Recurse switch to process all descendants:

unprotect-item-command-powershell-ise

As you can see, the home Item is now unprotected again:

not-protected-again-home

Its descendant is also unprotected:

not-protected-again-page-three

If you have any thoughts or ideas around improving anything you’ve seen in this post, or have other ideas for commands that should be included in Sitecore PowerShell Extensions, please drop a comment.

I would also like to point out that I had written a previous blog post on creating a custom command in Sitecore PowerShell Extensions. You might want to go check that out as well.

Until next time, have a scriptastic day! 🙂

Add Sitecore Rocks Commands to Protect and Unprotect Items

The other day I read this post where the author showcased a new Clipboard command he had added into Sitecore Rocks, and immediately wanted to experiment with adding my own custom command into Sitecore Rocks.

After some research, I stumbled upon this post which gave a walk-through on augmenting Sitecore Rocks by adding a Server Component — this is an assembled library of code for your Sitecore instance to handle requests from Sitecore Rocks — and a Plugin — this is an assembled library of code that can contain custom commands — and decided to follow its lead.

I first created a Sitecore Rocks Server Component project in Visual Studio:

sitecore-rocks-server-component

After some pondering, I decided to ‘cut my teeth’ on creating custom commands to protect and unprotect Items in Sitecore (for more information on protecting/unprotecting Items in Sitecore, check out ‘How to Protect or Unprotect an Item’ in Sitecore’s Client Configuration
Cookbook
).

I decided to use the template method pattern for the classes that will handle requests from Sitecore Rocks — I envisioned some shared logic across the two — and put this shared logic into the following base class:

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Rocks.Server.Requests
{
    public abstract class EditItem
    {
        public string Execute(string id, string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(id, "id");
            return ExecuteEditItem(GetItem(id, databaseName));
        }

        private string ExecuteEditItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            item.Editing.BeginEdit();
            string response = UpdateItem(item);
            item.Editing.EndEdit();
            return response;
        }

        protected abstract string UpdateItem(Item item);
        
        private static Item GetItem(string id, string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(id, "id");
            Database database = GetDatabase(databaseName);
            Assert.IsNotNull(database, string.Format("database: {0} does not exist!", databaseName));
            return database.GetItem(id);
        }

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

The EditItem base class above gets the Item in the requested database, and puts the Item into edit mode. It then passes the Item to the UpdateItem method — subclasses must implement this method — and then turns off edit mode for the Item.

As a side note, all Server Component request handlers must have a method named Execute.

For protecting a Sitecore item, I built the following subclass of the EditItem class above:

using Sitecore.Data.Items;

namespace Sitecore.Rocks.Server.Requests.Attributes
{
    public class ProtectItem : EditItem
    {
        protected override string UpdateItem(Item item)
        {
            item.Appearance.ReadOnly = true;
            return string.Empty;
        }
    }
}

The ProtectItem class above just protects the Item passed to it.

I then built the following subclass of EditItem to unprotect an item passed to its UpdateItem method:

using Sitecore.Data.Items;

namespace Sitecore.Rocks.Server.Requests.Attributes
{
    public class UnprotectItem : EditItem
    {
        protected override string UpdateItem(Item item)
        {
            item.Appearance.ReadOnly = false;
            return string.Empty;
        }
    }
}

I built the above Server Component solution, and put its resulting assembly into the /bin folder of my Sitecore instance.

I then created a Plugin solution to handle the protect/unprotect commands in Sitecore Rocks:

sitecore-rocks-plugin

I created the following command to protect a Sitecore Item:

using System;
using System.Linq;

using Sitecore.VisualStudio.Annotations;
using Sitecore.VisualStudio.Commands;
using Sitecore.VisualStudio.Data;
using Sitecore.VisualStudio.Data.DataServices;

using SmartAssembly.SmartExceptionsCore;

namespace Sitecore.Rocks.Sandbox.Commands
{
    [Command]
    public class ProtectItemCommand : CommandBase
    {
        public ProtectItemCommand()
        {
            Text = "Protect Item";
            Group = "Edit";
            SortingValue = 4010;
        }

        public override bool CanExecute([CanBeNull] object parameter)
        {
            IItemSelectionContext context = null;
            bool canExecute = false;
            try
            {
                context = parameter as IItemSelectionContext;
                canExecute = context != null && context.Items.Count() == 1 && !IsProtected(context.Items.FirstOrDefault());
            }
            catch (Exception ex)
            {
                StackFrameHelper.CreateException3(ex, context, this, parameter);
                throw;
            }

            return canExecute;
        }

        private static bool IsProtected(IItem item)
        {
            ItemVersionUri itemVersionUri = new ItemVersionUri(item.ItemUri, LanguageManager.CurrentLanguage, Sitecore.VisualStudio.Data.Version.Latest);
            Item item2 = item.ItemUri.Site.DataService.GetItemFields(itemVersionUri);
            foreach (Field field in item2.Fields)
            {
                if (string.Equals("__Read Only", field.Name, StringComparison.CurrentCultureIgnoreCase) && field.Value == "1")
                {
                    return true;
                }
            }

            return false;
        }

        public override void Execute([CanBeNull] object parameter)
        {
            IItemSelectionContext context = null;
            try
            {
                context = parameter as IItemSelectionContext;
                IItem item = context.Items.FirstOrDefault();
                item.ItemUri.Site.DataService.ExecuteAsync
                (
                    "Attributes.ProtectItem",
                    CreateEmptyCallback(),
                    new object[] 
                    { 
	                    item.ItemUri.ItemId.ToString(), 
	                    item.ItemUri.DatabaseName.ToString()
                    }
                );
            }
            catch (Exception ex)
            {
                StackFrameHelper.CreateException3(ex, context, this, parameter);
                throw;
            }
        }

        private ExecuteCompleted CreateEmptyCallback()
        {
            return (response, executeResult) => { return; };
        }
    }
}

The ProtectItemCommand command above is only displayed when the selected Item is not protected — this is ascertained by logic in the CanExecute method — and fires off a request to the Sitecore.Rocks.Server.Requests.Attributes.ProtectItem request handler in the Server Component above to protect the selected Item.

I then built the following command to do the exact opposite of the command above: only appear when the selected Item is protected, and make a request to Sitecore.Rocks.Server.Requests.Attributes.UnprotectItem — shown above in the Server Component — to unprotect the selected Item:

using System;
using System.Linq;

using Sitecore.VisualStudio.Annotations;
using Sitecore.VisualStudio.Commands;
using Sitecore.VisualStudio.Data;
using Sitecore.VisualStudio.Data.DataServices;

using SmartAssembly.SmartExceptionsCore;

namespace Sitecore.Rocks.Sandbox.Commands
{
    [Command]
    public class UnprotectItemCommand : CommandBase
    {
        public UnprotectItemCommand()
        {
            Text = "Unprotect Item";
            Group = "Edit";
            SortingValue = 4020;
        }

        public override bool CanExecute([CanBeNull] object parameter)
        {
            IItemSelectionContext context = null;
            bool canExecute = false;
            try
            {
                context = parameter as IItemSelectionContext;
                canExecute = context != null && context.Items.Count() == 1 && IsProtected(context.Items.FirstOrDefault());
            }
            catch (Exception ex)
            {
                StackFrameHelper.CreateException3(ex, context, this, parameter);
                throw;
            }

            return canExecute;
        }

        private static bool IsProtected(IItem item)
        {
            ItemVersionUri itemVersionUri = new ItemVersionUri(item.ItemUri, LanguageManager.CurrentLanguage, Sitecore.VisualStudio.Data.Version.Latest);
            Item item2 = item.ItemUri.Site.DataService.GetItemFields(itemVersionUri);
            foreach (Field field in item2.Fields)
            {
                if (string.Equals("__Read Only", field.Name, StringComparison.CurrentCultureIgnoreCase) && field.Value == "1")
                {
                    return true;
                }
            }

            return false;
        }

        public override void Execute([CanBeNull] object parameter)
        {
            IItemSelectionContext context = null;
            try
            {
                context = parameter as IItemSelectionContext;
                IItem item = context.Items.FirstOrDefault();
                item.ItemUri.Site.DataService.ExecuteAsync
                (
                    "Attributes.UnprotectItem",
                    CreateEmptyCallback(),
                    new object[] 
                    { 
	                    item.ItemUri.ItemId.ToString(), 
	                    item.ItemUri.DatabaseName.ToString()
                    }
                );
            }
            catch (Exception ex)
            {
                StackFrameHelper.CreateException3(ex, context, this, parameter);
                throw;
            }
        }

        private ExecuteCompleted CreateEmptyCallback()
        {
            return (response, executeResult) => { return; };
        }
    }
}

I had to do a lot of discovery in Sitecore.Rocks.dll via .NET Reflector in order to build the above commands, and had a lot of fun while searching and learning.

Unfortunately, I could not get the commands above to show up in the Sitecore Explorer context menu in my instance of Sitecore Rocks even though my plugin did make its way out to my C:\Users\[my username]\AppData\Local\Sitecore\Sitecore.Rocks\Plugins\ folder.

I troubleshooted for some time but could not determine why these commands were not appearing — if you have any ideas, please leave a comment — and decided to register my commands using Extensions in Sitecore Rocks as a fallback plan:

sitecore-rocks-extensions-menu-option

After clicking ‘Extensions’ in the Sitecore dropdown menu in Visual Studio, I was presented with the following dialog, and added my classes via the ‘Add’ button on the right:

sitecore-rocks-extension-dialog

Let’s see this in action.

I first created a Sitecore Item for testing:

item-unprotected

I navigated to that Item in the Sitecore Explorer in Sitecore Rocks, and right-clicked on it:

item-unprotected-1

After clicking ‘Protect Item’, I verified the Item was protected in Sitecore:

item-protected

I then went back to our test Item in the Sitecore Explorer of Sitecore Rocks, and right-clicked again:

sitecore-rocks-unprotect-item

After clicking ‘Unprotect Item’, I took a look at the Item in Sitecore, and saw that it was no longer protected:

item-unprotected-again

If you have any thoughts on this, or ideas for other commands that you would like to see in Sitecore Rocks, please drop a comment.

Until next time, have a Sitecoretastic day, and don’t forget: Sitecore Rocks!

Expand Tokens on Sitecore Items Using a Custom Command in Sitecore PowerShell Extensions

During my Sitecore from the Command Line presentation at the Sitecore User Group – New England, I had shown attendees how they could go about adding a custom command into the Sitecore PowerShell Extensions module.

This blog post shows what I had presented — although the code in this post is an improved version over what I had presented at my talk. Many thanks to Sitecore MVP Adam Najmanowicz for helping me make this code better!

The following command will expand “out of the box” tokens in all fields of a supplied Sitecore item — check out Expand Tokens on Sitecore Items Using a Custom Command in Revolver where I discuss the problem commands like this address, and this article by Sitecore MVP Jens Mikkelsen which lists “out of the box” tokens available in Sitecore:

using System;
using System.Management.Automation;

using Sitecore.Configuration;

using Sitecore.Data;
using Sitecore.Data.Items;

using Cognifide.PowerShell.PowerShellIntegrations.Commandlets;

namespace CommandLineExtensions.PowerShell.Commandlets
{
    [Cmdlet("Expand", "Token")]
    [OutputType(new[] { typeof(Item) })]
    public class ExpandTokenCommand : BaseCommand
    {
        private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer();

        [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)]
        public Item Item { get; set; }

        protected override void ProcessRecord()
        {
            Item.Editing.BeginEdit();
            try
            {
                TokenReplacer.ReplaceItem(Item);
                Item.Editing.EndEdit();
            }
            catch (Exception ex)
            {
                Item.Editing.CancelEdit();
                throw ex;
            }

            WriteItem(Item);
        }
    }
}

The command above subclasses Cognifide.PowerShell.PowerShellIntegrations.Commandlets.BaseCommand — the base class for most (if not all) commands in Sitecore PowerShell Extensions.

An item is passed to the command via a parameter, and is magically set on the Item property of the command class instance.

The ValueFromPipeline parameter being set to “true” on the Item property’s Parameter attribute will allow for chaining of this command with others so that items can be fed into it via a pipe bridging the commands together in PowerShell.

An instance of the Sitecore.Data.MasterVariablesReplacer class — which is created by the GetMasterVariablesReplacer() method of the Sitecore.Configuration.Factory class based on the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — is used to expand tokens on the supplied Sitecore item after the item was flagged for editing.

Once tokens have been expanded on the item — or not in the event an exception is encountered — the item is written to the Results window via the WriteItem method which is defined in the BaseCommand class.

I then had to wire up the custom command via a patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
		    <add Name="Custom Commandlets" type="*, CommandLineExtensions" />
      </commandlets>
    </powershell>
  </sitecore>                                                                                     
</configuration>

Let’s take this custom command for a spin.

I created a bunch of test items, and set tokens in their fields. I then selected the following page at random for testing:

page-one-unexpanded-tokens

I opened up the Integrated Scripting Environment of Sitecore PowerShell Extensions, typed in the following PowerShell code, and executed by pressing Ctrl-E:

spe-ise-expand-tokens-page-one

As you can see tokens were expanded on the Page One item:

page-one-expanded-tokens

How about expanding tokens on all descendants of the Home item? Let’s see an example of how we can do that.

I chose the following content item — a grandchild of the Home item — for testing:

inner-page-one-unexpanded-tokens

I switched back over to the Integrated Scripting Environment, wrote the following code for testing — the Get-ChildItem command with the -r parameter (this means do this recursively) will grab all descendants of the Home item, and pipe each item in the result set into the Expand-Token command — and clicked the Execute button:

spe-ise-expand-on-descendants

I then went back to the grandchild item of the Home page in the content tree, and saw that tokens were expanded in its fields:

spe-expanded-on-descendants

If you have any thoughts or comments on this, or ideas for new commands in Sitecore PowerShell Extensions, please share in a comment.

Until next time, have a scriptolicious day!

Expand Tokens on Sitecore Items Using a Custom Command in Revolver

On September 18, 2013, I presented Sitecore from the Command Line at the Sitecore User Group – New England.

During my presentation, I gave an example of creating a custom command in Revolver — the first scripting platform for Sitecore built by Alistair Deneys — and thought I would write something up for those who had missed the presentation, or wanted to revisit what I had shown.

One thing that plagues some Sitecore developers — if you disagree please leave a comment — is not having a nice way to expand tokens on items when tokens are added to Standard Values after items had been created previously.

Newly added tokens “bleed” into preexisting items’ fields, and I’ve seen developers perform crazy feats of acrobatic gymnastics to expand them — writing a standalone web form to recursive crawl the content tree to expand these is such an example (take a look at Empower Your Content Authors to Expand Standard Values Tokens in the Sitecore Client where I offer an alternative way to expand tokens on content items).

The following custom Revolver command will expand tokens on a supplied Sitecore item, and help out on the front of expanding newly added tokens on preexisting items:

using System;
using Sitecore.Configuration;
using System.Linq;

using Sitecore.Data;
using Sitecore.Data.Items;

using Revolver.Core;
using Revolver.Core.Commands;

namespace CommandLineExtensions.Revolver.Commands
{
    public class ExpandTokensCommand : BaseCommand
    {
        private static readonly MasterVariablesReplacer TokenReplacer = Factory.GetMasterVariablesReplacer();

        public override string Description()
        {
            return "Expand tokens on an item";
        }

        public override HelpDetails Help()
        {
            HelpDetails details = new HelpDetails
            {
                Description = Description(),
                Usage = "<cmd> [path]"
            };
            
            details.AddExample("<cmd>");
            details.AddExample("<cmd> /item1/item2");
            return details;
        }

        public override CommandResult Run(string[] args)
        {
            string path = string.Empty;
            if (args.Any())
            {
                path = args.FirstOrDefault();
            }

            using (new ContextSwitcher(Context, path))
            {
                if (!Context.LastGoodPath.EndsWith(path, StringComparison.CurrentCultureIgnoreCase))
                {
                    return new CommandResult
                    (
                        CommandStatus.Failure, 
                        string.Format("Failed to expand tokens on item {0}\nReason:\n\n An item does not exist at that location!", path)
                    );
                }

                CommandResult result;
                Item item = Context.CurrentItem;
                item.Editing.BeginEdit();
                try
                {
                    TokenReplacer.ReplaceItem(item);
                    result = new CommandResult(CommandStatus.Success, string.Concat("Expanded tokens on item ", Context.LastGoodPath));
                    item.Editing.EndEdit();
                }
                catch (Exception ex)
                {
                    item.Editing.CancelEdit();
                    result = new CommandResult(CommandStatus.Failure, string.Format("Failed to expand tokens on item {0}\nReason:\n\n{1}", path, ex));
                }

                return result;
            }
        }
    }
}

Tokens are expanded using an instance of the Sitecore.Data.MasterVariablesReplacer class — you can roll your own, and wire it up in the “MasterVariablesReplacer” setting of your Sitecore instance’s Web.config — which is provided by Sitecore.Configuration.Factory.GetMasterVariablesReplacer().

All custom commands in Revolver must implement the Revolver.Core.ICommand interface. I subclassed Revolver.Core.Commands.BaseCommand — which does implement this interface — since it seemed like the right thing to do given that all “out of the box” commands I saw in Revolver were subclassing it, and then implemented the Description(), Help() and Run() abstract methods.

I then had to bind the custom command to a new name — I chose “et” for “Expand Tokens”:

@echooff
@stoponerror

bind CommandLineExtensions.Revolver.Commands.ExpandTokensCommand,CommandLineExtensions et

@echoon

Since it wouldn’t be efficient to type and run this bind script every time I want to use the “et” command, I added it into a startup script in the core database:

bind-custom-commands-script

I then had to create a user script for the startup script to run. I chose the Everyone role here for demonstration purposes:

bind-custom-commands-user-script

The above startup script will be invoked when Revolver is opened, and our custom command will be bound.

Let’s see all of the above in action.

I added some tokens in my home item:

home-item-unexpanded-tokens

I then opened up Revolver, navigated to /sitecore/content, and ran the custom command on the home item:

ran-et-revolver

As you can see the tokens were expanded:

home-item-expanded-tokens

You might be thinking “that’s wonderful Mike — except now I have to navigate to every item in my content tree using Revolver, and then run this custom command on it”.

Well, I do have a solution for this: a custom script that grabs an item and all of its descendants using a Sitecore query, and passes them to the custom command to expand tokens:

@echooff
@stoponerror

if ($1$ = \$1\$) (exit (Missing required parameter path))

@echoon

query -ns $1$/descendant-or-self::* et

I put this script in the core database, and named it “etr” for “Expand Tokens Recursively”:

etr-script

I navigated to a descendant of /sitecore/content/home, and see that it has some unexpanded tokens on it:

descendant-unexpanded-tokens

I went back to Revolver, and ran the “etr” command on the home item:

etr-revolver

As you can see tokens were expanded on the descendant item:

descendant-expanded-tokens

If you have any thoughts on this, or have ideas for other custom commands in Revolver, please share in a comment.

Navigate to Base Templates of a Template using a Sitecore Command

Have you ever said to yourself when looking at base templates of a template in its Content tab “wouldn’t it be great if I could easily navigate to one of these?”

the-problem-1

I have had this thought more than once despite having the ability to do this in a template’s Inheritance tab — you can do this by clicking one of the base template links listed:

inheritance-tab

For some reason I sometimes forget you have the ability to get to a base template of a template in the Inheritance tab — why I forget is no doubt a larger issue I should try to tackle, albeit I’ll leave that for another day — and decided to build something that will be more difficult for me to forget: launching a dialog via a new item context menu option, and selecting one of the base templates of a template in that dialog.

I decided to atomize functionality in my solution by building custom pipelines/processors wherever I felt doing so made sense.

I started off by building a custom pipeline that gets base templates for a template, and defined a data transfer object (DTO) class for it:

using System.Collections.Generic;

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

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GetBaseTemplatesArgs : PipelineArgs
    {
        public TemplateItem TemplateItem { get; set; }

        public bool IncludeAncestorBaseTemplates { get; set; }

        private List<TemplateItem> _BaseTemplates;
        public List<TemplateItem> BaseTemplates 
        {
            get
            {
                if (_BaseTemplates == null)
                {
                    _BaseTemplates = new List<TemplateItem>();
                }

                return _BaseTemplates;
            }
            set
            {
                _BaseTemplates = value;
            }
        }
    }
}

Client code must supply the template item that will be used as the starting point for gathering base templates, and can request all ancestor base templates — excluding the Standard Template as you will see below — by setting the IncludeAncestorBaseTemplates property to true.

I then created a class with a Process method that will serve as the only pipeline processor for my new pipeline:

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

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

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GetBaseTemplates
    {
        public void Process(GetBaseTemplatesArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.TemplateItem, "args.TemplateItem");
            List<TemplateItem> baseTemplates = new List<TemplateItem>();
            GatherBaseTemplateItems(baseTemplates, args.TemplateItem, args.IncludeAncestorBaseTemplates);
            args.BaseTemplates = baseTemplates;
        }

        private static void GatherBaseTemplateItems(List<TemplateItem> baseTemplates, TemplateItem templateItem, bool includeAncestors)
        {
            if (includeAncestors)
            {
                foreach (TemplateItem baseTemplateItem in templateItem.BaseTemplates)
                {
                    GatherBaseTemplateItems(baseTemplates, baseTemplateItem, includeAncestors);
                }
            }

            if (!IsStandardTemplate(templateItem) && templateItem.BaseTemplates != null && templateItem.BaseTemplates.Any())
            {
                baseTemplates.AddRange(GetBaseTemplatesExcludeStandardTemplate(templateItem.BaseTemplates));
            }
        }

        private static IEnumerable<TemplateItem> GetBaseTemplatesExcludeStandardTemplate(TemplateItem templateItem)
        {
            if (templateItem == null)
            {
                return new List<TemplateItem>();
            }

            return GetBaseTemplatesExcludeStandardTemplate(templateItem.BaseTemplates);
        }

        private static IEnumerable<TemplateItem> GetBaseTemplatesExcludeStandardTemplate(IEnumerable<TemplateItem> baseTemplates)
        {
            if (baseTemplates != null && baseTemplates.Any())
            {
                return baseTemplates.Where(baseTemplate => !IsStandardTemplate(baseTemplate));
            }

            return baseTemplates;
        }

        private static bool IsStandardTemplate(TemplateItem templateItem)
        {
            return templateItem.ID == TemplateIDs.StandardTemplate;
        }
    }
}

Methods in the above class add base templates to a list when the templates are not the Standard Template — I thought it would be a rare occurrence for one to navigate to it, and decided not to include it in the collection.

Further, the method that gathers base templates is recursively executed when client code requests all ancestor base templates be include in the collection.

The next thing I built was functionality to prompt the user for a base template via a dialog, and track which base template was chosen. I decided to do this using a custom client processor, and built the following DTO for it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Web.UI.Sheer;
using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GotoBaseTemplateArgs : ClientPipelineArgs
    {
        public TemplateItem TemplateItem { get; set; }

        public string SelectedBaseTemplateId { get; set; }
    }
}

Just like the other DTO defined above, client code must suppy a template item. The SelectedBaseTemplateId property is set after a user selects a base template in the modal launched by the following class:

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

using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Applications.Dialogs.ItemLister;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Shell.Framework.Pipelines
{
    public class GotoBaseTemplate
    {
        public string SelectTemplateButtonText { get; set; }

        public string ModalIcon { get; set; }

        public string ModalTitle { get; set; }

        public string ModalInstructions { get; set; }

        public void SelectBaseTemplate(GotoBaseTemplateArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.TemplateItem, "args.TemplateItem");
            Assert.ArgumentNotNullOrEmpty(SelectTemplateButtonText, "SelectTemplateButtonText");
            Assert.ArgumentNotNullOrEmpty(ModalIcon, "ModalIcon");
            Assert.ArgumentNotNullOrEmpty(ModalTitle, "ModalTitle");
            Assert.ArgumentNotNullOrEmpty(ModalInstructions, "ModalInstructions");
            
            if (!args.IsPostBack)
            {
                ItemListerOptions itemListerOptions = new ItemListerOptions
                {
                    ButtonText = SelectTemplateButtonText,
                    Icon = ModalIcon,
                    Title = ModalTitle,
                    Text = ModalInstructions
                };

                itemListerOptions.Items = GetBaseTemplateItemsForSelection(args.TemplateItem).Select(template => template.InnerItem).ToList();
                itemListerOptions.AddTemplate(TemplateIDs.Template);
                SheerResponse.ShowModalDialog(itemListerOptions.ToUrlString().ToString(), true);
                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                args.SelectedBaseTemplateId = args.Result;
                args.IsPostBack = false;
            }
            else
            {
                args.AbortPipeline();
            }
        }

        private IEnumerable<TemplateItem> GetBaseTemplateItemsForSelection(TemplateItem templateItem)
        {
            GetBaseTemplatesArgs args = new GetBaseTemplatesArgs
            {
                TemplateItem = templateItem,
                IncludeAncestorBaseTemplates = true,
            };
            CorePipeline.Run("getBaseTemplates", args);
            return args.BaseTemplates;
        }

        public void Execute(GotoBaseTemplateArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(args.SelectedBaseTemplateId, "args.SelectedBaseTemplateId");
            Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", args.SelectedBaseTemplateId), 1);
        }
    }
}

The SelectBaseTemplate method above gives the user a list of base templates to choose from — this includes all ancestor base templates of a template minus the Standard Template.

The title, icon, helper text of the modal are supplied via the processor’s xml node in its configuration file — you’ll see this later on in this post.

Once a base template is chosen, its Id is then set in the SelectedBaseTemplateId property of the GotoBaseTemplateArgs instance.

The Execute method brings the user to the selected base template item in the Sitecore content tree.

Now we need a way to launch the code above.

I did this using a custom command that will be wired up to the item context menu:

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

using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;

using Sitecore.Sandbox.Shell.Framework.Pipelines;
using Sitecore.Web.UI.Sheer;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Commands
{
    public class GotoBaseTemplateCommand : Command
    {
        public override void Execute(CommandContext context)
        {
            Context.ClientPage.Start("gotoBaseTemplate", new GotoBaseTemplateArgs { TemplateItem = GetItem(context) });
        }

        public override CommandState QueryState(CommandContext context)
        {
            if (ShouldEnable(GetItem(context)))
            {
                return CommandState.Enabled;
            }

            return CommandState.Hidden;
        }

        private static bool ShouldEnable(Item item)
        {
            return item != null
                    && IsTemplate(item)
                    && GetBaseTemplates(item).Any();
        }

        private static Item GetItem(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.Items, "context.Items");
            return context.Items.FirstOrDefault();
        }

        private static bool IsTemplate(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return TemplateManager.IsTemplate(item);
        }

        private static IEnumerable<TemplateItem> GetBaseTemplates(TemplateItem templateItem)
        {
            Assert.ArgumentNotNull(templateItem, "templateItem");
            GetBaseTemplatesArgs args = new GetBaseTemplatesArgs 
            { 
                TemplateItem = templateItem, 
                IncludeAncestorBaseTemplates = false 
            };

            CorePipeline.Run("getBaseTemplates", args);
            return args.BaseTemplates;
        }
    }
}

The command above is visible only when the item is a template, and has base templates on it — we invoke the custom pipeline built above to get base templates.

When the command is invoked, we call our custom client processor to prompt the user for a base template to go to.

I then glued everything together using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <sitecore>
    <commands>
      <command name="item:GotoBaseTemplate" type="Sitecore.Sandbox.Commands.GotoBaseTemplateCommand, Sitecore.Sandbox"/>
    </commands>
    <pipelines>
      <getBaseTemplates>
        <processor type="Sitecore.Sandbox.Shell.Framework.Pipelines.GetBaseTemplates, Sitecore.Sandbox"/>
      </getBaseTemplates>
    </pipelines>
    <processors>
      <gotoBaseTemplate>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.GotoBaseTemplate, Sitecore.Sandbox" method="SelectBaseTemplate">
          <SelectTemplateButtonText>OK</SelectTemplateButtonText>
          <ModalIcon>Applications/32x32/nav_up_right_blue.png</ModalIcon>
          <ModalTitle>Select A Base Template</ModalTitle>
          <ModalInstructions>Select the base template you want to navigate to.</ModalInstructions>
        </processor>
        <processor mode="on" type="Sitecore.Sandbox.Shell.Framework.Pipelines.GotoBaseTemplate, Sitecore.Sandbox" method="Execute"/>
      </gotoBaseTemplate>
    </processors>
  </sitecore>
</configuration>

I’ve left out how I’ve added the command shown above 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 how we did.

I first created some templates for testing. The following template named ‘Meta’ uses two other test templates as base templates:

meta-template

I also created a ‘Base Page’ template which uses the ‘Meta’ template above:

base-page-template

Next I created ‘The Coolest Page Template Ever’ template — this uses the ‘Base Page’ template as its base template:

the-coolest-page-template-ever-template

I then right-clicked on ‘The Coolest Page Template Ever’ template to launch its context menu, and selected our new menu option:

context-menu-go-to-base-template

I was then presented with a dialog asking me to select the base template I want to navigate to:

base-template-lister-modal-1

I chose one of the base templates, and clicked ‘OK’:

base-template-lister-modal-2

I was then brought to the base template I had chosen:

brought-to-selected-base-template

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

Chain Together Sitecore Client Commands using a Composite Command

Today, I procrastinated on doing chores around the house by exploring whether one could chain together Sitecore client commands in order to reduce the number of clicks and/or keypresses required when invoking these commands separately — combining the click of the ‘Save’ button followed by one of the publishing buttons would be an example of this.

Immediately, the composite design pattern came to mind for a candidate solution — you can read more about this pattern in the the Gang of Four’s book on design patterns.

This high-level plan of attack lead to the following custom composite command.

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

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Text;

namespace Sitecore.Sandbox.Commands
{
    public class CompositeCommand : Command
    {
        public override void Execute(CommandContext commandContext)
        {
            IEnumerable<Command> commands = GetCommands();

            foreach (Command command in commands)
            {
                command.Execute(commandContext);
            }
        }

        protected IEnumerable<Command> GetCommands()
        {
            ListString commandNames = GetCommandNames();
            IList<Command> commands = new List<Command>();

            foreach(string commandName in commandNames)
            {
                AddToListIfNotNull(commands, CommandManager.GetCommand(commandName));
            }

            return commands;
        }

        private ListString GetCommandNames()
        {
            return new ListString(GetCommandsFieldValue(), '|');
        }

        private string GetCommandsFieldValue()
        {
            XmlNode xmlNode = Factory.GetConfigNode(string.Format("commands/command[@name='{0}']", Name));
            bool canGetCommands = xmlNode != null && xmlNode.Attributes["commands"] != null;

            if (canGetCommands)
            {
                return xmlNode.Attributes["commands"].Value;
            }
            
            return string.Empty;
        }

        private static void AddToListIfNotNull<T>(IList<T> list, T objectToAdd) where T : class
        {
            Assert.ArgumentNotNull(list, "list");
            if (objectToAdd != null)
            {
                list.Add(objectToAdd);
            }
        }
    }
}

The above command — using the Sitecore.Configuration.Factory class — gets its XML configuration element; parses the list of commands it wraps from a new attribute I’ve added — I’ve named this attribute “commands”; gets instances of these commands via Sitecore.Shell.Framework.Commands.CommandManager (from Sitecore.Kernel.dll); and invokes all Command instances’ Execute() methods consecutively — ordered from left to right, separated by pipes, in the “commands” attribute on the command XML element in /App_Config/Commands.config.

To test this out, I thought I’d create a composite command combining the item save command with the publish command — the command that launches the Publish Item Wizard.

I first had to wire up my new command by adding a new command XML element to /App_Config/Commands.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<!-- bunch of commands up here --> 
	<command name="composite:savethenpublish" commands="contenteditor:save|item:publish" type="Sitecore.Sandbox.Commands.CompositeCommand,Sitecore.Sandbox"/>
</configuration>

Next, in the core db, I had to create a reference under the home strip:

save-publish-reference

Followed by a chunk containing a button that holds the name of our new command:

save-publish-chunk-button

Let’s see this new composite command in action. I did the following:

save-and-publish-item

After the Sitecore save animation completed, the Publish Item Wizard popped up:

save-publish-item-2

Now, it’s time for some fun. Let’s combine commands that make little sense in chaining together.

Let’s chain together:

  1. the command I built in my post on expanding Standard Values tokens
  2. the command to move an item before all its siblings in the content tree
  3. the command to move an item down in the content tree

Here’s what the command’s configuration looks like in /App_Config/Commands.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<!-- bunch of commands up here --> 
	<command name="composite:chainsomeunrelatedcommands" commands="item:expandtokens|item:movefirst|item:movedown" type="Sitecore.Sandbox.Commands.CompositeCommand,Sitecore.Sandbox"/>
</configuration>

I decided to add this to the item context menu — for more information on adding to the context menu for items, please check out my post that shows you how to do this — so I created new menu option in the context menu for items in the core database:

chain-command-context-menu-option

I switched back to the master database; added some Standard Values tokens to some fields in the Page 4 item I created for testing; right-clicked on on it; and clicked my new context menu option:

chain-context-menu-option-invoke

After a second or two, I saw that the tokens were expanded, and the item was moved:

chain-context-menu-option-finished

That’s all for now. It’s now time to go do some house chores. Otherwise, people might start thinking I’m truly addicted to building things in Sitecore. 🙂

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.

Display How Many Bucketed Items Live Within a Sitecore Item Bucket Using a Custom DataView

Over the past few weeks — if you haven’t noticed — I’ve been having a blast experimenting with Sitecore Item Buckets. It seems new ideas on what to build for it keep flooding my thoughts everyday. 😀

However, the other day an old idea that I wanted to solve a while back bubbled its way up into the forefront of my consciousness: displaying the count of Bucketed Items which live within each Item Bucket in the Content Tree.

I’m sure someone has built something to do this before though I didn’t really do any research on it as I was up for the challenge.

darth-vader-didnt-read

In all honesty, I enjoy spending my nights after work and on weekends building things in Sitecore — even if someone has built something like it before — as it’s a great way to not only discover new treasures hidden within the Sitecore assemblies, but also improve my programming skills — the saying “you lose it if you don’t use it” applies here.

nerds

You might be asking “Mike, we don’t store that many Sitecore Items in our Item Buckets; I can just go count them all by hand”.

Well, if that’s the case for you then you might want to reconsider why you are using the Item Buckets feature.

However, in theory, thousands if not millions of Items can live within an Item Bucket in Sitecore. If counting by hand is your thing — or even writing some sort of “script” (I’m not referring to PowerShell scripts that you would write using Sitecore PowerShell Extensions (SPE) — I definitely recommend harnessing all of the power this module has to offer — but instead to standalone ASP.NET Web Forms which some people erroneously call “scripts”) to generate some kind of report, then by all means go for it.

counting

That’s just not how I roll.

aint-no-time-for-that

So how are we going to display these counts to the user? We are ultimately going to create a custom Sitecore DataView.

If you aren’t familiar with DataViews in Sitecore, they basically allow you to change how Items are displayed in the Sitecore Content Tree.

I’m not going to go too much into details of how these work. I recommend having a read of the following posts by two fellow Sitecore MVPs for more information and to see other examples:

I do want to warn you: there is a lot of code in this post.

arghhhhh

You might want to go get a snack for this as it might take a while to get through all the code that I am showing here. Don’t worry, I’ll wait for you to get back.

eat-popcorn

Anyways, let’s jump right into it.

partay-meow

For this feature, I want to add a checkbox toggle in the Sitecore Ribbon to give users the ability turn this feature on and off.

bucketed-items-count-view-ribbon

In order to save the state of this checkbox, I defined the following interface:

namespace Sitecore.Sandbox.Web.UI.HtmlControls.Registries
{
    public interface IRegistry
    {
        bool GetBool(string key);

        bool GetBool(string key, bool defaultvalue);

        int GetInt(string key);

        int GetInt(string key, int defaultvalue);

        string GetString(string key);

        string GetString(string key, string defaultvalue);

        string GetValue(string key);

        void SetBool(string key, bool val);

        void SetInt(string key, int val);

        void SetString(string key, string value);

        void SetValue(string key, string value);
    }
}

Classes of the above interface will keep track of settings which need to be stored somewhere.

The following class implements the interface above:

namespace Sitecore.Sandbox.Web.UI.HtmlControls.Registries
{
    public class Registry : IRegistry
    {
        public virtual bool GetBool(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetBool(key);
        }

        public virtual bool GetBool(string key, bool defaultvalue)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetBool(key, defaultvalue);
        }

        public virtual int GetInt(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetInt(key);
        }

        public virtual int GetInt(string key, int defaultvalue)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetInt(key, defaultvalue);
        }

        public virtual string GetString(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetString(key);
        }

        public virtual string GetString(string key, string defaultvalue)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetString(key, defaultvalue);
        }

        public virtual string GetValue(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetValue(key);
        }

        public virtual void SetBool(string key, bool val)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetBool(key, val);
        }

        public virtual void SetInt(string key, int val)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetInt(key, val);
        }

        public virtual void SetString(string key, string value)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetString(key, value);
        }

        public virtual void SetValue(string key, string value)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetValue(key, value);
        }
    }
}

I’m basically wrapping calls to methods on the static Sitecore.Web.UI.HtmlControls.Registry class which is used for saving state on the checkboxes in the Sitecore ribbon — it might be used for keeping track of other things in the Sitecore Content Editor though that is beyond the scope of this post. Nothing magical going on here.

I then defined the following interface for keeping track of Content Editor settings for things related to Item Buckets:

namespace Sitecore.Sandbox.Buckets.Settings
{
    public interface IBucketsContentEditorSettings
    {
        bool ShowBucketedItemsCount { get; set; }

        bool AreItemBucketsEnabled { get; }
    }
}

The ShowBucketedItemsCount boolean property lets the caller know if we are to show the Bucketed Items count, and the AreItemBucketsEnabled boolean property lets the caller know if the Item Buckets feature is enabled in Sitecore.

The following class implements the interface above:

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Determiners.Features;
using Sitecore.Sandbox.Web.UI.HtmlControls.Registries;

namespace Sitecore.Sandbox.Buckets.Settings
{
    public class BucketsContentEditorSettings : IBucketsContentEditorSettings
    {
        protected IFeatureDeterminer ItemBucketsFeatureDeterminer { get; set; }
        
        protected IRegistry Registry { get; set; }

        protected string ShowBucketedItemsCountRegistryKey { get; set; }

        public bool ShowBucketedItemsCount
        {
            get
            {
                return ShouldShowBucketedItemsCount();
            }
            set
            {
                ToggleShowBucketedItemsCount(value);
            }
        }

        public bool AreItemBucketsEnabled
        {
            get
            {
                return GetAreItemBucketsEnabled();
            }
        }

        protected virtual bool ShouldShowBucketedItemsCount()
        {
            if (!AreItemBucketsEnabled)
            {
                return false;
            }

            EnsureRegistryDependencies();
            return Registry.GetBool(ShowBucketedItemsCountRegistryKey, false);
        }

        protected virtual void ToggleShowBucketedItemsCount(bool turnOn)
        {
            
            if (!AreItemBucketsEnabled)
            {
                return;
            }

            EnsureRegistryDependencies();
            Registry.SetBool(ShowBucketedItemsCountRegistryKey, turnOn);
        }

        protected virtual void EnsureRegistryDependencies()
        {
            Assert.IsNotNull(Registry, "Registry must be defined in configuration!");
            Assert.IsNotNullOrEmpty(ShowBucketedItemsCountRegistryKey, "ShowBucketedItemsCountRegistryKey must be defined in configuration!");
        }

        protected virtual bool GetAreItemBucketsEnabled()
        {
            Assert.IsNotNull(ItemBucketsFeatureDeterminer, "ItemBucketsFeatureDeterminer must be defined in configuration!");
            return ItemBucketsFeatureDeterminer.IsEnabled();
        }
    }
}

I’m injecting an IFeatureDeterminer instance into the instance of the class above via the Sitecore Configuration Factory — have a look at the patch configuration file further down in this post — specifically the ItemBucketsFeatureDeterminer which is defined in a previous blog post. The IFeatureDeterminer instance determines whether the Item Buckets feature is turned on/off (I’m not going to repost that code here so if you haven’t seen this code, please go have a look now so you have an understanding of what it’s doing).

Its instance is used in the GetAreItemBucketsEnabled() method which just delegates to its IsEnabled() method and returns the value from that call. The GetAreItemBucketsEnabled() method is used in the get accessor of the AreItemBucketsEnabled property.

I’m also injecting an IRegistry instance into the instance of the class above — this is also defined in the patch configuration file further down — which is used for storing/retrieving the value of the ShowBucketedItemsCount property.

It is leveraged in the ShouldShowBucketedItemsCount() and ToggleShowBucketedItemsCount() methods where a boolean value is saved or retrieved, respectively, in the Sitecore Registry under a certain key — this key is also injected into the ShowBucketedItemsCountRegistryKey property via the Sitecore Configuration Factory.

So, we now have a way to keep track of whether we should display the Bucketed Items count. We just need a way to let the user turn this on/off. To do that, I need to create a custom Sitecore.Shell.Framework.Commands.Command.

Since Sitecore Commands are instantiated by the CreateObject() method on the MainUtil class (this lives in the Sitecore namespace in Sitecore.Kernel.dll and isn’t as advanced as the Sitecore.Configuration.Factory class as it won’t instantiate nested objects defined in configuration as does the Sitecore Configuration Factory), I built the following Command which will decorate Commands defined in Sitecore configuration:

using System.Xml;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Xml;

namespace Sitecore.Sandbox.Shell.Framework.Commands
{
    public class ExtendedConfigCommand : Command
    {
        private Command command;
        protected Command Command
        {
            get
            {
                if(command == null)
                {
                    command = GetCommand();
                    EnsureCommand();
                }

                return command;
            }
        }
        
        protected virtual Command GetCommand()
        {
            XmlNode currentCommandNode = Factory.GetConfigNode(string.Format("commands/command[@name='{0}']", Name));
            string configPath = XmlUtil.GetAttribute("extendedCommandPath", currentCommandNode);
            Assert.IsNotNullOrEmpty(configPath, string.Format("The extendedCommandPath attribute must be set {0}!", currentCommandNode));
            Command command = Factory.CreateObject(configPath, false) as Command;
            Assert.IsNotNull(command, string.Format("The command defined at '{0}' was either not properly set or is not an instance of Sitecore.Shell.Framework.Commands.Command. Double-check it!", configPath));
            return command;
        }

        protected virtual void EnsureCommand()
        {
            Assert.IsNotNull(Command, "GetCommand() cannot return a null Sitecore.Shell.Framework.Commands.Command instance!");
        }

        public override void Execute(CommandContext context)
        {
            Command.Execute(context);
        }

        public override string GetClick(CommandContext context, string click)
        {
            return Command.GetClick(context, click);
        }

        public override string GetHeader(CommandContext context, string header)
        {
            return Command.GetHeader(context, header);
        }

        public override string GetIcon(CommandContext context, string icon)
        {
            return Command.GetIcon(context, icon);
        }

        public override Control[] GetSubmenuItems(CommandContext context)
        {
            return Command.GetSubmenuItems(context);
        }

        public override string GetToolTip(CommandContext context, string tooltip)
        {
            return Command.GetToolTip(context, tooltip);
        }

        public override string GetValue(CommandContext context, string value)
        {
            return Command.GetValue(context, value);
        }

        public override CommandState QueryState(CommandContext context)
        {
            return Command.QueryState(context);
        }
    }
}

The GetCommand() method reads the XmlNode for the current command, and gets the value set on its extendedCommandPath attribute. This value must to be a config path defined under the <sitecore> element in Sitecore configuration.

If the attribute doesn’t exist or is empty, or a Command instance isn’t properly created, an exception is thrown.

Otherwise, it is set on the Command property on the class.

All methods here delegate to the same methods on the Command stored in the Command property.

I then defined the following Command which will be used by the checkbox we are adding to the Sitecore Ribbon:

using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Buckets.Settings;

namespace Sitecore.Sandbox.Buckets.Shell.Framework.Commands
{
    public class ToggleBucketedItemsCountCommand : Command
    {
        protected IBucketsContentEditorSettings BucketsContentEditorSettings { get; set; }

        public override void Execute(CommandContext context)
        {
            if (!AreItemBucketsEnabled())
            {
                return;
            }
            
            ToggleShowBucketedItemsCount();
            Reload();
        }

        protected virtual void ToggleShowBucketedItemsCount()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            BucketsContentEditorSettings.ShowBucketedItemsCount = !BucketsContentEditorSettings.ShowBucketedItemsCount;
        }

        protected virtual void Reload()
        {
            SheerResponse.SetLocation(string.Empty);
        }

        public override CommandState QueryState(CommandContext context)
        {
            if(!AreItemBucketsEnabled())
            {
                return CommandState.Hidden;
            }

            if(!ShouldShowBucketedItemsCount())
            {
                return CommandState.Enabled;
            }

            return CommandState.Down;
        }

        protected virtual bool AreItemBucketsEnabled()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            return BucketsContentEditorSettings.AreItemBucketsEnabled;
        }

        protected virtual bool ShouldShowBucketedItemsCount()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            return BucketsContentEditorSettings.ShowBucketedItemsCount;
        }
    }
}

The QueryState() method determines whether we should display the checkbox — it will only be displayed if the Item Buckets feature is on — and what the state of the checkbox should be — if we are currently showing Bucketed Items count, the checkbox will be checked (this is represented by CommandState.Down). Otherwise, it will be unchecked (this is represented by CommandState.Enabled).

The Execute() method encapsulates the logic of what we are to do when the user checks/unchecks the checkbox. It’s basically delegating to the ToggleShowBucketedItemsCount() method to toggle the value of whether we are to display the Bucketed Items count, and then reloads the Content Editor to refresh the display in the Content Tree.

I then had to define this checkbox in the Core database:

bucketed-items-count-checkbox-core

I’m not going to go into details of how the above works as I’ve written over a gazillion posts on the subject. I recommend having a read of one of these older posts.

After going back to my Master database, I saw the new checkbox in the Sitecore Ribbon:

buckted-items-count-new-checkbox

Since we could be dealing with thousands — if not millions — of Bucketed Items for each Item Bucket, we need a performant way to grab the count of these Items. In this solution, I am leveraging the Sitecore.ContentSearch API to get these counts though needed to add some custom
Computed Index Field classes:

using Sitecore.Buckets.Managers;
using Sitecore.Configuration;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Buckets.Util.Methods;

namespace Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields
{
    public class IsBucketed : AbstractComputedIndexField
    {
        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; private set; }

        public IsBucketed()
        {
            ItemBucketsFeatureMethods = GetItemBucketsFeatureMethods();
            Assert.IsNotNull(ItemBucketsFeatureMethods, "GetItemBucketsFeatureMethods() cannot return null!");
        }

        protected virtual IItemBucketsFeatureMethods GetItemBucketsFeatureMethods()
        {
            IItemBucketsFeatureMethods methods = Factory.CreateObject("buckets/methods/itemBucketsFeatureMethods", false) as IItemBucketsFeatureMethods;
            Assert.IsNotNull(methods, "the IItemBucketsFeatureMethods instance was not defined properly in /sitecore/buckets/methods/itemBucketsFeatureMethods!");
            return methods;
        }

        public override object ComputeFieldValue(IIndexable indexable)
        {
            Item item = indexable as SitecoreIndexableItem;
            if (item == null)
            {
                return null;
            }

            
            return IsBucketable(item) && IsItemContainedWithinBucket(item);
        }

        protected virtual bool IsBucketable(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return BucketManager.IsBucketable(item);
        }

        protected virtual bool IsItemContainedWithinBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(IsItemBucket(item))
            {
                return false;
            }

            return ItemBucketsFeatureMethods.IsItemContainedWithinBucket(item);
        }

        protected virtual bool IsItemBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (!ItemBucketsFeatureMethods.IsItemBucket(item))
            {
                return false;
            }

            return true;
        }
    }
}

An instance of the class above ultimately determines if an Item is bucketed within an Item Bucket, and passes a boolean value to its caller denoting this via its ComputeFieldValue() method.

What determines whether an Item is bucketed? The code above says it’s bucketed only when the Item is bucketable and is contained within an Item Bucket.

The IsBucketable() method above ascertains whether the Item is bucketable by delegating to the IsBucketable() method on the BucketManager class in Sitecore.Buckets.dll.

The IsItemContainedWithinBucket() method determines if the Item is contained within an Item Bucket — you might be laughing as the name on the method is self-documenting — by delegating to the IsItemContainedWithinBucket() method on the IItemBucketsFeatureMethods instance — I’ve defined the code for this in this post so go have a look.

Moreover, the code does not consider Item Buckets to be Bucketed as that just doesn’t make much sense. 😉 This would also give us an inaccurate count.

The following Computed Index Field’s ComputeFieldValue() method returns the string representation of the ancestor Item Bucket’s Sitecore.Data.ID for the Item — if it is contained within an Item Bucket:

using Sitecore.Configuration;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.ContentSearch.Utilities;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Buckets.Util.Methods;

namespace Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields
{
    public class ItemBucketAncestorId : AbstractComputedIndexField
    {
        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; private set; }

        public ItemBucketAncestorId()
        {
            ItemBucketsFeatureMethods = GetItemBucketsFeatureMethods();
            Assert.IsNotNull(ItemBucketsFeatureMethods, "GetItemBucketsFeatureMethods() cannot return null!");
        }

        protected virtual IItemBucketsFeatureMethods GetItemBucketsFeatureMethods()
        {
            IItemBucketsFeatureMethods methods = Factory.CreateObject("buckets/methods/itemBucketsFeatureMethods", false) as IItemBucketsFeatureMethods;
            Assert.IsNotNull(methods, "the IItemBucketsFeatureMethods instance was not defined properly in /sitecore/buckets/methods/itemBucketsFeatureMethods!");
            return methods;
        }

        public override object ComputeFieldValue(IIndexable indexable)
        {
            Item item = indexable as SitecoreIndexableItem;
            if (item == null)
            {
                return null;
            }

            Item itemBucketAncestor = GetItemBucketAncestor(item);
            if(itemBucketAncestor == null)
            {

                return null;
            }

            return NormalizeGuid(itemBucketAncestor.ID);
        }

        protected virtual Item GetItemBucketAncestor(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(IsItemBucket(item))
            {
                return null;
            }

            Item itemBucket = ItemBucketsFeatureMethods.GetItemBucket(item);
            if(!IsItemBucket(itemBucket))
            {
                return null;
            }

            return itemBucket;
        }

        protected virtual bool IsItemBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (!ItemBucketsFeatureMethods.IsItemBucket(item))
            {
                return false;
            }

            return true;
        }

        protected virtual string NormalizeGuid(ID id)
        {
            return IdHelper.NormalizeGuid(id);
        }
    }
}

Not to go too much into details of the class above, it will only return an Item Bucket’s Sitecore.Data.ID as a string if the Item lives within an Item Bucket and is not itself an Item Bucket.

If the Item is not within an Item Bucket or is an Item Bucket, null is returned to the caller via the ComputeFieldValue() method.

I then created the following subclass of Sitecore.ContentSearch.SearchTypes.SearchResultItem — this lives in Sitecore.ContentSearch.dll — in order to use the values in the index that the previous Computed Field Index classes returned for their storage in the search index:

using System.ComponentModel;

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Converters;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;

namespace Sitecore.Sandbox.Buckets.ContentSearch.SearchTypes
{
    public class BucketedSearchResultItem : SearchResultItem
    {
        [IndexField("item_bucket_ancestor_id")]
        [TypeConverter(typeof(IndexFieldIDValueConverter))]
        public ID ItemBucketAncestorId { get; set; }

        [IndexField("is_bucketed")]
        public bool IsBucketed { get; set; }
    }
}

Now, we need a class to get the Bucketed Item count for an Item Bucket. I defined the following interface for class implementations that do just that:

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Buckets.Providers.Items
{
    public interface IBucketedItemsCountProvider
    {
        int GetBucketedItemsCount(Item itemBucket);
    }
}

I then created the following class that implements the interface above:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Xml;

using Sitecore.Configuration;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Linq;
using Sitecore.ContentSearch.Linq.Utilities;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.ContentSearch.Utilities;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Xml;

using Sitecore.Sandbox.Buckets.ContentSearch.SearchTypes;

namespace Sitecore.Sandbox.Buckets.Providers.Items
{
    public class BucketedItemsCountProvider : IBucketedItemsCountProvider
    {
        protected IDictionary<string, ISearchIndex> SearchIndexMap { get; private set; }

        public BucketedItemsCountProvider()
        {
            SearchIndexMap = CreateNewSearchIndexMap();
        }

        protected virtual IDictionary<string, ISearchIndex> CreateNewSearchIndexMap()
        {
            return new Dictionary<string, ISearchIndex>();
        }

        protected virtual void AddSearchIndexMap(XmlNode configNode)
        {
            if(configNode == null)
            {
                return;
            }

            string databaseName = XmlUtil.GetAttribute("database", configNode, null);
            Assert.IsNotNullOrEmpty(databaseName, "The database attribute on the searchIndexMap configuration element cannot be null or the empty string!");
            Assert.ArgumentCondition(!SearchIndexMap.ContainsKey(databaseName), "database", "The searchIndexMap configuration element's database attribute values must be unique!");

            Database database = Factory.GetDatabase(databaseName);
            Assert.IsNotNull(database, string.Format("No database exists with the name of '{0}'! Make sure the database attribute on your searchIndexMap configuration element is set correctly!", databaseName));
            
            string searchIndexName = XmlUtil.GetAttribute("searchIndex", configNode, null);
            Assert.IsNotNullOrEmpty(searchIndexName, "The searchIndex attribute on the searchIndexMap configuration element cannot be null or the empty string!");

            ISearchIndex searchIndex = GetSearchIndex(searchIndexName);
            Assert.IsNotNull(searchIndex, string.Format("No search index exists with the name of '{0}'! Make sure the searchIndex attribute on your searchIndexMap configuration element is set correctly", searchIndexName));

            SearchIndexMap.Add(databaseName, searchIndex);
        }

        public virtual int GetBucketedItemsCount(Item bucketItem)
        {
            Assert.ArgumentNotNull(bucketItem, "bucketItem");

            ISearchIndex searchIndex = GetSearchIndex();
            using (IProviderSearchContext searchContext = searchIndex.CreateSearchContext())
            {
                var predicate = GetSearchPredicate<BucketedSearchResultItem>(bucketItem.ID);
                IQueryable<SearchResultItem> query = searchContext.GetQueryable<BucketedSearchResultItem>().Filter(predicate);
                SearchResults<SearchResultItem> results = query.GetResults();
                return results.Count();
            }
        }

        protected virtual ISearchIndex GetSearchIndex()
        {
            string databaseName = GetContentDatabaseName();
            Assert.IsNotNullOrEmpty(databaseName, "The GetContentDatabaseName() method cannot return null or the empty string!");
            Assert.ArgumentCondition(SearchIndexMap.ContainsKey(databaseName), "databaseName", string.Format("There is no ISearchIndex instance mapped to the database: '{0}'!", databaseName));
            return SearchIndexMap[databaseName];
        }

        protected virtual string GetContentDatabaseName()
        {
            Database database = Context.ContentDatabase ?? Context.Database;
            Assert.IsNotNull(database, "Argggggh! There's no content database! Houston, we have a problem!");
            return database.Name;
        }

        protected virtual ISearchIndex GetSearchIndex(string searchIndexName)
        {
            Assert.ArgumentNotNullOrEmpty(searchIndexName, "searchIndexName");
            return ContentSearchManager.GetIndex(searchIndexName);
        }

        protected virtual Expression<Func<TSearchResultItem, bool>> GetSearchPredicate<TSearchResultItem>(ID itemBucketId) where TSearchResultItem : BucketedSearchResultItem
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(itemBucketId), "itemBucketId", "itemBucketId cannot be null or empty!");
            var predicate = PredicateBuilder.True<TSearchResultItem>();
            predicate = predicate.And(item => item.ItemBucketAncestorId == itemBucketId);
            predicate = predicate.And(item => item.IsBucketed);
            return predicate;
        }
    }
}

Ok, so what’s going on in the class above? The AddSearchIndexMap() method is called by the Sitecore Configuration Factory to add database-to-search-index mappings — have a look at the patch configuration file further below. The code is looking up the appropriate search index for the content/context database.

The GetBucketedItemsCount() method gets the “predicate” from the GetSearchPredicate() method which basically says “Hey, I want an Item that has an ancestor Item Bucket Sitecore.Data.ID which is the same as the Sitecore.Data.ID passed to the method, and also this Item should be bucketed”.

The GetBucketedItemsCount() method then employs the Sitecore.ContentSearch API to get the result-set of the Items for the query, and returns the count of those Items.

Just as Commands, DataViews in Sitecore are instantiated by the CreateObject() method on MainUtil. I want to utilize the Sitecore Configuration Factory instead so that my nested configuration elements are instantiated and injected into my custom DataView. I built the following interface to make that possible:

using System.Collections;

using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

namespace Sitecore.Sandbox.Web.UI.HtmlControls.DataViews
{
    public interface IDataViewBaseExtender
    {
        void FilterItems(ref ArrayList children, string filter);

        void GetChildItems(ItemCollection items, Item item);

        Database GetDatabase();

        Item GetItemFromID(string id, Language language, Version version);

        Item GetParentItem(Item item);

        bool HasChildren(Item item, string filter);

        void Initialize(string parameters);

        bool IsAncestorOf(Item ancestor, Item item);

        void SortItems(ArrayList children, string sortBy, bool sortAscending);
    }
}

All of the methods in the above interface correspond to virtual methods defined on the Sitecore.Web.UI.HtmlControl.DataViewBase class in Sitecore.Kernel.dll.

I then built the following abstract class which inherits from any DataView class that inherits from Sitecore.Web.UI.HtmlControl.DataViewBase:

using System.Collections;

using Sitecore.Collections;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Web.UI.HtmlControls.DataViews
{
    public abstract class ExtendedDataView<TDataView> : DataViewBase where TDataView : DataViewBase
    {
        protected IDataViewBaseExtender DataViewBaseExtender { get; private set; }

        protected ExtendedDataView()
        {
            DataViewBaseExtender = GetDataViewBaseExtender();
            EnsureDataViewBaseExtender();
        }

        protected virtual IDataViewBaseExtender GetDataViewBaseExtender()
        {
            string configPath = GetDataViewBaseExtenderConfigPath();
            Assert.IsNotNullOrEmpty(configPath, "GetDataViewBaseExtenderConfigPath() cannot return null or the empty string!");
            IDataViewBaseExtender dataViewBaseExtender = Factory.CreateObject(configPath, false) as IDataViewBaseExtender;
            Assert.IsNotNull(dataViewBaseExtender, string.Format("the IDataViewBaseExtender instance was not defined properly in '{0}'!", configPath));
            return dataViewBaseExtender;
        }

        protected abstract string GetDataViewBaseExtenderConfigPath();

        protected virtual void EnsureDataViewBaseExtender()
        {
            Assert.IsNotNull(DataViewBaseExtender, "GetDataViewBaseExtender() cannot return a null IDataViewBaseExtender instance!");
        }

        protected override void FilterItems(ref ArrayList children, string filter)
        {
            DataViewBaseExtender.FilterItems(ref children, filter);
        }

        protected override void GetChildItems(ItemCollection items, Item item)
        {
            DataViewBaseExtender.GetChildItems(items, item);
        }

        public override Database GetDatabase()
        {
            return DataViewBaseExtender.GetDatabase();
        }

        protected override Item GetItemFromID(string id, Language language, Version version)
        {
            return DataViewBaseExtender.GetItemFromID(id, language, version);
        }

        protected override Item GetParentItem(Item item)
        {
            return DataViewBaseExtender.GetParentItem(item);
        }

        public override bool HasChildren(Item item, string filter)
        {
            return DataViewBaseExtender.HasChildren(item, filter);
        }

        public override void Initialize(string parameters)
        {
            DataViewBaseExtender.Initialize(parameters);
        }

        public override bool IsAncestorOf(Item ancestor, Item item)
        {
            return DataViewBaseExtender.IsAncestorOf(ancestor, item);
        }

        protected override void SortItems(ArrayList children, string sortBy, bool sortAscending)
        {
            DataViewBaseExtender.SortItems(children, sortBy, sortAscending);
        }
    }
}

The GetDataViewBaseExtender() method gets the config path for the configuration-defined IDataViewBaseExtender — these IDataViewBaseExtender configuration definitions may or may not have nested configuration elements which will also be instantiated by the Sitecore Configuration Factory — from the abstract GetDataViewBaseExtenderConfigPath() method (subclasses must define this method).

The GetDataViewBaseExtender() then employs the Sitecore Configuration Factory to create this IDataViewBaseExtender instance, and return it to the caller (it’s being called in the class’ constructor).

If the instance is null, an exception is thrown.

All other methods in the above class delegate to methods with the same name and parameters on the IDataViewBaseExtender instance.

I then built the following subclass of the abstract class above:

using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Web.UI.HtmlControls.DataViews
{
    public class ExtendedMasterDataView : ExtendedDataView<MasterDataView>
    {
        protected override string GetDataViewBaseExtenderConfigPath()
        {
            return "extendedDataViews/extendedMasterDataView";
        }
    }
}

The above class is used for extending the MasterDataView in Sitecore.

It’s now time for the “real deal” DataView that does what we want: show the Bucketed Item counts for Item Buckets. The instance of the following class does just that:

using System.Collections;

using Sitecore.Buckets.Forms;
using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;

using Sitecore.Sandbox.Buckets.Providers.Items;
using Sitecore.Sandbox.Buckets.Settings;
using Sitecore.Sandbox.Buckets.Util.Methods;
using Sitecore.Sandbox.Web.UI.HtmlControls.DataViews;

namespace Sitecore.Sandbox.Buckets.Forms
{
    public class BucketedItemsCountDataView : BucketDataView, IDataViewBaseExtender
    {
        protected IBucketsContentEditorSettings BucketsContentEditorSettings { get; set; }

        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; }

        protected IBucketedItemsCountProvider BucketedItemsCountProvider { get; set; }

        protected string SingularBucketedItemsDisplayNameFormat { get; set; }

        protected string PluralBucketedItemsDisplayNameFormat { get; set; }

        void IDataViewBaseExtender.FilterItems(ref ArrayList children, string filter)
        {
            FilterItems(ref children, filter);
        }

        void IDataViewBaseExtender.GetChildItems(ItemCollection children, Item parent)
        {
            GetChildItems(children, parent);
        }

        protected override void GetChildItems(ItemCollection children, Item parent)
        {
            
            base.GetChildItems(children, parent);
            if(!ShouldShowBucketedItemsCount())
            {
                return;
            }

            for (int i = children.Count - 1; i >= 0; i--)
            {
                Item child = children[i];
                if (IsItemBucket(child))
                {
                    int count = GetBucketedItemsCount(child);
                    Item alteredItem = GetCountDisplayNameItem(child, count);
                    children.RemoveAt(i);
                    children.Insert(i, alteredItem);
                }
            }
        }

        protected virtual bool ShouldShowBucketedItemsCount()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            return BucketsContentEditorSettings.ShowBucketedItemsCount;
        }

        protected virtual bool IsItemBucket(Item item)
        {
            Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!");
            Assert.ArgumentNotNull(item, "item");
            return ItemBucketsFeatureMethods.IsItemBucket(item);
        }

        protected virtual int GetBucketedItemsCount(Item itemBucket)
        {
            Assert.IsNotNull(BucketedItemsCountProvider, "BucketedItemsCountProvider must be set in configuration!");
            Assert.ArgumentNotNull(itemBucket, "itemBucket");
            return BucketedItemsCountProvider.GetBucketedItemsCount(itemBucket);
        }

        protected virtual Item GetCountDisplayNameItem(Item item, int count)
        {
            FieldList fields = new FieldList();
            item.Fields.ReadAll();
            
            foreach (Field field in item.Fields)
            {
                fields.Add(field.ID, field.Value);
            }

            int bucketedCount = GetBucketedItemsCount(item);
            string displayName = GetItemNameWithBucketedCount(item, bucketedCount);
            ItemDefinition itemDefinition = new ItemDefinition(item.ID, displayName, item.TemplateID, ID.Null);
            return new Item(item.ID, new ItemData(itemDefinition, item.Language, item.Version, fields), item.Database) { RuntimeSettings = { Temporary = true } };
        }

        protected virtual string GetItemNameWithBucketedCount(Item item, int bucketedCount)
        {
            Assert.IsNotNull(SingularBucketedItemsDisplayNameFormat, "SingularBucketedItemsDisplayNameFormat must be set in configuration!");
            Assert.IsNotNull(PluralBucketedItemsDisplayNameFormat, "PluralBucketedItemsDisplayNameFormat must be set in configuration!");

            if (bucketedCount == 1)
            {
                return ReplaceTokens(SingularBucketedItemsDisplayNameFormat, item, bucketedCount);
            }

            return ReplaceTokens(PluralBucketedItemsDisplayNameFormat, item, bucketedCount);
        }

        protected virtual string ReplaceTokens(string format, Item item, int bucketedCount)
        {
            Assert.ArgumentNotNullOrEmpty(format, "format");
            Assert.ArgumentNotNull(item, "item");
            string replaced = format;
            replaced = replaced.Replace("$displayName", item.DisplayName);
            replaced = replaced.Replace("$bucketedCount", bucketedCount.ToString());
            return replaced;
        }

        Database IDataViewBaseExtender.GetDatabase()
        {
            return GetDatabase();
        }

        Item IDataViewBaseExtender.GetItemFromID(string id, Language language, Version version)
        {
            return GetItemFromID(id, language, version);
        }

        Item IDataViewBaseExtender.GetParentItem(Item item)
        {
            return GetParentItem(item);
        }

        bool IDataViewBaseExtender.HasChildren(Item item, string filter)
        {
            return HasChildren(item, filter);
        }

        void IDataViewBaseExtender.Initialize(string parameters)
        {
            Initialize(parameters);
        }

        bool IDataViewBaseExtender.IsAncestorOf(Item ancestor, Item item)
        {
            return IsAncestorOf(ancestor, item);
        }

        void IDataViewBaseExtender.SortItems(ArrayList children, string sortBy, bool sortAscending)
        {
            SortItems(children, sortBy, sortAscending);
        }
    }
}

You might be saying to yourself “Mike, what in the world is going on here?” 😉 Let me explain by starting with the GetChildItems() method.

The GetChildItems() method is used to build up the collection of child Items that display in the Content Tree when you expand a parent node. It does this by populating the ItemCollection instance passed to it.

The particular implementation above is delegating to the base class’ implementation to get the list of child Items for display in the Content Tree.

If we should not show the Bucketed Items count — this is determined by the ShouldShowBucketedItemsCount() method which just returns the boolean value set on the ShowBucketedItemsCount property of the injected IBucketsContentEditorSettings instance — the code just exits.

If we are to show the Bucketed Items count, we iterate over the ItemCollection collection and see if any of these child Items are Item Buckets — this is determined by the IsItemBucket() method.

If we find an Item Bucket, we get its count of Bucketed Items via the GetBucketedItemsCount() method which delegates to the GetBucketedItemsCount() method on the injected IBucketedItemsCountProvider instance.

Once we have the count, we call the GetCountDisplayNameItem() method which populates a FieldList collection with all of the fields defined on the Item Bucket; call the GetItemNameWithBucketedCount() method to get the new display name to show in the Content Tree — this method determines which display name format to use depending on whether we should use singular or pluralized messaging, and expands value on tokens via the ReplaceTokens() method — these tokens are defined in the patch configuration file below; creates an ItemDefinition instance so we can set the new display name; and returns a new Sitecore.Data.Items.Item instance to the caller.

No, don’t worry, we aren’t adding a new Item in the content tree but creating a fake “wrapper” of the real one, and replacing this in the ItemCollection.

We also have to fully implement the IDataViewBaseExtender interface. For most methods, I just delegate to the corresponding methods defined on the base class except for the IDataViewBaseExtender.GetChildItems() method which uses the GetChildItems() method defined above.

I then bridged everything above together via the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <buckets>
      <extendedCommands>
        <toggleBucketedItemsCountCommand type="Sitecore.Sandbox.Buckets.Shell.Framework.Commands.ToggleBucketedItemsCountCommand, Sitecore.Sandbox" singleInstance="on">
          <BucketsContentEditorSettings ref="buckets/settings/bucketsContentEditorSettings" />
        </toggleBucketedItemsCountCommand>
      </extendedCommands>
      <providers>
        <items>
          <bucketedItemsCountProvider type="Sitecore.Sandbox.Buckets.Providers.Items.BucketedItemsCountProvider, Sitecore.Sandbox" singleInstance="true">
            <searchIndexMaps hint="raw:AddSearchIndexMap">
              <searchIndexMap database="master" searchIndex="sitecore_master_index" />
              <searchIndexMap database="web" searchIndex="sitecore_web_index" />
            </searchIndexMaps>
          </bucketedItemsCountProvider>
        </items>
      </providers>
      <settings>
        <bucketsContentEditorSettings type="Sitecore.Sandbox.Buckets.Settings.BucketsContentEditorSettings, Sitecore.Sandbox" singleInstance="true">
          <ItemBucketsFeatureDeterminer ref="determiners/features/itemBucketsFeatureDeterminer"/>
          <Registry ref="registries/registry" />
          <ShowBucketedItemsCountRegistryKey>/Current_User/UserOptions.View.ShowBucketedItemsCount</ShowBucketedItemsCountRegistryKey>
        </bucketsContentEditorSettings>
      </settings>
    </buckets>
    <commands>
      <command name="contenteditor:togglebucketeditemscount" type="Sitecore.Sandbox.Shell.Framework.Commands.ExtendedConfigCommand, Sitecore.Sandbox" extendedCommandPath="buckets/extendedCommands/toggleBucketedItemsCountCommand" />
    </commands>
    <contentSearch>
      <indexConfigurations>
        <defaultLuceneIndexConfiguration>
          <fieldMap>
            <fieldNames>
              <field fieldName="item_bucket_ancestor_id" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
                <analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
              </field>
              <field fieldName="is_bucketed" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Boolean" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
            </fieldNames>
          </fieldMap>
          <documentOptions>
            <fields hint="raw:AddComputedIndexField">
              <field fieldName="item_bucket_ancestor_id">Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields.ItemBucketAncestorId, Sitecore.Sandbox</field>
              <field fieldName="is_bucketed">Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields.IsBucketed, Sitecore.Sandbox</field>
            </fields>
          </documentOptions>
        </defaultLuceneIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
    <dataviews>
      <dataview name="Master">
        <patch:attribute name="assembly">Sitecore.Sandbox</patch:attribute>
        <patch:attribute name="type">Sitecore.Sandbox.Web.UI.HtmlControls.DataViews.ExtendedMasterDataView</patch:attribute>
      </dataview>
    </dataviews>
    <extendedDataViews>
      <extendedMasterDataView type="Sitecore.Sandbox.Buckets.Forms.BucketedItemsCountDataView, Sitecore.Sandbox" singleInstance="true">
        <BucketsContentEditorSettings ref="buckets/settings/bucketsContentEditorSettings" />
        <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
        <BucketedItemsCountProvider ref="buckets/providers/items/bucketedItemsCountProvider" />
        <SingularBucketedItemsDisplayNameFormat>$displayName &lt;span style="font-style: italic; color: blue;"&gt;($bucketedCount bucketed item)&lt;span&gt;</SingularBucketedItemsDisplayNameFormat>
        <PluralBucketedItemsDisplayNameFormat>$displayName &lt;span style="font-style: italic; color: blue;"&gt;($bucketedCount bucketed items)&lt;span&gt;</PluralBucketedItemsDisplayNameFormat>
      </extendedMasterDataView>
    </extendedDataViews>
    <registries>
      <registry type="Sitecore.Sandbox.Web.UI.HtmlControls.Registries.Registry, Sitecore.Sandbox" singleInstance="true" />
    </registries>
  </sitecore>
</configuration>

bridge-collapse

Let’s see this in action:

bucketed-items-count-testing

As you can see, it is working as intended.

partay-hard

Magical, right?

magic

Well, not really — it just appears that way. 😉

magic-not-really

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