Editor Extensions – Copy a page with its components

What Sitecore standard copy functionality does:

  1. Copies the whole item tree under the selected item
  2. Copies all item versions (if you have more)
  3. All references (included the datasources in your components) remain the same

So in some cases it is good but especially when you have pages with local component datasources the standard copy function does not really for you.

To solve this problem implemented the following solution but it has some prerequisites:

  1. The Page item should have folder item named Content, the the item identicated as a Page
  2. The Content folder should contain all local datasource items for the actual Page

What this button does?

  1. Creates a new item from the selected item with the Content item recursively
  2. Creates new language versions from the last version of the selected Page item
  3. Sets the new item’s datasource references in the Final Layout and Shared Layout
  4. Sets the new item’s rule action datasource references in the Final Layout and Shared Layout

At first I have the following config which registers a command for the button in Content Editor and a processor which does the main job.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/&quot; xmlns:x="http://www.sitecore.net/xmlconfig/"&gt;
<sitecore>
<commands>
<command name="skeleton:copypage" type="Helix.Skeleton.Foundation.EditorExtensions.Commands.CopyPage,Helix.Skeleton.Foundation.EditorExtensions"/>
</commands>
<processors>
<skeletonUiCopyPage>
<processor mode="on" type="Helix.Skeleton.Foundation.EditorExtensions.Pipelines.CopyPage.CopyPage,Helix.Skeleton.Foundation.EditorExtensions" method="GetDestination"/>
<processor mode="on" type="Helix.Skeleton.Foundation.EditorExtensions.Pipelines.CopyPage.CopyPage,Helix.Skeleton.Foundation.EditorExtensions" method="CheckDestination"/>
<processor mode="on" type="Helix.Skeleton.Foundation.EditorExtensions.Pipelines.CopyPage.CopyPage,Helix.Skeleton.Foundation.EditorExtensions" method="CheckLanguage"/>
<processor mode="on" type="Helix.Skeleton.Foundation.EditorExtensions.Pipelines.CopyPage.CopyPage,Helix.Skeleton.Foundation.EditorExtensions" method="Execute"/>
</skeletonUiCopyPage>
</processors>
</sitecore>
</configuration>

Here we have the command which calls the pipeline.

namespace Helix.Skeleton.Foundation.EditorExtensions.Commands
{
using System.Collections.Specialized;
using System.Linq;
using System.Web.Mvc;
using Helix.Skeleton.Foundation.EditorExtensions.Consts.CopyPage;
using Helix.Skeleton.Foundation.EditorExtensions.Services;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Shell.Framework.Pipelines;
public class CopyPage : Command
{
private const string CopyPagePipelineName = "skeletonUiCopyPage";
private readonly ICopyPageService duplicatePageService;
public CopyPage()
{
this.duplicatePageService = DependencyResolver.Current.GetService<ICopyPageService>();
}
public override void Execute(CommandContext context)
{
NameValueCollection parameters = new NameValueCollection
{
{ ParameterKeys.DatabaseKey, context.Items.First().Database.Name },
{ ParameterKeys.ItemsKey, string.Join("|", context.Items.Select(i => i.ID)) },
{ ParameterKeys.LanguageKey, context.Items.First().Language.Name }
};
Context.ClientPage.Start(CopyPagePipelineName, new CopyItemsArgs { Parameters = parameters });
}
public override CommandState QueryState(CommandContext context)
{
var currentItem = this.GetCurrentItem(context);
if (currentItem == null)
{
return CommandState.Disabled;
}
var contentFolder = this.duplicatePageService.GetContentFolder(currentItem.Paths.FullPath, currentItem.Database.Name);
if (contentFolder == null)
{
return CommandState.Disabled;
}
return base.QueryState(context);
}
private Item GetCurrentItem(CommandContext context)
{
if (context.Items == null || context.Items.Count() != 1)
{
return null;
}
return context.Items.First();
}
}
}
view raw CopyPage.cs hosted with ❤ by GitHub

Here is the referenced pipeline.

