Home » Items (Page 5)

Category Archives: Items

Expand Your Scope: Add Additional Axes Via a Custom Sitecore Item Web API itemWebApiRequest Pipeline Processor

The Sitecore Item Web API offers client code the choice of retrieving an Item’s parent, the Item itself, all of its children, or any combination of these by simply setting the scope query string parameter in the request.

For example, if you want an Item’s children, you would only set the scope query string parameter to be the axe “c” — this would be scope=c — or if you wanted all data for the Item and its children, you would just set the scope query string parameter to be the self and children axes separated by a pipe — e.g. scope=s|c. Multiple axes must be separated by a pipe.

The other day, however, for my current project, I realized I needed a way to retrieve all data for an Item and all of its descendants via the Sitecore Item Web API.

The three options that ship with the Sitecore Item Web API cannot help me here, unless I want to make multiple requests to get data for an Item and all of it’s children, and then loop over all children and get their children, ad infinitum (well, hopefully it does stop somewhere).

Such a solution would require more development time — I would have to write additional code to do all of the looping — and this would — without a doubt — yield poorer performance versus getting all data upfront in a single request.

Through my excavating efforts in \App_Config\Include\Sitecore.ItemWebApi.config and Sitecore.ItemWebApi.dll, I discovered we can replace this “out of the box” functionality — this lives in /configuration/sitecore/pipelines/itemWebApiRequest/processor[@type=”Sitecore.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.ItemWebApi”] in the Sitecore.ItemWebApi.config — via a custom itemWebApiRequest pipeline processor.

I thought it would be a good idea to define each of our scope operations in its own pipeline processor, albeit have all of these pipeline processors be nested within our itemWebApiRequest pipeline processor.

For the lack of a better term, I’m calling each of these a scope sub-pipeline processor (if you can think of a better name, or have seen this approach done before, please drop a comment).

The first thing I did was create a custom processor class to house two additional properties for our sub-pipeline processor:

using System.Xml;

using Sitecore.Pipelines;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request
{
    public class ScopeProcessor : Processor
    {
        public string Suppresses { get; private set; }
        public string QueryString { get; private set; }

        public ScopeProcessor(XmlNode configNode)
            : base(GetAttributeValue(configNode, "name"), GetAttributeValue(configNode, "type"), GetAttributeValue(configNode, "methodName"))
        {
            Suppresses = GetAttributeValue(configNode, "suppresses");
            QueryString = GetAttributeValue(configNode, "queryString");
        }
        public ScopeProcessor(string name, string type, string methodName, string suppresses, string queryString)
            : base(name, type, methodName)
        {
            Suppresses = suppresses;
            QueryString = queryString;
        }

        private static string GetAttributeValue(XmlNode configNode, string attributeName)
        {
            Assert.ArgumentNotNull(configNode, "configNode");
            Assert.ArgumentNotNullOrEmpty(attributeName, "attributeName");
            XmlAttribute attribute = configNode.Attributes[attributeName];

            if (attribute != null)
            {
                return attribute.Value;
            }

            return string.Empty;
        }
    }
}

The QueryString property will contain the axe for the given scope, and Suppresses property maps to another scope sub-pipeline processor query string value that will be ignored when both are present.

I then created a new PipelineArgs class for the scope sub-pipeline processors:

using System.Collections.Generic;

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

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request.DTO
{
    public class ScopeProcessorRequestArgs : PipelineArgs
    {
        private List<Item> _Items;
        public List<Item> Items
        {
            get
            {
                if (_Items == null)
                {
                    _Items = new List<Item>();
                }

                return _Items;
            }
            set
            {
                _Items = value;
            }
        }


        private List<Item> _Scope;
        public List<Item> Scope
        {
            get
            {
                if (_Scope == null)
                {
                    _Scope = new List<Item>();
                }

                return _Scope;
            }
            set
            {
                _Scope = value;
            }
        }

        public ScopeProcessorRequestArgs()
        {
        }
    }
}

Basically, the above class just holds Items that will be processed, and keeps track of Items in scope — these Items are added via the scope sub-pipeline processors for the supplied axes.

Now it’s time for our itemWebApiRequest pipeline processor:

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

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Request;
using Sitecore.Web;

using Sitecore.Sandbox.ItemWebApi.Pipelines.Request.DTO;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Request
{
    public class ResolveScope : RequestProcessor
    {
        private IDictionary<string, ScopeProcessor> _ScopeProcessors;
        private IDictionary<string, ScopeProcessor> ScopeProcessors
        {
            get
            {
                if(_ScopeProcessors == null)
                {
                    _ScopeProcessors = new Dictionary<string, ScopeProcessor>();
                }

                return _ScopeProcessors;
            }
        }

        public override void Process(RequestArgs arguments)
        {
            if(!HasItemsInSet(arguments))
            {
                return;
            }

            arguments.Scope = GetItemsInScope(arguments).ToArray();
        }

        private static bool HasItemsInSet(RequestArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            Assert.ArgumentNotNull(arguments.Items, "arguments.Items");
            if (arguments.Items.Length < 1)
            {
                Logger.Warn("Cannot resolve the scope because the item set is empty.");
                arguments.Scope = new Item[0];
                return false;
            }

            return true;
        }

        private IEnumerable<Item> GetItemsInScope(RequestArgs arguments)
        {
            List<Item> itemsInScope = new List<Item>();
            foreach (Item item in arguments.Items)
            {
                ScopeProcessorRequestArgs args = new ScopeProcessorRequestArgs();
                args.Items.Add(item);
                GetItemsInScope(args);
                itemsInScope.AddRange(args.Scope);
            }

            return itemsInScope;
        }

        private void GetItemsInScope(ScopeProcessorRequestArgs arguments)
        {
            IEnumerable<ScopeProcessor> scopeProcessors = GetScopeProcessorsForRequest();
            foreach (ScopeProcessor scopeProcessor in scopeProcessors)
            {
                scopeProcessor.Invoke(arguments);
            }
        }

        private IEnumerable<ScopeProcessor> GetScopeProcessorsForRequest()
        {
            List<ScopeProcessor> scopeProcessors = GetScopeProcessorsForAxes();
            List<ScopeProcessor> scopeProcessorsForRequest = new List<ScopeProcessor>();
            foreach(ScopeProcessor scopeProcessor in scopeProcessors)
            {
                bool canAddProcessor = !scopeProcessors.Exists(processor => processor.Suppresses.Equals(scopeProcessor.QueryString));
                if (canAddProcessor)
                {
                    scopeProcessorsForRequest.Add(scopeProcessor);
                }
            }

            return scopeProcessorsForRequest;
        }

        private List<ScopeProcessor> GetScopeProcessorsForAxes()
        {
            List<ScopeProcessor> scopeProcessors = new List<ScopeProcessor>();
            foreach (string axe in GetAxes())
            {
                ScopeProcessor scopeProcessor;
                ScopeProcessors.TryGetValue(axe, out scopeProcessor);
                if(scopeProcessor != null && !scopeProcessors.Contains(scopeProcessor))
                {
                    scopeProcessors.Add(scopeProcessor);
                }
            }

            return scopeProcessors;
        }

        private IEnumerable<string> GetAxes()
        {
            string queryString = WebUtil.GetQueryString("scope", null);
            if (string.IsNullOrWhiteSpace(queryString))
            {
                return new string[] { "s" };
            }

            return queryString.Split(new char[] { '|' }).Distinct();
        }

        private IEnumerable<string> GetScopeProcessorQueryStringValues()
        {
            return ScopeProcessors.Values.Select(scopeProcessors => scopeProcessors.QueryString).ToList();
        }

        public virtual void AddScopeProcessor(XmlNode configNode)
        {
            ScopeProcessor scopeProcessor = new ScopeProcessor(configNode);
            bool canAdd = !string.IsNullOrEmpty(scopeProcessor.QueryString)
                            && !ScopeProcessors.ContainsKey(scopeProcessor.QueryString);

            if (canAdd)
            {
                ScopeProcessors.Add(scopeProcessor.QueryString, scopeProcessor);
            }
        }

        public virtual void AddItemSelf(ScopeProcessorRequestArgs arguments)
        {
            foreach (Item item in arguments.Items)
            {
                arguments.Scope.AddRange(GetCanBeReadItems(new Item[] { item }));
            }
        }

        public virtual void AddItemParent(ScopeProcessorRequestArgs arguments)
        {
            foreach (Item item in arguments.Items)
            {
                arguments.Scope.AddRange(GetCanBeReadItems(new Item[] { item.Parent }));
            }
        }
            
        public virtual void AddItemDescendants(ScopeProcessorRequestArgs arguments)
        {
            foreach (Item item in arguments.Items)
            {
                arguments.Scope.AddRange(GetCanBeReadItems(item.Axes.GetDescendants()));
            }
        }

        public virtual void AddItemChildren(ScopeProcessorRequestArgs arguments)
        {
            foreach(Item item in arguments.Items)
            {
                arguments.Scope.AddRange(GetCanBeReadItems(item.GetChildren()));
            }
        }

        private static IEnumerable<Item> GetCanBeReadItems(IEnumerable<Item> list)
        {
            if (list == null)
            {
                return new List<Item>();
            }

            return list.Where(item => CanReadItem(item));
        }

        private static bool CanReadItem(Item item)
        {
            return Context.Site.Name != "shell"
                    && item.Access.CanRead()
                    && item.Access.CanReadLanguage();
        }
    }
}

