Modifying Templates in Sitecore: When to Worry, When not to Worry

When working with Sitecore, it’s possible you may hit a point where you want to refactor some of the templates you or someone else created earlier in the project. Depending on what you’re changing and the structure of your content, you face the possibility of losing content.  Explaining to content editors why their content vanished is not fun. My goal is to help you avoid that situation.

Changing an item’s template

When you change an item’s template either via the content editor or the API, Sitecore gathers a list of fields from the item’s current template and tries to find those fields by ID in the new template. For each field that does not exist in the new template, Sitecore attempts to resolve each field by name.  Therefore, if you change an item’s template and the new template has the same field IDs (via inheritance) or the new template contains fields with the same name as the current template, no data will be lost. Since Sitecore warns you about which field values will be lost before committing any change it is unlikely that you would inadvertently lose content when changing an item’s template.

Unfortunately there are other times when Sitecore does not take similar measures to ensure that no data is lost when making changes either to items or templates.  Below are two instances that require caution:

  1. Clones
  2. Changing a template’s base templates

Clones

When you change the template of an item’s clone, only values of fields with the same ID will be kept.  In other words, unlike regular content items, changing the template of a clone does not resolve the values of fields by field name.  It should be rare that this causes a problem because most of the time you don’t want to change a clone’s template and if you do, you most likely want the clone’s new template to inherit the cloned item’s template or one of its base templates. If this is the case, the actual fields you want the clone to be based on will still exist in its template. If do want to change a clone’s template so that it is radically different from the item it is based on, then I would ask yourself why you are using clones to solve the problem at hand and perhaps explore other options.

Changing a template’s base templates

Ideally it should be rare to want to change a template’s base templates after much content has been entered.  However, there are times when you may find this necessary. In these situations caution should be exercised since it will impact all items that use the template you’re updating. Sitecore does not attempt to update each item’s field values with its template’s new base fields by name — only ID. So, unless the new base template inherits fields with the same ID as the template that was removed, the values of those fields for all items that use that template will be lost.  If this is unacceptable you have several options:

  1. You can duplicate your original template and change the base templates of your duplicated template to the new base templates.  You can then manually change each item to use the new template or write a script to change each items template to the new one.  Content will not be lost for any item’s fields that are the same based on its actual field ID or field name.
  2. You can write code to query the content tree for items affected by the template change and update each items field values that either do not exist on the new base template or now do exist by name, but with a different ID. If fields of the same name do exist you want to update the table in the DB to point to the new field ID.  If the field does not exist by ID or name, then you want to remove this entry from the database.  Luckily, since this operation is similar to what is done when changing an item’s template, the Sitecore API provides functionality to do just that. This could end up being a very long running process so it’s a good idea to ask the user whether or not to perform this action.

Below is one possible solution to this problem.  It replaces the the class that responds to the templatebuilder:basetemplateschanged  command with a new class that attempts to resolve each item’s fields by field name if they no longer exist by ID in the new base templates.  Most of the functionality from the original class is preserved even though the Run method from the base class has to be hidden.  I’m generally not a fan of hiding methods, but in this case, I don’t think hiding the Run method should cause any issues because it is unlikely that a developer would at some point cast our new class as its base class since it exists just to handle a command.

Note: This code has not been tested and should not be used in a production environment.

Add the following class to your project:

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

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Data.Templates;
using Sitecore.Web.UI.Sheer;
using Sitecore.Diagnostics;

namespace Hzi.ExampleCode.Commands
{
    [Serializable]
    public class SetBaseTemplates : Sitecore.Shell.Framework.Commands.TemplateBuilder.SetBaseTemplates
    {
        private Item _existingItem;
        private string _newValue;

        public override void Execute(Sitecore.Shell.Framework.Commands.CommandContext context)
        {
            _existingItem = context.Items[0];
            base.Execute(context);
        }

        new protected virtual void Run(ClientPipelineArgs args)
        {
            base.Run(args);

            if (args.HasResult && args.IsPostBack)
            {
                _newValue = args.Result;
                args.IsPostBack = false;
                Sitecore.Context.ClientPage.Start(this, "ConfirmBaseTemplates", args);
            }
        }