namespace Helix.Skeleton.Foundation.EditorExtensions.Pipelines.CopyPage
{
using System.Collections.Specialized;
using System.Web.Mvc;
using Helix.Skeleton.Foundation.EditorExtensions.Consts.CopyPage;
using Helix.Skeleton.Foundation.EditorExtensions.Models.CopyPage;
using Helix.Skeleton.Foundation.EditorExtensions.Services;
using Sitecore;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Globalization;
using Sitecore.Shell.Framework.Pipelines;
using Sitecore.Web.UI.Sheer;
public class CopyPage : CopyItems
{
private readonly ICopyPageService copyPageService;
public CopyPage()
{
this.copyPageService = DependencyResolver.Current.GetService<ICopyPageService>();
}
public override void Execute(CopyItemsArgs args)
{
if (args == null)
{
return;
}
var parameters = this.MapParameters(args.Parameters);
if (!this.AreParametersValid(parameters))
{
SheerResponse.Alert("Parameters are not valid.");
return;
}
var database = Factory.GetDatabase(parameters.Database);
if (database == null)
{
SheerResponse.Alert("Database is null.");
return;
}
var oldItem = database.GetItem(ID.Parse(parameters.ItemId), Language.Parse(parameters.Language));
if (oldItem == null)
{
SheerResponse.Alert("Current item is null.");
return;
}
var targetItem = database.GetItem(ID.Parse(parameters.DestinationId));
if (targetItem == null)
{
SheerResponse.Alert("Target item is null.");
return;
}
var oldContentFolder = this.copyPageService.GetContentFolder(oldItem.Paths.FullPath, parameters.Database);
if (oldContentFolder == null)
{
SheerResponse.Alert("Current item is not a page.");
return;
}
var newItemName = ItemUtil.GetCopyOfName(targetItem, oldItem.Name);
// Create new page item without version and language
var newItemWithoutVersion = ItemManager.CreateItem(newItemName, oldItem.Parent, oldItem.TemplateID);
// Create new content folder item
oldContentFolder.CopyTo(newItemWithoutVersion, oldContentFolder.Name);
foreach (var language in oldItem.Languages)
{
var languageSpecificNewItem = this.copyPageService.CopyLastVersion(database, language, oldItem, newItemWithoutVersion);
if (languageSpecificNewItem == null)
{
continue;
}
this.copyPageService.AdjustLayout(database, languageSpecificNewItem, oldItem, FieldIDs.LayoutField);
this.copyPageService.AdjustLayout(database, languageSpecificNewItem, oldItem, FieldIDs.FinalLayoutField);
}
}
private IParameters MapParameters(NameValueCollection parameters)
{
return new Parameters
{
Database = parameters.Get(ParameterKeys.DatabaseKey),
ItemId = parameters.Get(ParameterKeys.ItemsKey),
Language = parameters.Get(ParameterKeys.LanguageKey),
DestinationId = parameters.Get(ParameterKeys.DestinationKey)
};
}
private bool AreParametersValid(IParameters parameters)
{
return parameters != null
&& !string.IsNullOrEmpty(parameters.DestinationId)
&& !string.IsNullOrEmpty(parameters.Database)
&& !string.IsNullOrEmpty(parameters.ItemId)
&& !string.IsNullOrEmpty(parameters.Language);
}
}
}
view raw CopyPage.cs hosted with ❤ by GitHub

As you can see it calls the another service class where I have implemented the main logic.

namespace Helix.Skeleton.Foundation.EditorExtensions.Services
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Helix.Skeleton.Foundation.EditorExtensions.Consts.CopyPage;
using Helix.Skeleton.Foundation.EditorExtensions.Models.CopyPage;
using Sitecore;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Layouts;
using Sitecore.Rules.ConditionalRenderings;
public class CopyPageService : ICopyPageService
{
private const string ContentFolderName = "Content";
private const string GetItemsByNameAndTemplateQueryFormat = "{0}/*[@@name='{1}' and @@templateid='{2}']";
public Item GetContentFolder(string currentItemPath, string databaseName)
{
var foundContentItems = Factory
.GetDatabase(databaseName)
.SelectItems(string.Format(GetItemsByNameAndTemplateQueryFormat, currentItemPath.Replace("-", "#-#").Replace("$", "#$#"), ContentFolderName, TemplateIDs.Folder.ToString()));
if (foundContentItems.Length > 1)
{
return null;
}
return foundContentItems.FirstOrDefault();
}
public Item CopyLastVersion(Database database, Language language, Item source, Item destinationWithoutVersion)
{
var languageSpecificSource = database.GetItem(source.ID, language);
if (languageSpecificSource.Versions.Count == 0)
{
return null;
}
var languageSpecificDestination = database.GetItem(destinationWithoutVersion.ID, language);
languageSpecificDestination = languageSpecificDestination.Versions.AddVersion();
languageSpecificDestination.Editing.BeginEdit();
languageSpecificSource.Fields.ReadAll();
foreach (Field field in languageSpecificSource.Fields)
{
languageSpecificDestination.Fields[field.ID].SetValue(field.Value, true);
}
languageSpecificDestination.Editing.EndEdit();
languageSpecificDestination.Editing.AcceptChanges();
return languageSpecificDestination;
}
public void AdjustLayout(Database database, Item newItem, Item oldItem, ID layoutFieldId)
{
var allDevices = database.Resources.Devices.GetAll();
var newLayoutField = new LayoutField(newItem.Fields[layoutFieldId]);
var oldDatasources = new List<IOldDatasourceItem>();
foreach (var device in allDevices)
{
var renderingReferences = newLayoutField.GetReferences(device);
if (renderingReferences == null)
{
continue;
}
// Collect datasource references
oldDatasources.AddRange(this.GetOldReferences(database, renderingReferences.Where(r => r != null), r => r.Settings.DataSource));
// Collect personalization references
var conditionalRenderings = renderingReferences
.Where(r => r?.Settings?.Rules?.Rules != null)
.SelectMany(r => r.Settings.Rules.Rules
.SelectMany(rule => rule?.Actions
.Select(a => a as SetDataSourceAction<ConditionalRenderingsRuleContext>).Where(a => a != null)));
oldDatasources.AddRange(this.GetOldReferences(database, conditionalRenderings, c => c.DataSource));
}
var newLayoutDefinition = LayoutDefinition.Parse(newLayoutField.Value);
foreach (var device in newLayoutDefinition.Devices.Cast<DeviceDefinition>())
{
var renderings = device.Renderings.Cast<RenderingDefinition>();
// Set datasource referneces
this.SetNewReferences(
new SetNewReferencesParameters<RenderingDefinition>
{
Database = database,
NewItem = newItem,
OldItem = oldItem,
NewLayout = newLayoutField,
NewLayoutDefinition = newLayoutDefinition,
OldDatasources = oldDatasources,
SourceList = renderings.Where(r => r.Datasource != null),
GetIdValue = r => r.Datasource,
SetIdValue = (r, d) => r.Datasource = d
});
// Set personalization references
var actions = renderings
.Where(r => r?.Rules?.Descendants(RuleAttributeKeys.ActionKey) != null)
.SelectMany(r => r.Rules.Descendants(RuleAttributeKeys.ActionKey)
.Select(a => a.Attribute(RuleAttributeKeys.DatasourceKey))
.Where(a => a != null));
this.SetNewReferences(
new SetNewReferencesParameters<XAttribute>
{
Database = database,
NewItem = newItem,
OldItem = oldItem,
NewLayout = newLayoutField,
NewLayoutDefinition = newLayoutDefinition,
OldDatasources = oldDatasources,
SourceList = actions,
GetIdValue = r => r.Value,
SetIdValue = (r, d) => r.SetValue(d)
});
}
}
private IEnumerable<IOldDatasourceItem> GetOldReferences<T>(Database database, IEnumerable<T> sourceList, Func<T, string> getDatasourceId)
{
var oldDatasources = new List<IOldDatasourceItem>();
if (sourceList == null)
{
return oldDatasources;
}
foreach (var rendering in sourceList)
{
ID oldDatasourceId;
if (!ID.TryParse(getDatasourceId.Invoke(rendering), out oldDatasourceId))
{
continue;
}
var oldDatasource = database.GetItem(oldDatasourceId);
if (oldDatasource == null)
{
continue;
}
// Handle duplicated paths
var oldDatasourcesWithSamePath = database.SelectItems(oldDatasource.Paths.FullPath).ToList();
oldDatasources.Add(
new OldDatasourceItem
{
Id = oldDatasourceId,
Path = oldDatasource.Paths.FullPath,
Index = oldDatasourcesWithSamePath.IndexOf(oldDatasourcesWithSamePath.FirstOrDefault(d => d.ID == oldDatasourceId))
});
}
return oldDatasources;
}
private void SetNewReferences<T>(ISetNewReferencesParameters<T> parameters)
{
foreach (var rendering in parameters.SourceList)
{
var datasource = parameters.GetIdValue.Invoke(rendering);
var matchedOldDatasource = parameters.OldDatasources.FirstOrDefault(d => d.Id.ToString() == datasource);
if (matchedOldDatasource == null)
{
continue;
}
string newDatasourcePath = matchedOldDatasource.Path.ToString().Replace(parameters.OldItem.Paths.FullPath, parameters.NewItem.Paths.FullPath);
var newDatasource = parameters.Database.SelectItems(newDatasourcePath)[matchedOldDatasource.Index];
parameters.SetIdValue.Invoke(rendering, newDatasource.ID.ToString());
parameters.NewItem.Editing.BeginEdit();
parameters.NewLayout.Value = parameters.NewLayoutDefinition.ToXml();
parameters.NewItem.Editing.EndEdit();
}
}
}
}
view raw CopyPageService.cs hosted with ❤ by GitHub