When this class is instantiated, each scope sub-pipeline processor is added to a dictionary, keyed by its query string axe value.

When this processor is invoked, it performs some validation — similarly to what is being done in the “out of the box” Sitecore.ItemWebApi.Pipelines.Request.ResolveScope class — and determines which scope processors are applicable for the given request. Only those that found in the dictionary via the supplied axes are used, minus those that are suppressed.

Once the collection of scope sub-pipeline processors is in place, each are invoked with a ScopeProcessorRequestArgs instance containing an Item to be processed.

When a scope sub-pipeline processor is done executing, Items that were retrieved from it are added into master list of scope Items to be returned to the caller.

I then glued all of this together — including the scope sub-pipeline processors — in \App_Config\Include\Sitecore.ItemWebApi.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- stuff is defined up here too -->
      <itemWebApiRequest>
		<!-- stuff is defined up here -->
		<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox">
		  <scopeProcessors hint="raw:AddScopeProcessor">
			<scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemSelf" name="self" queryString="s" />
			<scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemParent" name="parent" queryString="p" />
			<scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemDescendants" name="recursive" queryString="r" suppresses="c" />
			<scopeProcessor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Request.ResolveScope, Sitecore.Sandbox" methodName="AddItemChildren" name="children" queryString="c" />
		  </scopeProcessors>
		</processor>
		<!-- some stuff is defined down here -->
		</itemWebApiRequest>
    </pipelines>
      <!-- there's more stuff defined down here -->
  </sitecore>
</configuration>

Let’s take the above for a spin.

First we need some items for testing. Lucky for me, I hadn’t cleaned up after myself when creating a previous blog post — yes, now I have a legitimate excuse for not picking up after myself — so let’s use these for testing:

items-in-sitecore-scope

After modifying some code in my copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK — I updated which item we are requesting conjoined for our scope query string parameter (scope=r) — I launched it to retrieve our test items:

scope-request-output

If you have any thoughts on this, or ideas on improving the above, please leave a comment.

Go Green: Put Items in the Recycle Bin When Deleting Via the Sitecore Item Web API

This morning I discovered that items are permanently deleted by the Sitecore Item Web API during a delete action. This is probably called out somewhere in its developer’s guide but I don’t recall having read this.

Regardless of whether it’s highlighted somewhere in documentation, I decided to investigate why this happens.

After combing through Sitecore Item Web API pipelines defined in \App_Config\Include\Sitecore.ItemWebApi.config and code in Sitecore.ItemWebApi.dll, I honed in on the following:

delete-scope-bye-bye

This above code lives in the only itemWebApiDelete pipeline processor that comes with the Sitecore Item Web API, and this processor can be found at /configuration/sitecore/pipelines/itemWebApiDelete/processor[@type=”Sitecore.ItemWebApi.Pipelines.Delete.DeleteScope, Sitecore.ItemWebApi”] in the \App_Config\Include\Sitecore.ItemWebApi.config file.

I don’t know about you, but I’m not always comfortable with deleting items permanently in Sitecore. I heavily rely on Sitecore’s Recycle Bin — yes, I have deleted items erroneously in the past, but recovered quickly by restoring them from the Recycle Bin (I hope I’m not the only one who has done this. :-/)

Unearthing the above prompted me to write a new itemWebApiDelete pipeline processor that puts items in the Recycle Bin when the Recycle Bin setting — see /configuration/sitecore/settings/setting[@name=”RecycleBinActive”] in the Web.config — is enabled:

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

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Delete;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Delete
{
    public class RecycleScope : DeleteProcessor
    {
        private const int OKStatusCode = 200;

        public override void Process(DeleteArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            IEnumerable<Item> itemsToDelete = arguments.Scope;
            DeleteItems(itemsToDelete);
            arguments.Result = GetStatusInformation(OKStatusCode, GetDeletionInformation(itemsToDelete));
        }

        private static void DeleteItems(IEnumerable<Item> itemsToDelete)
        {
            foreach (Item itemToDelete in itemsToDelete)
            {
                DeleteItem(itemToDelete);
            }
        }

        private static void DeleteItem(Item itemToDelete)
        {
            Assert.ArgumentNotNull(itemToDelete, "itemToDelete");

            // put items in the recycle bin if it's turned on
            if (Settings.RecycleBinActive)
            {
                itemToDelete.Recycle();
            }
            else
            {
                itemToDelete.Delete();
            }
        }

        private static Dynamic GetDeletionInformation(IEnumerable<Item> itemsToDelete)
        {
            return GetDeletionInformation(itemsToDelete.Count(), GetItemIds(itemsToDelete));
        }

        private static Dynamic GetDeletionInformation(int count, IEnumerable<ID> itemIds)
        {
            Dynamic deletionInformation = new Dynamic();
            deletionInformation["count"] = count;
            deletionInformation["itemIds"] = itemIds.Select(id => id.ToString());
            return deletionInformation;
        }

        private static IEnumerable<ID> GetItemIds(IEnumerable<Item> items)
        {
            Assert.ArgumentNotNull(items, "items");
            return items.Select(item => item.ID);
        }

        private static Dynamic GetStatusInformation(int statusCode, Dynamic result)
        {
            Assert.ArgumentNotNull(result, "result");
            Dynamic status = new Dynamic();
            status["statusCode"] = statusCode;
            status["result"] = result;
            return status;
        }
    }
}

There really isn’t anything magical about the code above. It utilizes most of the same logic that comes with the itemWebApiDelete pipeline processor that ships with the Sitecore Item Web API, although I did move code around into new methods.

The only major difference is the invocation of the Recycle method on item instances when the Recycle Bin is enabled in Sitecore. If the Recycle Bin is not enabled, we call the Delete method instead — as does the “out of the box” pipeline processor.

I then replaced the existing itemWebApiDelete pipeline processor in \App_Config\Include\Sitecore.ItemWebApi.config with our new one defined above:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- stuff is defined up here -->
      <itemWebApiDelete>
        <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Delete.RecycleScope, Sitecore.Sandbox" />
	  </itemWebApiDelete>
      <!-- there's more stuff defined down here -->
  </sitecore>
</configuration>

Let’s see this in action.

We first need a test item. Let’s create one together:

recycle-item-web-api-delete-test-item

I then tweaked the delete method in my copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK, to point to our test item in the master database — I have omitted this code for the sake of brevity — and then ran the console application calling the delete method only:

test-item-delete-console-response

As you can see, our test item is now in the Recycle Bin:

test-item-recycle-bin

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

Add Additional Item Properties in Sitecore Item Web API Responses

The other day I was exploring pipelines of the Sitecore Item Web API, and took note of the itemWebApiGetProperties pipeline. This pipeline adds information about an item in the response returned by the Sitecore Item Web API. You can find this pipeline at /configuration/sitecore/pipelines/itemWebApiGetProperties in \App_Config\Include\Sitecore.ItemWebApi.config.

The following properties are set for an item in the response via the lonely pipeline processor — /configuration/sitecore/pipelines/itemWebApiGetProperties/processor[@type=”Sitecore.ItemWebApi.Pipelines.GetProperties.GetProperties, Sitecore.ItemWebApi”] — that ships with the Sitecore Item Web API:

out-of-the-box-properties

Here’s an example of what the properties set by the above pipeline processor look like in the response — I invoked a read request to the Sitecore Item Web API via a copy of the console application written by Kern Herskind Nightingale, Director of Technical Services at Sitecore UK:

properties-out-of-box-console

You might be asking “how difficult would it be to add in my own properties?” It’s not difficult at all!

I whipped up the following itemWebApiGetProperties pipeline processor to show how one can add more properties for an item:

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.GetProperties;

namespace Sitecore.Sandbox.ItemWebApi.Pipelines.GetProperties
{
    public class GetEvenMoreProperties : GetPropertiesProcessor
    {
        public override void Process(GetPropertiesArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            arguments.Properties.Add("ParentID", arguments.Item.ParentID.ToString());
            arguments.Properties.Add("ChildrenCount", arguments.Item.Children.Count);
            arguments.Properties.Add("Level", arguments.Item.Axes.Level);
            arguments.Properties.Add("IsItemClone", arguments.Item.IsItemClone);
            arguments.Properties.Add("CreatedBy", arguments.Item["__Created by"]);
            arguments.Properties.Add("UpdatedBy", GetItemUpdatedBy(arguments.Item));
        }

        private static Dynamic GetItemUpdatedBy(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            string[] usernamePieces = item["__Updated by"].Split('\\');

            Dynamic username = new Dynamic();
            if (usernamePieces.Length > 1)
            {
                username["Domain"] = usernamePieces[0];
                username["Username"] = usernamePieces[1];
            }
            else if (usernamePieces.Length > 0)
            {
                username["Username"] = usernamePieces[0];
            }

            return username;
        }
    }
}

The ParentID, ChildrenCount, Level and IsItemClone properties are simply added to the properties SortedDictionary within the GetPropertiesArgs instance, and will be serialized as is.

For the UpdatedBy property, I decided to leverage the Sitecore.ItemWebApi.Dynamic class in order to have the username set in the “__Updated by” field be represented by a JSON object. This JSON object sets the domain and username — without the domain — into different JSON properties.

As a side note — when writing your own service code for the Sitecore Item Web API — I strongly recommend using instances of the Sitecore.ItemWebApi.Dynamic class — or something similar — for complex objects. Developers writing code to consume your JSON will thank you many times for it. 🙂

I registered my new processor to the itemWebApiGetProperties pipeline in my Sitecore instance’s \App_Config\Include\Sitecore.ItemWebApi.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- there's stuff here -->
      <itemWebApiGetProperties>
        <processor type="Sitecore.ItemWebApi.Pipelines.GetProperties.GetProperties, Sitecore.ItemWebApi" />
        <processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.GetProperties.GetEvenMoreProperties, Sitecore.Sandbox" />
      </itemWebApiGetProperties>
      <!-- there's stuff here as well -->
  </sitecore>
</configuration>

Let’s take this for a spin.

I ran the console application again to see what the response now looks like:

additional-properties

As you can see, our additional properties are now included in the response.

If you can think of other item properties that would be useful for Sitecore Item Web API client applications, please share in a comment.

Until next time, have a Sitecorelicious day!

Replace Proxies With Clones in the Sitecore CMS

The other day I stumbled upon a thread in the Sitecore Developer Network (SDN) forums that briefly touched upon replacing proxies with clones, and I wondered whether anyone had built any sort of tool that creates clones for items being proxied — basically a tool that would automate creating clones from proxies — and removes the proxies once the clones are in place.

Since I am not aware of such a tool — not to mention that I’m hooked on programming and just love coding — I decided to create one.

The following command is my attempt at such a tool:

using System;
using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Data.Proxies;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;

namespace Sitecore.Sandbox.Commands
{
    public class TransformProxyToClones : Command
    {
        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            TransformProxyItemToClones(GetContextItem(context));
        }

        private static void TransformProxyItemToClones(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(!CanTransform(item))
            {
                return;
            }

            string proxyType = GetProxyType(item);
            Item source = GetItem(GetSourceItemFieldValue(item));
            Item target = GetItem(GetTargetItemFieldValue(item));
            
            if (AreEqualIgnoreCase(proxyType, "Entire sub-tree"))
            {
                DeleteItem(item);
                CloneEntireSubtree(source, target);
            }
            else if (AreEqualIgnoreCase(proxyType, "Root item only"))
            {
                DeleteItem(item);
                CloneRootOnly(source, target);
            }
        }

        private static void CloneEntireSubtree(Item source, Item destination)
        {
            Clone(source, destination, true);
        }
        
        private static void CloneRootOnly(Item root, Item destination)
        {
            Clone(root, destination, false);
        }

        private static Item Clone(Item cloneSource, Item cloneDestination, bool deep)
        {
            Assert.ArgumentNotNull(cloneSource, "cloneSource");
            Assert.ArgumentNotNull(cloneDestination, "cloneDestination");
            return cloneSource.CloneTo(cloneDestination, deep);
        }

        private static void DeleteItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (Settings.RecycleBinActive)
            {
                item.Recycle();
            }
            else
            {
                item.Delete();
            }
        }

        public override CommandState QueryState(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            if (CanTransform(GetContextItem(context)))
            {
                return CommandState.Enabled;
            }

            return CommandState.Hidden;
        }

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

        private static bool CanTransform(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return IsProxy(item)
                    && IsSourceDatabaseFieldEmpty(item)
                    && !string.IsNullOrWhiteSpace(GetProxyType(item))
                    && !string.IsNullOrWhiteSpace(GetSourceItemFieldValue(item))
                    && !string.IsNullOrWhiteSpace(GetTargetItemFieldValue(item));
        }

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

        private static string GetProxyType(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item["Proxy type"];
        }

        private static bool IsSourceDatabaseFieldEmpty(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return string.IsNullOrWhiteSpace(item["Source database"]);
        }

        private static string GetSourceItemFieldValue(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item["Source item"];
        }

        private static string GetTargetItemFieldValue(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item["Target item"];
        }
        
        private static Item GetItem(string path)
        {
            Assert.ArgumentNotNullOrEmpty(path, "path");
            return Context.ContentDatabase.GetItem(path);
        }

        private static bool AreEqualIgnoreCase(string one, string two)
        {
            return string.Equals(one, two, StringComparison.CurrentCultureIgnoreCase);
        }
    }
}

The above command is only visible for proxy items having both source and target items set, and the proxy is for the context content database.

When the command is invoked, the source item — conjoined with its descendants if its sub-tree is also being proxied — is cloned to the target item, after the proxy definition item is deleted.

I registered the above command in Sitecore using an include configuration file:

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

I also wired this up in the core database for the item context menu (I’ve omitted a screenshot on how this is done; If you would like to see how this is done, please see part 1 and part 2 of my post showing how to add to the item context menu).

Let’s take this for a drive.

I created a bunch of items for testing:

proxy-test-tree-created-items

I then created a proxy for these test items:

proxy-item-sub-tree

I then right-clicked on our test proxy item to launch its context menu, and then clicked on the “Transform Proxy To Clones” menu option:

transform-proxy-to-clones-context

The proxy item was removed, and we now have clones:

proxy-gone-now-clones

If you can think of any other ideas around proxies or clones, or know of other tools that create clones from proxies, please leave a comment.

Show Clones of An Item In the Sitecore CMS

“Out of the box”, there is no specific way to see all clones for a given item — at least I haven’t found one yet.

One could see these by looking at referrers of an item:

navigate-links-dropdown-clones

Unfortunately, referrers in this dropdown aren’t just reserved for clones — all referrers of the item will display in this dropdown. An example would include an item referencing the item in a Droplink field.

In a previous post, I provided a solution for auto-cloning new subitems to clones of their parents. That solution leveraged an instance of the ItemClonesGatherer utility class I defined in that post to return a collection of clones for a given item.

Yesterday, I realized this class could be reused for a feature to show a listing of clones for an item in Sitecore, and this post showcases that solution.

I had to come up with a medium for displaying a list of clones of an item. I decided I would display these in a new content editor tab.

I recalled reading an article by Sitecore MVP Mark Stiles on adding new Editor tabs in Sitecore.

As Mark Stiles had done in his post, I created a stand-alone ASP.NET web form (/sitecore modules/Shell/ShowClones/default.aspx). This web form will display the content of our new “Show Clones” tab:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Sitecore650rev120706.sitecore_modules.Shell.ShowClones.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Show Clones</title>
    <style type="text/css">
        h1
        {
            color: #072d6b;
            font-size: 14pt;
            margin-bottom: 5px;
        }
        a
        {
            text-decoration: none;
            font-size: 8pt;
            font-family: Tahoma;
        }
        a:hover
        {
            text-decoration: underline;
        }
        #clones
        {
            margin: 10px;
        }
        #clone_listing
        {
            list-style-type: none;
            margin: 0 0 0 15px;
        }
        #clone_listing li
        {
            margin: 0;
        }
        #clone_listing li + li
        {
            margin-top: 5px;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
        <div id="clones">
            <h1>Clones</h1>
            <asp:MultiView ID="mvClones" ActiveViewIndex="0" runat="server">
                <asp:View ID="vClones" runat="server">
                    <asp:Repeater ID="rptClones" runat="server">
                        <HeaderTemplate>
                            <ol id="clone_listing">
                        </HeaderTemplate>
                        <ItemTemplate>
                            <li>
                                <asp:HyperLink 
                                    ID="hlClone" 
                                    NavigateUrl="#"
                                    onclick='<%# string.Format("parent.scForm.invoke(\"item:load(id={0})\"); return false;", Eval("ID").ToString()) %>'
                                    Text='<%# Eval("Paths.FullPath") %>'
                                    runat="server" />
                            </li>
                        </ItemTemplate>
                        <FooterTemplate>
                            </ol>
                        </FooterTemplate>
                    </asp:Repeater>
                </asp:View>
                <asp:View ID="vNoClones" runat="server">
                    <script type="text/javascript">
                        parent.scContent.closeEditorTab('ShowClones');
                    </script>
                </asp:View>
            </asp:MultiView>
        </div>
    </form>
</body>
</html>

The web form’s code-behind:

using System;
using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Web;

using Sitecore.Sandbox.Utilities.Gatherers.Base;
using Sitecore.Sandbox.Utilities.Gatherers;

namespace Sitecore650rev120706.sitecore_modules.Shell.ShowClones
{
    public partial class Default : System.Web.UI.Page
    {
        private static readonly IItemsGatherer ClonesGatherer = ItemClonesGatherer.CreateNewItemClonesGatherer();

        protected void Page_Load(object sender, EventArgs e)
        {
            ShowClones();
        }

        private void ShowClones()
        {
            BindRepeater();
            ToggleViews();
        }

        private void BindRepeater()
        {
            rptClones.DataSource = GetClones();
            rptClones.DataBind();
        }

        private void ToggleViews()
        {
            if (rptClones.Items.Count > 0)
            {
                mvClones.SetActiveView(vClones);
            }
            else
            {
                mvClones.SetActiveView(vNoClones);
            }
        }

        private IEnumerable<Item> GetClones()
        {
            Item item = GetItem();
            if(item == null)
            {
                return new List<Item>();
            }

            ClonesGatherer.Source = item;
            return ClonesGatherer.Gather();
        }

        private Item GetItem()
        {
            Item item = null;

            try
            {
                item = Sitecore.Context.ContentDatabase.GetItem(GetID(), GetLanguage(), GetVersion());
            }
            catch(Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }

            return item;
        }

        private ID GetID()
        {
            Sitecore.Data.ID id;
            if(Sitecore.Data.ID.TryParse(WebUtil.GetQueryString("id"), out id))
            {
                return id;
            }

            return Sitecore.Data.ID.Null;
        }

        private Language GetLanguage()
        {
            Language language;
            if(Language.TryParse(WebUtil.GetQueryString("la"), out language))
            {
                return language;
            }

            return Sitecore.Context.Language;
        }

        private Sitecore.Data.Version GetVersion()
        {
            Sitecore.Data.Version version;
            if (Sitecore.Data.Version.TryParse(WebUtil.GetQueryString("vs"), out version))
            {
                return version;
            }

            return Sitecore.Data.Version.Latest;
        }
    }
}

The code-behind above uses an instance of the ItemClonesGatherer class to get all clones for the passed item. If clones are found, these are bound to a repeater to display them as links.

If there are no clones, the Multiview in the web form is toggled to render JavaScript to close the tab — the tab should not be open if the item does not have any clones.

After revisiting Mark’s article, and realized I needed a different solution: one that will allow me to add a new tab on the fly.

Such a solution should only allow the tab to open when an item has clones, and it should not be be associated with any templates — it must be template oblivious.

I stumbled upon a command in one of the Sitecore DLLs — which one it was is evading me at the moment — and noticed it was using an instance of Sitecore.Web.UI.Framework.Scripts.ShowEditorTab. I decided to take a chance on using an instance of this object, hoping it might open up a new content editor tab on the fly.

Using that command as a model, I came up with the following command:

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

using Sitecore.Sandbox.Utilities.Gatherers.Base;
using Sitecore.Sandbox.Utilities.Gatherers;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Resources;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Text;
using Sitecore.Web;
using Sitecore.Web.UI.Framework.Scripts;
using Sitecore.Web.UI.Sheer;

namespace Sitecore.Sandbox.Commands
{
    public class ShowClones : Command
    {
        private static readonly IItemsGatherer ClonesGatherer = ItemClonesGatherer.CreateNewItemClonesGatherer();

        public override void Execute(CommandContext commandContext)
        {
            ShowClonesEditorTab(GetItem(commandContext));
        }

        private static void ShowClonesEditorTab(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            const string command = "contenteditor:pagedesigner";
            const string id = "ShowClones";

            bool shouldClickEditorTab = IsEditorTabOpen(command);

            if (IsEditorTabOpen(command))
            {
                ClickOpenEditorTab(id);
                return;
            }
            
            OpenEditorTab(CreateNewShowClonesEditorTab(item, command, id));
        }

        private static bool IsEditorTabOpen(string command)
        {
            Assert.ArgumentNotNullOrEmpty(command, "command");
            return WebUtil.GetFormValue("scEditorTabs").Contains(command);
        }

        private static void ClickOpenEditorTab(string id)
        {
            Assert.ArgumentNotNullOrEmpty(id, "id");
            SheerResponse.Eval(string.Format("scContent.onEditorTabClick(null, null, '{0}')", id));
        }

        private static void OpenEditorTab(ShowEditorTab tab)
        {
            Assert.ArgumentNotNull(tab, "tab");
            SheerResponse.Eval(tab.ToString());
        }

        private static ShowEditorTab CreateNewShowClonesEditorTab(Item item, string command, string id)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNullOrEmpty(command, "command");
            Assert.ArgumentNotNullOrEmpty(id, "id");

            UrlString urlString = new UrlString("/sitecore modules/shell/showclones/default.aspx");
            item.Uri.AddToUrlString(urlString);
            UIUtil.AddContentDatabaseParameter(urlString);
            return new ShowEditorTab
            {
                Command = command,
                Header = Translate.Text("Show Clones"),
                Icon = Images.GetThemedImageSource("Network/32x32/link_view.png"),
                Url = urlString.ToString(),
                Id = id,
                Closeable = true,
                Activate = true
            };
        }

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

            return CommandState.Enabled;
        }

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

        private static bool HasClones(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return GetClones(item).Any();
        }

        private static IEnumerable<Item> GetClones(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            ClonesGatherer.Source = item;
            return ClonesGatherer.Gather();
        }
    }
}

The command above uses an instance of ItemClonesGatherer to get all clones for the item in the content tree, and ensures it is visible when the item has clones. The logic hides the command when the item does not have clones.

When the command is invoked, it will open a new “Show Clones” tab, or set focus on the “Show Clones” tab if it’s already present.

I then registered the command above in a patch include configuration file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="item:showclones" type="Sitecore.Sandbox.Commands.ShowClones,Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

Now, we need to lock and load this command in Sitecore. I switched over to the core database, and added a new item context menu option:

show-clones-command

Let’s see this in action.

I first created a bunch of clones:

clones-in-content-tree

On my source item, I right-clicked, and was presented with a new context menu option to “Show Clones”:

show-clones-context-menu

After clicking the “Show Clones” context menu option, a new “Show Clones” tab appeared:

show-clones-tab

I then clicked on one of the links in the “Show Clones” tab, and was brought to its associated clone:

was-brough-to-clone

If you have other ideas around using clones in Sitecore, or if you know of another way of listing clones of an item, please leave a comment.

Automagically Clone New Child Items to Clones of Parent Items In the Sitecore CMS

“Out of the box”, content authors are prompted via a content editor warning to take action on clones of a source item when a new subitem is added under the source item:

being-notified-about-new-child-item

Content authors have a choice to either clone or not clone the new subitem, and such an action must be repeated on all clones of the source item — such might be a daunting task, especially when there are multiple clones for a given source item.

In a recent project, I had a requirement to automatically clone newly added subitems under clones of their parents, and remove any content editor warnings by programmatically accepting the Sitecore notifications driving these warnings.

Although I cannot show you that solution, I did wake up from a sound sleep early this morning with another solution to this problem — it was quite a dream — and this post captures that idea.

In a previous post, I built a utility object that gathers things, and decided to reuse this basic concept. Here, I define a contract for what constitutes a general gatherer:

using System.Collections.Generic;

namespace Sitecore.Sandbox.Utilities.Gatherers.Base
{
    public interface IGatherer<T, U>
    {
        T Source { get; set; }

        IEnumerable<U> Gather();
    }
}

In this post, we will be gathering clones — these are Sitecore items:

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Utilities.Gatherers.Base
{
    public interface IItemsGatherer : IGatherer<Item, Item>
    {
    }
}

The following gatherer grabs all clones for a given item as they are cataloged in an instance of the LinkDatabase — in this solution we are using Globals.LinkDatabase as the default instance:

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

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

using Sitecore.Sandbox.Utilities.Gatherers.Base;

namespace Sitecore.Sandbox.Utilities.Gatherers
{
    public class ItemClonesGatherer : IItemsGatherer
    {
        private LinkDatabase LinkDatabase { get; set; }

        private Item _Source;
        public Item Source 
        {
            get
            {
                return _Source;
            }
            set
            {
                Assert.ArgumentNotNull(value, "Source");
                _Source = value;
            }
        }

        private ItemClonesGatherer()
            : this(GetDefaultLinkDatabase())
        {
        }

        private ItemClonesGatherer(LinkDatabase linkDatabase)
        {
            SetLinkDatabase(linkDatabase);
        }

        private ItemClonesGatherer(LinkDatabase linkDatabase, Item source)
        {
            SetLinkDatabase(linkDatabase);
            SetSource(source);
        }

        private void SetLinkDatabase(LinkDatabase linkDatabase)
        {
            Assert.ArgumentNotNull(linkDatabase, "linkDatabase");
            LinkDatabase = linkDatabase;
        }

        private void SetSource(Item source)
        {
            Source = source;
        }

        public IEnumerable<Item> Gather()
        {
            return (from itemLink in GetReferrers()
                    where IsClonedItem(itemLink)
                    select itemLink.GetSourceItem()).ToList();
        }

        private IEnumerable<ItemLink> GetReferrers()
        {
            Assert.ArgumentNotNull(Source, "Source");
            return LinkDatabase.GetReferrers(Source);
        }

        private static bool IsClonedItem(ItemLink itemLink)
        {
            return IsSourceField(itemLink) 
                    && !IsSourceItemNull(itemLink);
        }

        private static bool IsSourceField(ItemLink itemLink)
        {
            Assert.ArgumentNotNull(itemLink, "itemLink");
            return itemLink.SourceFieldID == FieldIDs.Source;
        }

        private static bool IsSourceItemNull(ItemLink itemLink)
        {
            Assert.ArgumentNotNull(itemLink, "itemLink");
            return itemLink.GetSourceItem() == null;
        }

        private static LinkDatabase GetDefaultLinkDatabase()
        {
            return Globals.LinkDatabase;
        }

        public static IItemsGatherer CreateNewItemClonesGatherer()
        {
            return new ItemClonesGatherer();
        }

        public static IItemsGatherer CreateNewItemClonesGatherer(LinkDatabase linkDatabase)
        {
            return new ItemClonesGatherer(linkDatabase);
        }

        public static IItemsGatherer CreateNewItemClonesGatherer(LinkDatabase linkDatabase, Item source)
        {
            return new ItemClonesGatherer(linkDatabase, source);
        }
    }
}

Next, we need a way to remove the content editor warning I alluded to above. The way to do this is to accept the notification that is triggered on the source item’s clones.

I decided to use the decorator pattern to accomplish this:

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

namespace Sitecore.Sandbox.Data.Clones
{
    public class AcceptAsIsNotification : Notification
    {
        private Notification InnerNotification { get; set; }

        private AcceptAsIsNotification(Notification innerNotification)
        {
            SetInnerNotification(innerNotification);
            SetProperties();
        }

        private void SetInnerNotification(Notification innerNotification)
        {
            Assert.ArgumentNotNull(innerNotification, "innerNotification");
            InnerNotification = innerNotification;
        }

        private void SetProperties()
        {
            ID = InnerNotification.ID;
            Processed = InnerNotification.Processed;
            Uri = InnerNotification.Uri;
        }

        public override void Accept(Item item)
        {
            base.Accept(item);
        }

        public override Notification Clone()
        {
            return InnerNotification.Clone();
        }

        public static Notification CreateNewAcceptAsIsNotification(Notification innerNotification)
        {
            return new AcceptAsIsNotification(innerNotification);
        }
    }
}

An instance of the above AcceptAsIsNotification class would wrap an instance of a notification, and accept the wrapped notification without any other action.

Notifications we care about in this solution are instances of Sitecore.Data.Clones.ChildCreatedNotification — in Sitecore.Kernel — for a given clone parent, and this notification clones a subitem during acceptance, a behavior we do not want to leverage since we have already cloned the subitem.

I then added an item:added event handler to use an instance of our gatherer defined above to get all clones of the newly added subitem’s parent; clone the subitem under these clones; and accept all instances of Sitecore.Data.Clones.ChildCreatedNotification on the parent item’s clones:

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

using Sitecore.Data;
using Sitecore.Data.Clones;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Events;

using Sitecore.Sandbox.Data.Clones;
using Sitecore.Sandbox.Utilities.Gatherers;
using Sitecore.Sandbox.Utilities.Gatherers.Base;

namespace Sitecore.Sandbox.Events.Handlers
{
    public class AutomagicallyCloneChildItemEventHandler
    {
        private static readonly IItemsGatherer ClonesGatherer = ItemClonesGatherer.CreateNewItemClonesGatherer();

        public void OnItemAdded(object sender, EventArgs args)
        {
            CloneItemIfApplicable(GetItem(args));
        }

        private static void CloneItemIfApplicable(Item item)
        {
            if (item == null)
            {
                return;
            }

            ChildCreatedNotification childCreatedNotification = CreateNewChildCreatedNotification();
            childCreatedNotification.ChildId = item.ID;
            IEnumerable<Item> clones = GetClones(item.Parent);
            foreach (Item clone in clones)
            {
                Item clonedChild = item.CloneTo(clone);
                RemoveChildCreatedNotifications(Context.ContentDatabase, clone);
            }
        }

        private static void RemoveChildCreatedNotifications(Database database, Item clone)
        {
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNull(clone, "clone");
            foreach (Notification notification in GetChildCreatedNotifications(database, clone))
            {
                AcceptNotificationAsIs(notification, clone);
            }
        }

        private static void AcceptNotificationAsIs(Notification notification, Item item)
        {
            Assert.ArgumentNotNull(notification, "notification");
            Assert.ArgumentNotNull(item, "item");
            Notification acceptAsIsNotification = AcceptAsIsNotification.CreateNewAcceptAsIsNotification(notification);
            acceptAsIsNotification.Accept(item);
        }

        private static IEnumerable<Notification> GetChildCreatedNotifications(Database database, Item clone)
        {
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNull(clone, "clone");
            return GetNotifications(database, clone).Where(notification => notification.GetType() == typeof(ChildCreatedNotification)).ToList();
        }
        private static IEnumerable<Notification> GetNotifications(Database database, Item clone)
        {
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNull(clone, "clone");
            return database.NotificationProvider.GetNotifications(clone);
        }

        private static IEnumerable<Item> GetClones(Item item)
        {
            ClonesGatherer.Source = item;
            return ClonesGatherer.Gather();
        }

        private static Item GetItem(EventArgs args)
        {
            return Event.ExtractParameter(args, 0) as Item;
        }

        private static ChildCreatedNotification CreateNewChildCreatedNotification()
        {
            return new ChildCreatedNotification();
        }
    }
}

I then plugged in the above in a patch include configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <sitecore>
    <events>
      <event name="item:added">
        <handler type="Sitecore.Sandbox.Events.Handlers.AutomagicallyCloneChildItemEventHandler, Sitecore.Sandbox" method="OnItemAdded"/>
      </event>
    </events>
  </sitecore>
</configuration>

Let’s try this out, and see how we did.

I created a new child item under my source item:

added-new-child-automatically-cloned

As you can see, clones were added under all clones of the source item.

Plus, the notification about a new child item being added under the source item is not present:

no-notification-accepted-programmatically

This notification was automatically accepted in code via an instance of AcceptAsIsNotification within the item:added event handler defined above.

If you can think of any other interesting ways to leverage Sitecore’s item cloning capabilities, please leave a comment.

Make a Difference by Comparing Sitecore Items Across Different Databases

The other day I pondered whether anyone had ever built a tool in the Sitecore client to compare field values for the same item across different databases.

Instead of researching whether someone had built such a tool — bad, bad, bad Mike — I decided to build something to do just that — well, really leverage existing code used by Sitecore “out of the box”.

I thought it would be great if I could harness code used by the versions Diff tool — a tool that allows users to visually ascertain differences in fields of an item across different versions in the same database:

compare-version

After digging around in Sitecore.Kernel.dll, I discovered I could reuse some logic from the versions Diff tool to accomplish this, and what follows showcases the fruit yielded from that research.

The first thing I built was a class — along with its interface — to return a collection of databases where an item resides:

using System.Collections.Generic;

namespace Sitecore.Sandbox.Utilities.Gatherers.Base
{
    public interface IDatabasesGatherer
    {
        IEnumerable<Sitecore.Data.Database> Gather();
    }
}
using System.Collections.Generic;
using System.Linq;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Sandbox.Utilities.Gatherers.Base;
using Sitecore.Configuration;

namespace Sitecore.Sandbox.Utilities.Gatherers
{
    public class ItemInDatabasesGatherer : IDatabasesGatherer
    {
        private ID ID { get; set; }

        private ItemInDatabasesGatherer(string id)
            : this(MainUtil.GetID(id))
        {
        }

        private ItemInDatabasesGatherer(ID id)
        {
            SetID(id);
        }

        private void SetID(ID id)
        {
            AssertID(id);
            ID = id;
        }

        public IEnumerable<Sitecore.Data.Database> Gather()
        {
            return GetAllDatabases().Where(database => DoesDatabaseContainItemByID(database, ID));
        }

        private static IEnumerable<Sitecore.Data.Database> GetAllDatabases()
        {
            return Factory.GetDatabases();
        }

        private bool DoesDatabaseContainItemByID(Sitecore.Data.Database database, ID id)
        {
            return GetItem(database, id) != null;
        }

        private static Item GetItem(Sitecore.Data.Database database, ID id)
        {
            Assert.ArgumentNotNull(database, "database");
            AssertID(id);
            return database.GetItem(id);
        }

        private static void AssertID(ID id)
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(id), "id", "ID must be set!");
        }

        public static IDatabasesGatherer CreateNewItemInDatabasesGatherer(string id)
        {
            return new ItemInDatabasesGatherer(id);
        }
        
        public static IDatabasesGatherer CreateNewItemInDatabasesGatherer(ID id)
        {
            return new ItemInDatabasesGatherer(id);
        }
    }
}