        protected void ConfirmBaseTemplates(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                SheerResponse.YesNoCancel("Modifying the base templates may result in a loss of data.  For items that use this template, would you like to update any fields removed from the base templates to fields of the same name?  This operation may take a long time.", "220", "160");
                args.WaitForPostBack();
            }
            else
            {
                if (args.HasResult)
                {
                    if (args.Result == "cancel")
                    {   // reset base templates to original value
                        ItemUri uri = ItemUri.Parse(args.Parameters["uri"]);
                        Item item = Database.GetItem(uri);
                        Error.AssertItemFound(item);

                        item.Editing.BeginEdit();
                        item[Sitecore.FieldIDs.BaseTemplate] = _existingItem[Sitecore.FieldIDs.BaseTemplate];
                        item.Editing.EndEdit();

                        return;
                    }

                    if (args.Result == "no")
                    {   // don't update the items
                        return;
                    }

                    UpdateItems();
                }
            }
        }

        private void UpdateItems()
        {
            List<TemplateChangeList> changes = GetChangeLists(GetRelevantTemplates());

            foreach (Item item in GetAffectedItems())
            {
                foreach (TemplateChangeList changeList in changes)
                {
                    lock (item.SyncRoot)
                    {
                        TemplateManager.ChangeTemplate(item, changeList);
                    }
                }

                RefreshItem(item);
            }
        }

        private List<TemplateChangeList> GetChangeLists(IEnumerable<string> relevantTemplates)
        {
            Template updatedItemAsTemplate = TemplateManager.GetTemplate(_existingItem.ID, _existingItem.Database);
            List<TemplateChangeList> changes = new List<TemplateChangeList>();

            foreach (string templateGuid in relevantTemplates)
            {
                Template affectedTemplate = TemplateManager.GetTemplate(new ID(templateGuid), _existingItem.Database);

                // Gets the differences between the affected template and the new set of base templates
                TemplateChangeList changeList = affectedTemplate.GetTemplateChangeList(updatedItemAsTemplate);
                TemplateChangeList cleanupList = new TemplateChangeList(changeList.Source, changeList.Target);

                // The cleanup list deletes any DB entries that had previously been orphaned
                // when making base template changes to avoid a SQL exception for violating the
                // ndxUnique index contstraint.
                foreach (TemplateChangeList.TemplateChange change in changeList.Changes)
                {
                    if (change.Action == TemplateChangeAction.ChangeFieldID)
                    {
                        cleanupList.Add(TemplateChangeAction.DeleteField, change.TargetField, change.TargetField);
                    }
                }
                changes.Add(cleanupList);
                changes.Add(changeList);
            }

            return changes;
        }

        // Get just the templates that may result in a loss of data
        private IEnumerable<string> GetRelevantTemplates()
        {
            string[] existingTemplates = _existingItem[Sitecore.FieldIDs.BaseTemplate].Split('|');
            string[] updatedTemplates = _newValue.Split('|');
            return existingTemplates.Except(updatedTemplates);
        }

        private void RefreshItem(Item item)
        {
            item.Editing.BeginEdit();
            item.RuntimeSettings.ForceModified = true;
            item.Editing.EndEdit();
        }

        private Item[] GetAffectedItems()
        {
            return _existingItem.Database.SelectItems("fast:/sitecore/content//*[@@templateid='" + _existingItem.ID + "']");
        }
    }
}

Then in the Commands.config, locate the command named templatebuilder:setbasetemplates and change it to match what is below updating the namespace and assembly to match your project:

<command name="templatebuilder:setbasetemplates" type="Hzi.ExampleCode.Commands.SetBaseTemplates,Hzi.ExampleCode"/>

A few notes:

  1. To be thorough, in addition to getting all items that use the modified template directly you may also want to update all items that use the modified template via inheritance. I omitted that from this example for performance reasons and to simplify the code.
  2. Before doing something like this in a production environment, it is worth reading Alistair Deneys’ post about long running processes. I didn’t implement any of those options mainly to keep this example simple, but since changing a template’s base template has the potential to affect every content item across a site, it is definitely worth considering how this may affect performance and the content editing experience.
  3. Another way of achieving the same end result, is to intercept the item:saving event.  The advantage to this approach is that you don’t need to inherit the SetBaseTemplates class and create a new command type. The disadvantage is that the code runs each time an item is saved so there needs to be a number of checks to ensure that the code exists when it is not responding to a change to a template’s base templates. It also makes cancelling the action more tricky.  If you are interested in this approach, however, let me know and I’ll send you that code.
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: