Experience Sitecore ! | Creating "Change item ID" module. Part 1 - Identifying the requirements

Experience Sitecore !

More than 200 articles about the best DXP by Martin Miles

Creating "Change item ID" module. Part 1 - Identifying the requirements

I am working on a bit of unusual solution at he moment and one of the "features" I was surprised to own was and external feed of data that serves me the content in various formats including GUIDs of items existing in my Sitecore database.Since feed is not well flexible, I decided to become flexible myself and one of challenges I rose for myself was changing IDs of existing items in Sitecore.

As you know, ID is read-only property of Item in Sitecore, it is being generated upon item's creation and remains the same for entire lifetime. Driven by curiosity, I started investigating how to achieve that and decided to look up any existing implantation. What can be similar to changing ID? Of course, that would be Rename feature.

Let's take a look at Rename icon in Content Editor context menu, when right clicking on an item. It is located at /sitecore/content/Applications/Content Editor/Context Menues/Default/Rename and sends the following message

item:rename(id=$Target)

that calls rename command. So why not to create one more command right after Rename that will send message:

item:changeid(id=$Target)

Of course, we need to associate that to a corresponding business class that inherits from Command:

<commands>
  <command name="item:changeid" type="ChangeId.ChangeIdCommand,ChangeId" patch:after="command[@name='item:rename']" />
</commands>

Let's follow Sitecore architectural principles and implement our change ID logic in a form of a pipeline, let's call it uiChangeIdForItem. In that case new ChangeIdCommand wil do nothing else but trigger the pipeline:

[Serializable]
public class ChangeIdCommand : Command
{
    public override void Execute(CommandContext context)
    {
        Assert.ArgumentNotNull(context, "context");
        if (context.Items.Length != 1 || context.Items[0] == null)
        {
            return;
        }

        StartPipeline("uiChangeIdForItem", context.Items[0]);
    }

    public override CommandState QueryState(CommandContext context)
    {
        Assert.ArgumentNotNull(context, "context");
        if (context.Items.Length != 1)
        {
            return CommandState.Disabled;
        }

        Item obj = context.Items[0];
        if (obj.Appearance.ReadOnly || !obj.Access.CanRename() || IsLockedByOther(obj))
        {
            return CommandState.Disabled;
        }

        return base.QueryState(context);
    }

    private static void StartPipeline(string pipelineName, Item item)
    {
        Assert.ArgumentNotNullOrEmpty(pipelineName, "pipelineName");
        Assert.ArgumentNotNull(item, "item");

        NameValueCollection parameters = new NameValueCollection();
        parameters.Add("database", item.Database.Name);
        parameters.Add("id", item.ID.ToString());
        parameters.Add("language", item.Language.ToString());
        parameters.Add("version", item.Version.ToString());

        Context.ClientPage.Start(pipelineName, parameters);
    }
}

Okay, but what would processors our pipeline consist from? To answer that we need to understand how it will be renaming an item and what additional -pre and -post processing steps may occur, passing parameters between steps and fallback scenarios. So let's take into consideration the following concerns:

1. Changing ID cannot be done directly in the code by using Sitecore API, that isn't something that is or will be supported in future. But by using API we can serialize and deserialize an item, so changing ID would be a matter of simply changing several bytes in plain text file. So based on that we already have a consequence of three processors: Serialize ==> ChangeId ==> Deserialize.

2. Deserialization of an item with modified ID will create a new item with new ID that would be an absolute clone of original item rather then updating ID of existing item. That means I can end up having two exact items, so I need to delete the one having original ID. That adds RemoveOriginalItem processor somewhere at the end of pipeline, when the rest is done.

3. To obtain new item I would need it to get from somewhere, so let's ask a user to provide that by raising a popup input box from UI. That adds an additional processor, let's call it GetNewName.

4. Changing ID is a dangerous task so a user should clearly realise what he/she is doing and understand consequences, that should be ideally an admin or at least a developer. So we should not allow to perform this operation to everyone but should check whether a user has appropriate permissions. Of course, permissions can be set externally from Security Editor but that's only UI and can be bypassed by sending the same command message from JavaScript or developers tools in browser. That's why it is essential to verify permissions at the backend prior to doing anything else, so that results in CheckPermissions processor in the beginning of pipeline.

5. Items in SItecore are organised hierarchically so if we change ID of an item by deserializing it into a new clone item, children still remain under original item that we are right about to delete that will remove children as well. So to correct that we need to explicitly move all the descendants under a new item. That can become an addtional MoveChildren processor after deserialization process.

6. Also if original item was referenced by any other items, those references will become broken once delete original item will be deleted so need to implement UpdateReferences processor. If there are many references to an item it may take a considerable time to update them all, so in user-friendly manner we need to prompt that and ask whether a user still want to process. Why not a candidate for yet another CheckNumberOfReferences processor?

Combining 10 above processors together in the right order makes will look as below:

<processors>
  <uiChangeIdForItem>
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="CheckPermissions" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="VerifyUnsupportedTypes" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="GetNewName" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="CheckLinks" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="Serialize" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="ChangeId" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="Deserialize" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="CopyTemplateChildren" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="RepairLinks" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="RemoveSourceItem" />
  </uiChangeIdForItem>
</processors>

So far, so good! Once we mentioned all the requirements and are certain about the steps, let's do the actual implementation in second part of this blog post.

Comments are closed