I then copied the xml from the versions Diff dialog — this lives in /sitecore/shell/Applications/Dialogs/Diff/Diff.xml — and replaced the versions Combobox dropdowns with my own for showing Sitecore database names:

<?xml version="1.0" encoding="utf-8" ?> 
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
  <ItemDiff>
    <FormDialog Icon="Applications/16x16/window_view.png" Header="Database Compare" Text="Compare the same item in different databases. The differences are highlighted." CancelButton="false">
      <CodeBeside Type="Sitecore.Sandbox.Shell.Applications.Dialogs.Diff.ItemDiff,Sitecore.Sandbox"/>
      <link href="/sitecore/shell/Applications/Dialogs/Diff/Diff.css" rel="stylesheet"/>
      <Stylesheet>
        .ie #GridContainer {
          padding: 4px;
        }
        
        .ff #GridContainer &gt; * {
          padding: 4px;
        }
        
        .ff .scToolbutton, .ff .scToolbutton_Down, .ff .scToolbutton_Hover, .ff .scToolbutton_Down_Hover {
          height: 20px;
          float: left;
        }
      </Stylesheet>
      <AutoToolbar DataSource="/sitecore/content/Applications/Dialogs/Diff/Toolbar" def:placeholder="Toolbar"/>
      <GridPanel Columns="2" Width="100%" Height="100%" GridPanel.Height="100%">
		<Combobox ID="DatabaseOneDropdown" Width="100%" GridPanel.Width="50%" GridPanel.Style="padding:0px 4px 4px 0px" Change="#"/>
        <Combobox ID="DatabaseTwoDropdown" Width="100%" GridPanel.Width="50%" GridPanel.Style="padding:0px 0px 4px 0px" Change="#"/>
        <Scrollbox ID="GridContainer" Padding="" Background="white" GridPanel.ColSpan="2" GridPanel.Height="100%">
          <GridPanel ID="Grid" Width="100%" CellPadding="0" Fixed="true"></GridPanel>  
        </Scrollbox>
      </GridPanel>
    </FormDialog>
  </ItemDiff>
</control>

I saved the above xml in /sitecore/shell/Applications/Dialogs/ItemDiff/ItemDiff.xml.

With the help of the code-beside of the versions Diff tool — this lives in Sitecore.Shell.Applications.Dialogs.Diff.DiffForm — I built the code-beside for the xml control above:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.shell.Applications.Dialogs.Diff;
using Sitecore.Text.Diff.View;
using Sitecore.Web;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;
using Sitecore.Web.UI.WebControls;

using Sitecore.Sandbox.Utilities.Gatherers.Base;
using Sitecore.Sandbox.Utilities.Gatherers;

namespace Sitecore.Sandbox.Shell.Applications.Dialogs.Diff
{
    public class ItemDiff : BaseForm
    {
        private const string IDKey = "id";
        private const string OneColumnViewRegistry = "OneColumn";
        private const string TwoColumnViewRegistry = "TwoColumn";
        private const string ViewRegistryKey = "/Current_User/ItemDatabaseDiff/View";

        protected Button Cancel;
        protected GridPanel Grid;
        protected Button OK;
        protected Combobox DatabaseOneDropdown;
        protected Combobox DatabaseTwoDropdown;

        private ID _ID;
        private ID ID
        {
            get
            {
                if (ID.IsNullOrEmpty(_ID))
                {
                    _ID = GetID();
                }

                return _ID;
            }
        }

        private Database _DatabaseOne;
        private Database DatabaseOne
        {
            get
            {
                if (_DatabaseOne == null)
                {
                    _DatabaseOne = GetDatabaseOne();
                }

                return _DatabaseOne;
            }
        }

        private Database _DatabaseTwo;
        private Database DatabaseTwo
        {
            get
            {
                if (_DatabaseTwo == null)
                {
                    _DatabaseTwo = GetDatabaseTwo();
                }

                return _DatabaseTwo;
            }
        }

        private ID GetID()
        {
            return MainUtil.GetID(GetServerPropertySetIfApplicable(IDKey, IDKey), ID.Null);
        }

        private Database GetDatabaseOne()
        {
            return GetDatabase(DatabaseOneDropdown.SelectedItem.Value);
        }

        private Database GetDatabaseTwo()
        {
            return GetDatabase(DatabaseTwoDropdown.SelectedItem.Value);
        }

        private static Database GetDatabase(string databaseName)
        {
            if(!string.IsNullOrEmpty(databaseName))
            {
                return Factory.GetDatabase(databaseName);
            }

            return null;
        }

        private static string GetServerPropertySetIfApplicable(string serverPropertyKey, string queryStringName, string defaultValue = null)
        {
            Assert.ArgumentNotNullOrEmpty(serverPropertyKey, "serverPropertyKey");
            string value = GetServerProperty(serverPropertyKey);

            if(!string.IsNullOrEmpty(value))
            {
                return value;
            }

            SetServerProperty(serverPropertyKey, GetQueryString(queryStringName, defaultValue));
            return GetServerProperty(serverPropertyKey);
        }

        private static string GetServerProperty(string key)
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");
            return GetServerProperty<string>(key);
        }
        
        private static T GetServerProperty<T>(string key) where T : class
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");
            return Context.ClientPage.ServerProperties[key] as T;
        }

        private static void SetServerProperty(string key, object value)
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");
            Context.ClientPage.ServerProperties[key] = value;
        }

        private static string GetQueryString(string name, string defaultValue = null)
        {
            Assert.ArgumentNotNullOrEmpty(name, "name");
            if(!string.IsNullOrEmpty(defaultValue))
            {
                return WebUtil.GetQueryString(name, defaultValue);
            }

            return WebUtil.GetQueryString(name);
        }

        private void Compare()
        {
            Compare(GetDiffView(), Grid, GetItemOne(), GetItemTwo());
        }

        private static void Compare(DiffView diffView, GridPanel gridPanel, Item itemOne, Item itemTwo)
        {
            Assert.ArgumentNotNull(diffView, "diffView");
            Assert.ArgumentNotNull(gridPanel, "gridPanel");
            Assert.ArgumentNotNull(itemOne, "itemOne");
            Assert.ArgumentNotNull(itemTwo, "itemTwo");
            diffView.Compare(gridPanel, itemOne, itemTwo, string.Empty);
        }

        private static DiffView GetDiffView()
        {
            if (IsOneColumnSelected())
            {
                return new OneColumnDiffView();
            }

            return new TwoCoumnsDiffView();
        }

        private Item GetItemOne()
        {
            Assert.IsNotNull(DatabaseOne, "DatabaseOne must be set!");
            return DatabaseOne.Items[ID];
        }

        private Item GetItemTwo()
        {
            Assert.IsNotNull(DatabaseOne, "DatabaseTwo must be set!");
            return DatabaseTwo.Items[ID];
        }

        private static void OnCancel(object sender, EventArgs e)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(e, "e");
            Context.ClientPage.ClientResponse.CloseWindow();
        }

        protected override void OnLoad(EventArgs e)
        {
            Assert.ArgumentNotNull(e, "e");
            base.OnLoad(e);

            OK.OnClick += new EventHandler(OnOK);
            Cancel.OnClick += new EventHandler(OnCancel);

            DatabaseOneDropdown.OnChange += new EventHandler(OnUpdate);
            DatabaseTwoDropdown.OnChange += new EventHandler(OnUpdate);
        }

        private static void OnOK(object sender, EventArgs e)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(e, "e");
            Context.ClientPage.ClientResponse.CloseWindow();
        }

        protected override void OnPreRender(EventArgs e)
        {
            Assert.ArgumentNotNull(e, "e");
            base.OnPreRender(e);

            if (!Context.ClientPage.IsEvent)
            {
                PopuplateDatabaseDropdowns();
                Compare();
                UpdateButtons();
            }
        }

        private void PopuplateDatabaseDropdowns()
        {
            IDatabasesGatherer IDatabasesGatherer = ItemInDatabasesGatherer.CreateNewItemInDatabasesGatherer(ID);
            PopuplateDatabaseDropdowns(IDatabasesGatherer.Gather());
        }

        private void PopuplateDatabaseDropdowns(IEnumerable<Database> databases)
        {
            PopuplateDatabaseDropdown(DatabaseOneDropdown, databases, Context.ContentDatabase);
            PopuplateDatabaseDropdown(DatabaseTwoDropdown, databases, Context.ContentDatabase);
        }

        private static void PopuplateDatabaseDropdown(Combobox databaseDropdown, IEnumerable<Database> databases, Database selectedDatabase)
        {
            Assert.ArgumentNotNull(databaseDropdown, "databaseDropdown");
            Assert.ArgumentNotNull(databases, "databases");

            foreach (Database database in databases)
            {
                databaseDropdown.Controls.Add
                (
                    new ListItem 
                    { 
                        ID = Sitecore.Web.UI.HtmlControls.Control.GetUniqueID("ListItem"), 
                        Header = database.Name,
                        Value = database.Name,
                        Selected = string.Equals(database.Name, selectedDatabase.Name)
                    }
                );
            }
        }

        private void OnUpdate(object sender, EventArgs e)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(e, "e");
            Refresh();
        }

        private void Refresh()
        {
            Grid.Controls.Clear();
            Compare();
            Context.ClientPage.ClientResponse.SetOuterHtml("Grid", Grid);
        }

        protected void ShowOneColumn()
        {
            SetRegistryString(ViewRegistryKey, OneColumnViewRegistry);
            UpdateButtons();
            Refresh();
        }

        protected void ShowTwoColumns()
        {
            SetRegistryString(ViewRegistryKey, TwoColumnViewRegistry);
            UpdateButtons();
            Refresh();
        }

        private static void UpdateButtons()
        {
            bool isOneColumnSelected = IsOneColumnSelected();
            SetToolButtonDown("OneColumn", isOneColumnSelected);
            SetToolButtonDown("TwoColumn", !isOneColumnSelected);
        }

        private static bool IsOneColumnSelected()
        {
            return string.Equals(GetRegistryString(ViewRegistryKey, OneColumnViewRegistry), OneColumnViewRegistry);
        }

        private static void SetToolButtonDown(string controlID, bool isDown)
        {
            Assert.ArgumentNotNullOrEmpty(controlID, "controlID");
            Toolbutton toolbutton = FindClientPageControl<Toolbutton>(controlID);
            toolbutton.Down = isDown;
        }

        private static T FindClientPageControl<T>(string controlID) where T : System.Web.UI.Control
        {
            Assert.ArgumentNotNullOrEmpty(controlID, "controlID");
            T control = Context.ClientPage.FindControl(controlID) as T;
            Assert.IsNotNull(control, typeof(T));
            return control;
        }

        private static string GetRegistryString(string key, string defaultValue = null)
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");

            if(!string.IsNullOrEmpty(defaultValue))
            {
                return Sitecore.Web.UI.HtmlControls.Registry.GetString(key, defaultValue);
            }

            return Sitecore.Web.UI.HtmlControls.Registry.GetString(key);
        }

        private static void SetRegistryString(string key, string value)
        {
            Assert.ArgumentNotNullOrEmpty(key, "key");
            Sitecore.Web.UI.HtmlControls.Registry.SetString(key, value);
        }
    }
}

The code-beside file above populates the two database dropdowns with the names of the databases where the Item is found, and selects the current content database on both dropdowns when the dialog is first launched.

Users have the ability to toggle between one and two column layouts — just as is offered by the versions Diff tool — and can compare field values on the item across any database where the item is found — the true magic occurs in the instance of the Sitecore.Text.Diff.View.DiffView class.

Now that we have a dialog form, we need a way to launch it:

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

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

using Sitecore.Sandbox.Utilities.Gatherers.Base;
using Sitecore.Sandbox.Utilities.Gatherers;

namespace Sitecore.Sandbox.Commands
{
    public class LaunchDatabaseCompare : Command
    {
        public override void Execute(CommandContext commandContext)
        {
            SheerResponse.CheckModified(false);
            SheerResponse.ShowModalDialog(GetDialogUrl(commandContext));
        }

        private static string GetDialogUrl(CommandContext commandContext)
        {
            return GetDialogUrl(GetItem(commandContext).ID);
        }

        private static string GetDialogUrl(ID id)
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(id), "id", "ID must be set!");
            UrlString urlString = new UrlString(UIUtil.GetUri("control:ItemDiff"));
            urlString.Append("id", id.ToString());
            return urlString.ToString();
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            IDatabasesGatherer databasesGatherer = ItemInDatabasesGatherer.CreateNewItemInDatabasesGatherer(GetItem(commandContext).ID);

            if (databasesGatherer.Gather().Count() > 1)
            {
                return CommandState.Enabled;
            }

            return CommandState.Disabled;
        }

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

The above command launches our ItemDiff dialog, and passes the ID of the selected item to it.

If the item is only found in one database — this will be the current content database — the command is disabled. What would be the point of comparing the item in the same database?

I then registered this command in a patch include configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <sitecore>
    <commands>
      <command name="item:launchdatabasecompare" type="Sitecore.Sandbox.Commands.LaunchDatabaseCompare,Sitecore.Sandbox"/>
    </commands>
  </sitecore>
</configuration>

Now that we have our command ready to go, we need to lock and load this command in the Sitecore client. I added a button for our new command in the Operations chunk under the Home ribbon:

database-compare-chunk-button-new

Time for some fun.

I created a new item for testing:

database-compare-new-item

I published this item, and made some changes to it:

database-compare-changed-item-in-master

I clicked the Database Compare button to launch our dialog form:

database-compare-launched-dialog

As expected, we see differences in this item across the master and web databases:

database-compare-one-column-comparison

Here are those differences in the two column layout:

database-compare-two-column-comparison

One thing I might consider adding in the future is supporting comparisons of different versions of items across databases. The above solution is limited in only allowing users to compare the latest version of the Item in each database.

If you can think of anything else that could be added to this to make it better, please drop a comment.

Delete Sitecore Items Using a Deletion Basket

For the past week, I’ve been battling a nasty strain of the rhinovirus — don’t worry, that’s just the fancy medical term for what is known as the common cold — albeit there appears to be nothing common about this cold. I think I have a frankencold (I just made up this word, and might submit it to Merriam-Webster for inclusion in the English dictionary).

In my sickened state — perhaps it could be classified as a state of frenzy — I started pondering over strange feature ideas. The ‘Dislike’ button was one idea that came to mind. It would serve as the antithesis to the ‘Like’ button found on most social networking outlets. Such a feature would definitely be a whimsical thing to build, although might foment more trouble than it’s worth.

Another idea that came to mind was a deletion basket in the Sitecore client. It would be similar in theme to a shopping cart found on most e-commerce websites. Users would queue items in their deletion basket, and delete them after they are finished adding items — a checkout step for the lack of a better term.

The latter idea seemed useful — most importantly fun — so I decided to build it.

I first created a Deletion Basket class:

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

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

namespace Sitecore.Sandbox.Utilities.Items.Base
{
    public interface IDeletionBasket
    {
        int Count();

        bool IsEmpty();

        IEnumerable<Item> GetItemsToDelete();

        bool Contains(Item item);

        void Add(Item item);

        void Remove(Item item);

        void DeleteAll();

        void Clear();
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Utilities.Items.Base;

namespace Sitecore.Sandbox.Utilities.Items
{
    public class DeletionBasket : IDeletionBasket
    {
        private static volatile IDeletionBasket current;
        private static object lockObject = new Object();

        public static IDeletionBasket Current
        {
            get
            {
                if (current == null)
                {
                    lock (lockObject)
                    {
                        if (current == null)
                            current = new DeletionBasket();
                    }
                }

                return current;
            }
        }

        private IList<Item> _ItemsToDelete;
        private IList<Item> ItemsToDelete
        {
            get
            {
                if (_ItemsToDelete == null)
                {
                    _ItemsToDelete = new List<Item>();
                }

                return _ItemsToDelete;
            }
        }

        private DeletionBasket()
        {
        }

        public int Count()
        {
            return ItemsToDelete.Count();
        }

        public bool IsEmpty()
        {
            return !ItemsToDelete.Any();
        }

        public IEnumerable<Item> GetItemsToDelete()
        {
            return ItemsToDelete;
        }

        public bool Contains(Item item)
        {
            return FindItemInBasket(item) != null;
        }

        private Item FindItemInBasket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return FindItemInBasketByID(item.ID);
        }

        private Item FindItemInBasketByID(ID id)
        {
            AssertID(id);
            return FindItemsInBasketByID(id).FirstOrDefault();
        }

        private IEnumerable<Item> FindItemsInBasketByID(ID id)
        {
            AssertID(id);
            return ItemsToDelete.Where(i => i.ID == id);
        }

        private static void AssertID(ID id)
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(id), "id", "ID must be set!");
        }

        public void Add(Item item)
        {
            Item itemInBasket = FindItemInBasket(item);
            if (itemInBasket == null)
            {
                ItemsToDelete.Add(item);
                AddChildren(item);
            }
        }

        private void AddChildren(Item item)
        {
            if (!item.HasChildren)
            {
                return;
            }

            foreach (Item child in item.Children)
            {
                Add(child);
            }
        }

        public void Remove(Item item)
        {
            Item itemInBasket = FindItemInBasket(item);
            if (itemInBasket != null)
            {
                ItemsToDelete.Remove(itemInBasket);
            }
        }

        public void DeleteAll()
        {
            Sitecore.Context.ClientPage.Start(this, "ConfirmAndDeleteAll", new ClientPipelineArgs());
        }

        private void ConfirmAndDeleteAll(ClientPipelineArgs args)
        {
            ShowConfirmationDialogIfApplicable(args);
            DeleteAllIfApplicable(args);
        }

        private void ShowConfirmationDialogIfApplicable(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                Context.ClientPage.ClientResponse.YesNoCancel(string.Format("Are you sure you want to delete the {0} item(s) in your Deletion Basket?", Count()), "200", "200");
                args.WaitForPostBack();
            }
        }

        private void DeleteAllIfApplicable(ClientPipelineArgs args)
        {
            bool canDelete = args.IsPostBack && args.Result == "yes";
            if (canDelete)
            {
                foreach (Item itemToDelete in ItemsToDelete)
                {
                    DeleteItem(itemToDelete);
                }

                Clear();
            }
        }

        private static void DeleteItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if (Settings.RecycleBinActive)
            {
                item.Recycle();
            }
            else
            {
                item.Delete();
            }
        }

        public void Clear()
        {
            ItemsToDelete.Clear();
        }
    }
}

The above class stores and deletes items from a list, and prompts users ascertaining if they truly want to delete items within the deletion basket.

Only one instance of the above class can exist — I employed the Singleton pattern for this purpose — to keep the code simple for this blog post, although it probably would make more sense to make baskets session aware — have different baskets for different sessions.

Next, I created a command to add or remove an item from the deletion basket — depending on whether the item is in the basket:

using System.Linq;

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

using Sitecore.Sandbox.Utilities.Items;
using Sitecore.Sandbox.Utilities.Items.Base;

namespace Sitecore.Sandbox.Commands
{
    class ToggleItemInDeletionBasket : Command
    {
        private static readonly Delete DeleteCommand = new Delete();

        public override void Execute(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            ToggleInDeletionBasketIfEnabled(commandContext);
        }

        private void ToggleInDeletionBasketIfEnabled(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            if (QueryState(commandContext) == CommandState.Enabled)
            {
                ToggleInDeletionBasketAndRefresh(GetItem(commandContext));
            }
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            return DeleteCommand.QueryState(commandContext);
        }

        private static void ToggleInDeletionBasketAndRefresh(Item item)
        {
            ToggleInDeletionBasket(item);
            RefreshItem(item);
        }

        private static void ToggleInDeletionBasket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            IDeletionBasket deletionBasket = DeletionBasket.Current;

            if(deletionBasket.Contains(item))
            {
                deletionBasket.Remove(item);
            }
            else
            {
                deletionBasket.Add(item);
            }
        }

        private static Item GetItem(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            Assert.ArgumentNotNull(commandContext.Items, "commandContext.Items");
            Assert.ArgumentCondition(commandContext.Items.Count() > 0, "commandContext.Items", "There must be at least one item in the array!");
            return commandContext.Items.FirstOrDefault();
        }

        public override string GetHeader(CommandContext commandContext, string header)
        {
            if (DeletionBasket.Current.Contains(GetItem(commandContext)))
            {
                return Translate.Text("Remove from Deletion Basket");
            }

            return Translate.Text("Add to Deletion Basket");
        }

        private static void RefreshItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", item.ID), 1);
        }
    }
}

The text of the command is different when the item is in the basket versus when it is not.

The command also reloads the item in the Sitecore client — this is done to update the state of the deletion basket buttons in the ribbon.

Now, we need a command to delete items queued in the deletion basket. The ‘Empty Deletion Basket’ command does just that:

using System.Linq;

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

using Sitecore.Sandbox.Utilities.Items;
using Sitecore.Sandbox.Utilities.Items.Base;

namespace Sitecore.Sandbox.Commands
{
    public class EmptyDeletionBasket : Command
    {
        public override void Execute(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            EmptyDeletionBasketIfEnabled(commandContext);
        }

        private void EmptyDeletionBasketIfEnabled(CommandContext commandContext)
        {
            if (QueryState(commandContext) == CommandState.Enabled)
            {
                DeleteAllItemsInDeletionBasketAndRefresh();
            }
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");

            if (DeletionBasket.Current.IsEmpty())
            {
                return CommandState.Hidden;
            }

            return base.QueryState(commandContext);
        }

        private static void DeleteAllItemsInDeletionBasketAndRefresh()
        {
            Item firstItemParent = GetFirstItemParent();
            DeleteAllItemsInDeletionBasket();
            RefreshItem(firstItemParent);
        }

        private static void DeleteAllItemsInDeletionBasket()
        {
            DeletionBasket.Current.DeleteAll();
        }

        private static Item GetFirstItemParent()
        {
            Item itemForRefresh = DeletionBasket.Current.GetItemsToDelete().FirstOrDefault();
            if (itemForRefresh != null && itemForRefresh.Parent != null)
            {
                return itemForRefresh.Parent;
            }

            return null;
        }

        private static void RefreshItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", item.ID), 1);
        }

        public override string GetHeader(CommandContext commandContext, string header)
        {
            IDeletionBasket deletionBasket = DeletionBasket.Current;
            return Translate.Text(string.Format("Delete {0} Item(s)", deletionBasket.Count()));
        }
    }
}

This command also reloads the Sitecore client — using the the parent item of the first item flagged for deletion (you can’t reload an item that was already deleted). This is also done to refresh the state of the buttons in the ribbon.

Plus, the command is hidden when there are no items in the deletion basket.

We shouldn’t force users to only add and delete items in their basket. We should also allow them to clear out their baskets, in case they change their mind on what they want to delete. I created a ‘Clear Deletion Basket’ command for this very reason:

using System.Linq;

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

using Sitecore.Sandbox.Utilities.Items;

namespace Sitecore.Sandbox.Commands
{
    public class ClearDeletionBasket : Command
    {
        public override void Execute(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");
            ClearDeletionBasketIfEnabled(commandContext);
        }

        private void ClearDeletionBasketIfEnabled(CommandContext commandContext)
        {
            if (QueryState(commandContext) == CommandState.Enabled)
            {
                ClearAllItemsInDeletionBasketAndRfresh();
            }
        }

        public override CommandState QueryState(CommandContext commandContext)
        {
            Assert.ArgumentNotNull(commandContext, "commandContext");

            if (DeletionBasket.Current.IsEmpty())
            {
                return CommandState.Disabled;
            }

            return base.QueryState(commandContext);
        }

        private static void ClearAllItemsInDeletionBasketAndRfresh()
        {
            Item lastItem = GetLastItem();
            ClearAllItemsInDeletionBasket();
            RefreshItem(lastItem);
        }

        private static void ClearAllItemsInDeletionBasket()
        {
            DeletionBasket.Current.Clear();
        }

        private static Item GetLastItem()
        {
            Item itemForRefresh = DeletionBasket.Current.GetItemsToDelete().LastOrDefault();
            if (itemForRefresh != null)
            {
                return itemForRefresh;
            }

            return null;
        }

        private static void RefreshItem(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            Context.ClientPage.ClientResponse.Timer(string.Format("item:load(id={0})", item.ID), 1);
        }
    }
}

In order to use our commands above, we have to define them in /App_Config/Commands.config:

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

	<!-- There's a bunch of commands up here -->
	
	<command name="item:toggleitemindeletionbasket" type="Sitecore.Sandbox.Commands.ToggleItemInDeletionBasket,Sitecore.Sandbox"/>
	<command name="item:cleardeletionbasket" type="Sitecore.Sandbox.Commands.ClearDeletionBasket,Sitecore.Sandbox"/>
	<command name="item:emptydeletionbasket" type="Sitecore.Sandbox.Commands.EmptyDeletionBasket,Sitecore.Sandbox"/>
</configuration>

Now that our commands are defined in /App_Config/Commands.config, we can now wire them up in the Core database.

I’ve wired up the ‘Empty Deletion Basket’ button:

Empty-Deletion-Basket-Core

Followed by wiring up the ‘Clear Deletion Basket’ dropdown button:

Clear-Deletion-Basket-Core

Next, I set up the ‘Toggle Item In Deletion Basket’ item context menu button:

Toggle-Item-In-Deletion-Basket-Core

Let’s see how we did.

I’ve switched back over to the master database, picked an item at random, and right-clicked. There’s our new ‘Toggle Item In Deletion Basket’ button:

Toggle-Not-In-Deletion-Basket

The appropriate wording is displayed since the item is not in the Deletion Basket.

I’ve clicked the ‘Toggle Item In Deletion Basket’ button in the item context menu, and then right-clicked on the item again to launch it again:

Toggle-In-Deletion-Basket

We see that button’s text has changed to convey that this item is in the deletion basket, and can be removed from the deletion basket by clicking the item context menu button again.

Plus, the deletion basket buttons in the ribbon have magically appeared.

I’ve clicked the dropdown on the deletion basket button in the ribbon, and we now see the ‘Clear Deletion Basket’ button:

Clear-Deletion-Basket

I’m going to add a bunch of items to our deletion basket:

Add-A-Bunch-Of-Items-Deletion-Basket

They’ve all been added. Let’s get rid of them:

chosen-items-basket-delete

Doh — I’ve been blocked by an instrusive confirmation box:

chosen-items-basket-delete-confirmation

I’ve clicked ‘Yes’, and now the items are gone:

chosen-items-basket-deleted

That was all in good fun.

Time to investigate adding a ‘Dislike’ button to the popular social media channels. 😉