codeflood logo

Sharing Content with Item Clones

In my previous Sitecore 6.4 sneak peek I gave a quick overview of a new feature in Sitecore 6.4 called item clones. These were introduced to replace proxy items which have now been deprecated and are likely to be removed completely in the next major release of Sitecore.

There’s already been a few posts about item clones from John West: Sitecore CMS 6.4: Cloning and from Adam Conn: Cloning What Ifs.

Item clones hold some benefits over proxy items. Firstly, they are lighter than proxies. Proxy items required a proxy item provider and configuration in the configuration file to enable them for the specific database you were working in, not to mention you had to match the settings in both the source and target databases, and the settings weren’t the same. Proxies also allow the author to inadvertently change the content of the source item through the proxy. This is a little unsafe, especially if your content authors aren’t aware the item they’re working on is a proxy and their single update will cause content in multiple locations to update, which may be undesirable.

So let’s delve into item clones and see how we can use those to share content in the Sitecore content tree.

The first thing we’ll want to do is clone an item. Simple navigate to the item to be cloned in the content tree, then on the configure tab in the content editor, click the clone button in the clones chunk. A dialog will appear so you can select the location to create the clone. And you’re done. By default the clone will also inherit all children of the source item (source item is the item which was cloned).

Behind the scenes, Sitecore has simply created you a new item in a new location based on the template of the current item and set the __source field to the ID of the source item. The source item URI is in the following format:

sitecore://master/{430F4003-9DC9-42D3-A303-E69E39E3B5F7}?lang=en&ver=1

You can see from the above item URI format that item clones also support cross database source items as well as a specific version. The tools and UI controls may not be there just yet, but you can easily craft an item URI yourself and paste it in manually.

Using the standard values infrastructure, the cloned item will then inherit field values from the source item. Because the values are inherited they can be overwritten on the cloned item. And just like with standard values, to discard the overridden value and revert to the source items field value, simply reset the field back to null by using the reset command on the fields chunk on the versions tab in the content editor.

If a field on the source item is updated which has not been overridden on the cloned item, then the cloned item will automatically inherit the new values, just like in standard values. If the field had been overridden on the cloned item though, the cloned item would be notified of the change by means of a content editor warning.

item clone updated

Clicking the accept link simply resets the field value back to null on the cloned item so it inherits the source item’s field value once again. Keep in mind though that you only have to accept the change in value if that field has been overridden on the cloned item, which is probably what you want. If the changes were pushed down automatically then you’d be losing data without being prompted.

Because the cloned item contains a strong reference back to the source item through the __source field the links database will pick the clones up. You can see all the clones of an item in the links gallery on the navigate tab in the content editor.

cloned items

Just look for referrers with the source item in the __source field.

But what if I didn’t want my content authors to override fields on the cloned item. Let’s say we have a pool of shared content in a folder, and these items are shared out around the site (or across multiple sites). High privileged users are generating this content which must appear in various locations, but we don’t want the content authors to change or override the content.

If no other content is to be created by normal authors in the locations where the cloned items exist then we can use Sitecore security to ensure the content can’t be updated. Just remove the write access right from the normal author role. But if we will also have new items being created by authors in that same location, then we’ll need a different approach.

Luckily we can use out of the box features to achieve this; workflow. Workflow security provides an additional contextual overlay on top of normal item security. We can create a “no edit” workflow which contains a single state which is marked as final and is also the initial state, then deny the Workflow State Write access right to the appropriate accounts. Then when someone creates a cloned item they can place the cloned item into the “no edit” workflow. The workflow fields are some of the fields which aren’t cloned. Refer to John West’s post above for the full list of fields which do not inherit through clones.

One of the default behaviours of item clones is to clone children of the source item to the cloned item as they are added. This keeps the 2 subtrees of content in sync. But I can see situations where we want to clone all items in a shared folder, but not the folder itself. To have this happen automatically we need to create an event handler which is processed when items are created in the shared folder and create clones in one or more target folders. The below class will achieve this.

using System;
using System.Collections.Generic;
using Sitecore.Data;
using Sitecore.Data.Items;

namespace ItemClones
{
  public class SharedFolderSync
  {
    List<string> m_targetPaths = null;
    string m_sourcePath = string.Empty;

    public Database Database
    {
      get;
      set;
    }

    public string SourcePath
    {
      get
      {
        return m_sourcePath;
      }

      set
      {
        m_sourcePath = value.ToLower();
      }
    }

    public string[] TargetPaths
    {
      get
      {
        return m_targetPaths.ToArray();
      }
    }

    public SharedFolderSync(string database)
    {
      Database = Sitecore.Configuration.Factory.GetDatabase(database);
      m_targetPaths = new List<string>();
    }

    public void AddTargetPath(string path)
    {
      m_targetPaths.Add(path);
    }

    public void OnItemAdded(object sender, EventArgs args)
    {
      var item = Sitecore.Events.Event.ExtractParameter(args, 0) as Item;

      if ((item.Database == Database) &&
        (item.Paths.FullPath.ToLower().StartsWith(m_sourcePath)))
      {
        foreach (var path in TargetPaths)
        {
          var targetItem = item.Database.GetItem(path);
          if (targetItem != null)
            item.CloneTo(targetItem, true);
        }
      }
    }
  }
}

And you’ll also need to bind this handler to the event through a configuration patch file.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">   <sitecore>
    <events>
      <event name="item:added">
        <handler type="ItemClones.SharedFolderSync,ItemClones"
          method="OnItemAdded">
          <param desc="database">master</param>
          <SourcePath>/sitecore/content/share</SourcePath>
          <TargetPaths hint="list:AddTargetPath">
            <Path>/sitecore/content/Home/News-and-Events</Path>
          </TargetPaths>
        </handler>
      </event>
    </events>
  </sitecore>