namespace Helix.Skeleton.Foundation.EditorExtensions.Services
{
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;
public interface ICopyPageService
{
Item GetContentFolder(string currentItemPath, string databaseName);
Item CopyLastVersion(Database database, Language language, Item source, Item destinationWithoutVersion);
void AdjustLayout(Database database, Item newItem, Item oldItem, ID layoutFieldId);
}
}
view raw ICopyPageService.cs hosted with ❤ by GitHub

I have some extra class which are the model and constant classes.

namespace Helix.Skeleton.Foundation.EditorExtensions.Consts.CopyPage
{
public class ParameterKeys
{
public const string DatabaseKey = "database";
public const string ItemsKey = "items";
public const string LanguageKey = "language";
public const string DestinationKey = "destination";
}
}
view raw ParameterKeys.cs hosted with ❤ by GitHub

namespace Helix.Skeleton.Foundation.EditorExtensions.Consts.CopyPage
{
public static class RuleAttributeKeys
{
public const string DatasourceKey = "DataSource";
public const string ActionKey = "action";
}
}
view raw RuleAttributeKeys.cs hosted with ❤ by GitHub

namespace Helix.Skeleton.Foundation.EditorExtensions.Models.CopyPage
{
using Sitecore.Data;
public interface IOldDatasourceItem
{
ID Id { get; set; }
string Path { get; set; }
int Index { get; set; }
}
}

namespace Helix.Skeleton.Foundation.EditorExtensions.Models.CopyPage
{
using Sitecore.Data;
public class OldDatasourceItem : IOldDatasourceItem
{
public ID Id { get; set; }
public string Path { get; set; }
public int Index { get; set; }
}
}
view raw OldDatasourceItem.cs hosted with ❤ by GitHub

namespace Helix.Skeleton.Foundation.EditorExtensions.Models.CopyPage
{
using System;
using System.Collections.Generic;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Layouts;
public interface ISetNewReferencesParameters<T>
{
Database Database { get; set; }
Item NewItem { get; set; }
Item OldItem { get; set; }
LayoutField NewLayout { get; set; }
LayoutDefinition NewLayoutDefinition { get; set; }
IEnumerable<IOldDatasourceItem> OldDatasources { get; set; }
IEnumerable<T> SourceList { get; set; }
Func<T, string> GetIdValue { get; set; }
Action<T, string> SetIdValue { get; set; }
}
}

namespace Helix.Skeleton.Foundation.EditorExtensions.Models.CopyPage
{
using System;
using System.Collections.Generic;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Layouts;
public class SetNewReferencesParameters<T> : ISetNewReferencesParameters<T>
{
public Database Database { get; set; }
public Item NewItem { get; set; }
public Item OldItem { get; set; }
public LayoutField NewLayout { get; set; }
public LayoutDefinition NewLayoutDefinition { get; set; }
public IEnumerable<IOldDatasourceItem> OldDatasources { get; set; }
public IEnumerable<T> SourceList { get; set; }
public Func<T, string> GetIdValue { get; set; }
public Action<T, string> SetIdValue { get; set; }
}
}

namespace Helix.Skeleton.Foundation.EditorExtensions.Models.CopyPage
{
public interface IParameters
{
string Database { get; set; }
string ItemId { get; set; }
string Language { get; set; }
string DestinationId { get; set; }
}
}
view raw IParameters.cs hosted with ❤ by GitHub

namespace Helix.Skeleton.Foundation.EditorExtensions.Models.CopyPage
{
public class Parameters : IParameters
{
public string Database { get; set; }
public string ItemId { get; set; }
public string Language { get; set; }
public string DestinationId { get; set; }
}
}
view raw Parameters.cs hosted with ❤ by GitHub

At the end create a button in the Core database.

  • Path: /sitecore/content/Applications/Content Editor/Ribbons/Chunks/Operations
  • Name: Copy Page To
  • Click field: skeleton:copypage

Aaaand here is the new button!

Capture

Happy copying!

 

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s