</configuration>

Now you’ve got all your content shared and secured and synced, the last piece of the puzzle is presentation. If the content is being shared around a single site and / or the presentation never has to change, then you can skip this last step. But if the presentation needs to change depending on the context we’ll need to look at how to do that. Sitecore only provides a single __renderings field to control the presentation of the item.

Even if you need to change the look & feel of the content when used in different contexts, you may be lucky enough to be able to achieve that purely through CSS. Simply have a web control which outputs the link tags for the stylesheets which recognises the context (host name, path, etc) and picks the appropriate files to include.

But if you have to completely change the presentation, or CSS won’t cut it, then we’ll need to look into changing the presentation per context.

The first solution to this problem was provided by John West on one of the MVP forums on SDN. For a multi-site setup where the content needs to change presentation per site John suggested creating a device per site and setting the presentation there, then configuring each site to use the appropriate device. This is a very nice and elegant solution and can be achieved out of the box with no additional code required.

My concern with the above approach though is when you’re already using multiple devices on each site. In these cases the explosion of devices might be quite large and make management quite difficult. So for those cases I would suggest extending the standard values provider to short circuit the __renderings field and picking a separate item containing the presentation required.

The class which provides standard values is configured in web.config in the /configuration/sitecore/standardValues section. To start, I’ll have to create a new class which inherits the existing standard values provider. For the purpose of this article I’ll make the presentation configurable by site. To provide this configuration the standard values item under the template definition can be duplicated and named after the site the standard values is relevant for.

using Sitecore;
using Sitecore.Data.Fields;

namespace ItemClones
{
  public class StandardValuesProvider : Sitecore.Data.StandardValuesProvider
  {
    public override string GetStandardValue(Sitecore.Data.Fields.Field field)
    {
      if (field.ID == FieldIDs.LayoutField)
      {
        // check if there's a site specific standard values
        var siteStandardValues =
          field.Item.Template.InnerItem.Axes.GetChild(Sitecore.Context.Site.Name);
        if (siteStandardValues != null &&
          siteStandardValues.TemplateID == field.Item.TemplateID &&
          siteStandardValues.ID != field.Item.ID)
          return ((LayoutField)siteStandardValues.Fields[field.ID]).Value;
      }

      return base.GetStandardValue(field);
    }
  }
}

And the config patch you’ll need to go with it:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <standardValues>
      <providers>
        <add name="sitecore">
          <patch:attribute name="type">
            ItemClones.StandardValuesProvider, ItemClones
          </patch:attribute>
        </add>
      </providers>
    </standardValues>
  </sitecore>
</configuration>

Keep in mind that the above code is going to run for every single access to the __renderings field, so it must be as efficient as possible. If you were going to use this approach you’d probably also want to make use of the standard values cache.

I hope this has helped spread a little more light over item clones and show an end to end solution for how to use them in content sharing scenarios.

Comments

A new project I'm working on is requiring quite a few different aspects from the previous Sitecore project I worked on. It's a multi-site solution and I've been struggling with an efficient way of sharing content across 5 domains with about equal amounts of shared and non-shared content.
The premise is that a single sign-on CMS is present so I am not able to split these sites up into separate Sitecore installations. Your solution looks elegant and I decided to take a test run to see if it would work for our implementation. I'm rather new to .Net in general and Sitecore. I'm not familiar with extending Sitecore very much, but have a feeling it will happen more and more with this project.
I'm getting the error below when trying to implement your solution. I created the class and placed it in my /bin folder (un-compiled) and included the config (ItemClones.SharedFolderSync.config) in the Include folder.
Could not resolve type name: ItemClones.SharedFolderSync,ItemClones (method: Sitecore.Configuration.Factory.CreateType(XmlNode configNode, String[] parameters, Boolean assert)).
Is there any guidance you are willing to give on what I'm doing wrong?

Alistair Deneys

Hi Alexander,
You need to compile the code and put the resulting dll in the bin folder.

Mads

Hi Alistair
Great post, it really helped me out.
In the project I am working with I would like to delete all clones if a item is deleted. So far I am using the item:deleting event to do this. But is there any way i can intercept the delete process before clone items are recreated as real items? Also the current way i can not stop sitecore from showing the window where it state that the item have clones.
Best regards Mads

Alistair Deneys

Hi Mads, Looking through web.config it appears the clones are created as real items (uncloned) in the uiDeleteItems and uiArchiveItems pipelines, so you could probably pretty easily create a pipeline processor for each of those pipelines to delete the clones when the item is deleted.

Mads

Hi Alistair
Thanks, i will look into this. Do you know if this would also remove the window that state that the item have clones which will not be deleted?
Best regards Mads

Alistair Deneys

Hi Mads, The CheckCloneLinks processor in the uiDeleteItems pipeline is responsible for that warning popup. You might need to disable or modify it to suppress it when required.

Philip

Wouldn't it make more sense to modify the dialog to give the user a choice? - Delete the item and its clones or - Delete the item and convert the clones to normal items or - Cancel
This should be built-in behavior in Sitecore.

Alistair Deneys

Hi Philip, That sounds like a very good suggestion.

[...] For examples of how to write event handlers for this, see here: [/blog/2010/11/02/sharing-content-with-item-clones/](/blog/2010/11/02/sharing-content-with-item-clones/) [...]

[&#8230;] For examples of how to write event handlers for this, see here: [/blog/2010/11/02/sharing-content-with-item-clones/](/blog/2010/11/02/sharing-content-with-item-clones/) [&#8230;]

Leave a comment

All fields are required.