Home » Sitecore (Page 12)
Category Archives: Sitecore
Tailor Sitecore Item Web API Field Values On Read
Last week Sitecore MVP Kamruz Jaman asked me in this tweet if I could answer this question on Stack Overflow.
The person asking the question wanted to know why alt text for images aren’t returned in responses from the Sitecore Item Web API, and was curious if it were possible to include these.
After digging around the Sitecore.ItemWebApi.dll and my local copy of /App_Config/Include/Sitecore.ItemWebApi.config — this config file defines a bunch of pipelines and their processors that can be augmented or overridden — I learned field values are returned via logic in the Sitecore.ItemWebApi.Pipelines.Read.GetResult class, which is exposed in /configuration/sitecore/pipelines/itemWebApiRead/processor[@type=”Sitecore.ItemWebApi.Pipelines.Read.GetResult, Sitecore.ItemWebApi”] in /App_Config/Include/Sitecore.ItemWebApi.config:
This is an example of a raw value for an image field — it does not include the alt text for the image:
I spun up a copy of the console application written by Kern Herskind Nightingale — Director of Technical Services at Sitecore UK — to show the value returned by the above pipeline processor for an image field:
The Sitecore.ItemWebApi.Pipelines.Read.GetResult class exposes a virtual method hook — the protected method GetFieldInfo() — that allows custom code to change a field’s value before it is returned.
I wrote the following class as an example for changing an image field’s value:
using System;
using System.IO;
using System.Web;
using System.Web.UI;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Read;
using Sitecore.Web.UI.WebControls;
using HtmlAgilityPack;
namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Read
{
public class EnsureImageFieldAltText : GetResult
{
protected override Dynamic GetFieldInfo(Field field)
{
Assert.ArgumentNotNull(field, "field");
Dynamic dynamic = base.GetFieldInfo(field);
AddAltTextForImageField(dynamic, field);
return dynamic;
}
private static void AddAltTextForImageField(Dynamic dynamic, Field field)
{
Assert.ArgumentNotNull(dynamic, "dynamic");
Assert.ArgumentNotNull(field, "field");
if(IsImageField(field))
{
dynamic["Value"] = AddAltTextToImages(field.Value, GetAltText(field));
}
}
private static string AddAltTextToImages(string imagesXml, string altText)
{
if (string.IsNullOrWhiteSpace(imagesXml) || string.IsNullOrWhiteSpace(altText))
{
return imagesXml;
}
HtmlDocument htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(imagesXml);
HtmlNodeCollection images = htmlDocument.DocumentNode.SelectNodes("//image");
foreach (HtmlNode image in images)
{
if (image.Attributes["src"] != null)
{
image.SetAttributeValue("src", GetAbsoluteUrl(image.GetAttributeValue("src", string.Empty)));
}
image.SetAttributeValue("alt", altText);
}
return htmlDocument.DocumentNode.InnerHtml;
}
private static string GetAbsoluteUrl(string url)
{
Assert.ArgumentNotNullOrEmpty(url, "url");
Uri uri = HttpContext.Current.Request.Url;
if (url.StartsWith(uri.Scheme))
{
return url;
}
string port = string.Empty;
if (uri.Port != 80)
{
port = string.Concat(":", uri.Port);
}
return string.Format("{0}://{1}{2}/~{3}", uri.Scheme, uri.Host, port, VirtualPathUtility.ToAbsolute(url));
}
private static string GetAltText(Field field)
{
Assert.ArgumentNotNull(field, "field");
if (IsImageField(field))
{
ImageField imageField = field;
if (imageField != null)
{
return imageField.Alt;
}
}
return string.Empty;
}
private static bool IsImageField(Field field)
{
Assert.ArgumentNotNull(field, "field");
return field.Type == "Image";
}
}
}
The class above — with the help of the Sitecore.Data.Fields.ImageField class — gets the alt text for the image, and adds a new alt XML attribute to the XML before it is returned.
The class also changes the relative url defined in the src attribute in to be an absolute url.
I then swapped out /configuration/sitecore/pipelines/itemWebApiRead/processor[@type=”Sitecore.ItemWebApi.Pipelines.Read.GetResult, Sitecore.ItemWebApi”] with the class above in /App_Config/Include/Sitecore.ItemWebApi.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<!-- Lots of stuff here -->
<!-- Handles the item read operation. -->
<itemWebApiRead>
<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Read.EnsureImageFieldAltText, Sitecore.Sandbox" />
</itemWebApiRead>
<!--Lots of stuff here too -->
</pipelines>
<!-- Even more stuff here -->
</sitecore>
</configuration>
I then reran the console application to see what the XML now looks like, and as you can see the new alt attribute was added:
You might be thinking “Mike, image field XML values are great in Sitecore’s Content Editor, but client code consuming this data might have trouble with it. Is there anyway to have HTML be returned instead of XML?
You bet!
The following subclass of Sitecore.ItemWebApi.Pipelines.Read.GetResult returns HTML, not XML:
using System;
using System.IO;
using System.Web;
using System.Web.UI;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.ItemWebApi;
using Sitecore.ItemWebApi.Pipelines.Read;
using Sitecore.Web.UI.WebControls;
using HtmlAgilityPack;
namespace Sitecore.Sandbox.ItemWebApi.Pipelines.Read
{
public class TailorFieldValue : GetResult
{
protected override Dynamic GetFieldInfo(Field field)
{
Assert.ArgumentNotNull(field, "field");
Dynamic dynamic = base.GetFieldInfo(field);
TailorValueForImageField(dynamic, field);
return dynamic;
}
private static void TailorValueForImageField(Dynamic dynamic, Field field)
{
Assert.ArgumentNotNull(dynamic, "dynamic");
Assert.ArgumentNotNull(field, "field");
if (field.Type == "Image")
{
dynamic["Value"] = SetAbsoluteUrlsOnImages(GetImageHtml(field));
}
}
private static string SetAbsoluteUrlsOnImages(string html)
{
if (string.IsNullOrWhiteSpace(html))
{
return html;
}
HtmlDocument htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(html);
HtmlNodeCollection images = htmlDocument.DocumentNode.SelectNodes("//img");
foreach (HtmlNode image in images)
{
if (image.Attributes["src"] != null)
{
image.SetAttributeValue("src", GetAbsoluteUrl(image.GetAttributeValue("src", string.Empty)));
}
}
return htmlDocument.DocumentNode.InnerHtml;
}
private static string GetAbsoluteUrl(string url)
{
Assert.ArgumentNotNullOrEmpty(url, "url");
Uri uri = HttpContext.Current.Request.Url;
if (url.StartsWith(uri.Scheme))
{
return url;
}
string port = string.Empty;
if (uri.Port != 80)
{
port = string.Concat(":", uri.Port);
}
return string.Format("{0}://{1}{2}{3}", uri.Scheme, uri.Host, port, VirtualPathUtility.ToAbsolute(url));
}
private static string GetImageHtml(Field field)
{
return GetImageHtml(field.Item, field.Name);
}
private static string GetImageHtml(Item item, string fieldName)
{
Assert.ArgumentNotNull(item, "item");
Assert.ArgumentNotNullOrEmpty(fieldName, "fieldName");
return RenderImageControlHtml(new Image { Item = item, Field = fieldName });
}
private static string RenderImageControlHtml(Image image)
{
Assert.ArgumentNotNull(image, "image");
string html = string.Empty;
using (TextWriter textWriter = new StringWriter())
{
using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(textWriter))
{
image.RenderControl(htmlTextWriter);
}
html = textWriter.ToString();
}
return html;
}
}
}
The class above uses an instance of the Image field control (Sitecore.Web.UI.WebControls.Image) to do all the work for us around building the HTML for the image, and we also make sure the url within it is absolute — just as we had done above.
I then wired this up to my local Sitecore instance in /App_Config/Include/Sitecore.ItemWebApi.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<!-- Lots of stuff here -->
<!-- Handles the item read operation. -->
<itemWebApiRead>
<processor type="Sitecore.Sandbox.ItemWebApi.Pipelines.Read.TailorFieldValue, Sitecore.Sandbox" />
</itemWebApiRead>
<!--Lots of stuff here too -->
</pipelines>
<!-- Even more stuff here -->
</sitecore>
</configuration>
I then executed the console application, and was given back HTML for the image:
If you can think of other reasons for manipulating field values in subclasses of Sitecore.ItemWebApi.Pipelines.Read.GetResult, please drop a comment.
Addendum
Kieran Marron — a Lead Developer at Sitecore — wrote another Sitecore.ItemWebApi.Pipelines.Read.GetResult subclass example that returns an image’s alt text in the Sitecore Item Web API response via a new JSON property. Check it out!
Display Content Management Server Information in the Sitecore CMS
The other day I cogitated over potential uses for the getAboutInformation pipeline. Found at /configuration/sitecore/pipelines/getAboutInformation in the Web.config, it can be leveraged to display information on the Sitecore login page, and inside of the About dialog — a dialog that can be launched from the Content Editor.
One thing that came to mind was displaying some information for the Content Management (CM) server where the Sitecore instance lives. Having this information readily available might aid in troubleshooting issues that arise, or seeing the name of the server might stop you from making content changes on the wrong CM server (I am guilty as charged for committing such a blunder in the past).
This post shows how I translated that idea into code.
The first thing we need is a way to get server information. I defined the following interface to describe information we might be interested in for a server:
namespace Sitecore.Sandbox.Utilities.Server.Base
{
public interface IServer
{
string Name { get; }
string Cpu { get; }
string OperatingSystem { get; }
}
}
We now need a class to implement the above interface. I stumbled upon a page whose author shared how one can acquire server information using classes defined in the System.Management namespace in .NET.
Using information from that page coupled with some experimentation, I came up with the following class:
using System.Collections.Generic;
using System.Linq;
using System.Management;
using Sitecore.Diagnostics;
using Sitecore.Sandbox.Utilities.Server.Base;
namespace Sitecore.Sandbox.Utilities.Server
{
public class Server : IServer
{
private string _Name;
public string Name
{
get
{
if (string.IsNullOrEmpty(_Name))
{
_Name = GetServerName();
}
return _Name;
}
}
private string _Cpu;
public string Cpu
{
get
{
if (string.IsNullOrEmpty(_Cpu))
{
_Cpu = GetCpuInformation();
}
return _Cpu;
}
}
private string _OperatingSystem;
public string OperatingSystem
{
get
{
if (string.IsNullOrEmpty(_OperatingSystem))
{
_OperatingSystem = GetOperatingSystemName();
}
return _OperatingSystem;
}
}
private Server()
{
}
private static string GetServerName()
{
return GetFirstManagementBaseObjectPropertyFirstInnerProperty("Win32_ComputerSystem", "name");
}
private static string GetCpuInformation()
{
return GetFirstManagementBaseObjectPropertyFirstInnerProperty("Win32_Processor", "name");
}
private static string GetOperatingSystemName()
{
return GetFirstManagementBaseObjectPropertyFirstInnerProperty("Win32_OperatingSystem", "name");
}
private static string GetFirstManagementBaseObjectPropertyFirstInnerProperty(string key, string propertyName)
{
return GetFirstManagementBaseObjectPropertyInnerProperties(key, propertyName).FirstOrDefault();
}
private static IEnumerable<string> GetFirstManagementBaseObjectPropertyInnerProperties(string key, string propertyName)
{
return GetFirstManagementBaseObjectProperty(key, propertyName).Split('|');
}
private static string GetFirstManagementBaseObjectProperty(string key, string propertyName)
{
return GetFirstManagementBaseObject(key)[propertyName].ToString();
}
private static ManagementBaseObject GetFirstManagementBaseObject(string key)
{
Assert.ArgumentNotNullOrEmpty(key, "key");
WqlObjectQuery query = new WqlObjectQuery(string.Format("select * from {0}", key));
ManagementObjectSearcher searcher = new ManagementObjectSearcher(query);
return searcher.Get().Cast<ManagementBaseObject>().FirstOrDefault();
}
public static IServer CreateNewServer()
{
return new Server();
}
}
}
The class above grabs server information via three separate ManagementObjectSearcher queries, one for each property defined in our IServer interface.
In order to use classes defined in the System.Management namespace, I had to reference System.Management in my project in Visual Studio:
Next, I created a class that contains methods that will serve as our getAboutInformation pipeline processors:
using System.Collections.Generic;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetAboutInformation;
using Sitecore.Sandbox.Utilities.Server.Base;
using Sitecore.Sandbox.Utilities.Server;
namespace Sitecore.Sandbox.Pipelines.GetAboutInformation
{
public class GetContentManagementServerInformation
{
private static readonly string CurrentServerInformationHtml = GetCurrentServerInformationHtml();
public void SetLoginPageText(GetAboutInformationArgs args)
{
args.LoginPageText = CurrentServerInformationHtml;
}
public void SetAboutText(GetAboutInformationArgs args)
{
args.AboutText = CurrentServerInformationHtml;
}
private static string GetCurrentServerInformationHtml()
{
return GetServerInformationHtml(Server.CreateNewServer());
}
private static string GetServerInformationHtml(IServer server)
{
Assert.ArgumentNotNull(server, "server");
IList<string> information = new List<string>();
if (!string.IsNullOrEmpty(server.Name))
{
information.Add(string.Format("<strong>Server Name</strong>: {0}", server.Name));
}
if (!string.IsNullOrEmpty(server.Cpu))
{
information.Add(string.Format("<strong>CPU</strong>: {0}", server.Cpu));
}
if (!string.IsNullOrEmpty(server.OperatingSystem))
{
information.Add(string.Format("<strong>OS</strong>: {0}", server.OperatingSystem));
}
return string.Join("<br />", information);
}
}
}
Both methods set properties on the GetAboutInformationArgs instance using the same HTML generated by the GetServerInformationHtml method. This method is given an instance of the Server class defined above by the GetCurrentServerInformationHtml method.
I then connected all of the above into Sitecore via a configuration include file:
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<getAboutInformation>
<processor type="Sitecore.Sandbox.Pipelines.GetAboutInformation.GetContentManagementServerInformation, Sitecore.Sandbox" method="SetLoginPageText" />
<processor type="Sitecore.Sandbox.Pipelines.GetAboutInformation.GetContentManagementServerInformation, Sitecore.Sandbox" method="SetAboutText" />
</getAboutInformation>
</pipelines>
</sitecore>
</configuration>
Let’s see this in action.
When hitting the Sitecore login page in my browser, I saw server information in the right sidebar, under the Sitecore version and revision numbers:
Next, I logged into Sitecore, opened the Content Editor, and launched the About dialog:
As you can see, my CM server information is also displayed here.
You might be questioning why I didn’t include more server information on both the login page and About dialog. One reason why I omitted displaying other properties is due to discovering that the login page area for showing the LoginPageText string does not grow vertically — I saw this when I did include a few more properties in addition to the three shown above.
Sadly, I did not see what would happen when including these additional properties in the the About dialog. Ascertaining whether it is possible to include more information in the About dialog is warranted, though I will leave that exercise for another day.
If you have any other thoughts or ideas for utilizing getAboutInformation pipeline processors, or other areas in Sitecore where server information might be useful, please drop a comment.
Suppress Most Sitecore Wizard Cancel Confirmation Prompts
By default, clicking the ‘Cancel’ button on most wizard forms in Sitecore yields the following confirmation dialog:
Have you ever said to yourself “Yes, I’m sure I’m sure” after seeing this, and wondered if there were a setting you could toggle to turn it off?
Earlier today, while surfing through my Web.config, the closeWizard client pipeline — located at /sitecore/processors/closeWizard in the Web.config — had caught my eye, and I was taken aback over how I had not noticed it before. I was immediately curious over what gems I might find within its only processor — /sitecore/processors/closeWizard/processor[@type=’Sitecore.Web.UI.Pages.WizardForm, Sitecore.Kernel’ and @method=’Confirmation’] — and whether there would be any utility in overriding/extending it.
At first, I thought having a closeWizard client pipeline processor to completely suppress the “Are you sure you want to close the wizard?” confirmation prompt would be ideal, but then imagined how irate someone might be after clicking the ‘Cancel’ button by accident, which would result in the loss of his/her work.
As a happy medium between always prompting users whether they are certain they want to close their wizards and not prompting at all, I came up with the following closeWizard client pipeline processor:
using System;
using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Pages;
using Sitecore.Web.UI.Sheer;
namespace Sitecore.Sandbox.Web.UI.Pages
{
public class SuppressConfirmationWizardForm : WizardForm
{
private const string SuppressConfirmationRegistryKey = "/Current_User/Content Editor/Suppress Close Wizard Confirmation";
private static string YesNoCancelDialogPrompt { get; set; }
private static int YesNoCancelDialogWidth { get; set; }
private static int YesNoCancelDialogHeight { get; set; }
static SuppressConfirmationWizardForm()
{
YesNoCancelDialogPrompt = Settings.GetSetting("SuppressConfirmationYesNoCancelDialog.Prompt");
YesNoCancelDialogWidth = Settings.GetIntSetting("SuppressConfirmationYesNoCancelDialog.Width", 100);
YesNoCancelDialogHeight = Settings.GetIntSetting("SuppressConfirmationYesNoCancelDialog.Height", 100);
}
public void CloseWizard(ClientPipelineArgs args)
{
if (IsCancel(args))
{
args.AbortPipeline();
return;
}
if (ShouldSaveShouldSuppressConfirmationSetting(args))
{
SaveShouldSuppressConfirmationSetting(args);
}
if (ShouldCloseWizard(args))
{
EndWizard();
}
else
{
SheerResponse.YesNoCancel(YesNoCancelDialogPrompt, YesNoCancelDialogWidth.ToString(), YesNoCancelDialogHeight.ToString());
args.WaitForPostBack();
}
}
private static bool ShouldCloseWizard(ClientPipelineArgs args)
{
Assert.ArgumentNotNull(args, "args");
return args.HasResult || ShouldSuppressConfirmationSetting();
}
private static bool ShouldSuppressConfirmationSetting()
{
return Registry.GetBool(SuppressConfirmationRegistryKey);
}
private static bool ShouldSaveShouldSuppressConfirmationSetting(ClientPipelineArgs args)
{
Assert.ArgumentNotNull(args, "args");
return args.HasResult && !IsCancel(args);
}
private static void SaveShouldSuppressConfirmationSetting(ClientPipelineArgs args)
{
Assert.ArgumentNotNull(args, "args");
Registry.SetBool(SuppressConfirmationRegistryKey, AreEqualIgnoreCase(args.Result, "yes"));
}
private static bool IsCancel(ClientPipelineArgs args)
{
Assert.ArgumentNotNull(args, "args");
return AreEqualIgnoreCase(args.Result, "cancel");
}
private static bool AreEqualIgnoreCase(string one, string two)
{
return string.Equals(one, two, StringComparison.CurrentCultureIgnoreCase);
}
}
}
The pipeline processor above will let users decide whether they want to continue seeing the “Are you sure?”‘ confirmation prompt — albeit I had to change the messaging to something more fitting giving the new functionality (see the patch include configuration file or testing screenshot below for the new messaging).
If a user clicks ‘Yes’, s/he will never be prompted with this dialog again — this preference is saved in a Sitecore registry setting for the user.
Plus, suppressing this dialog in one place will suppress it everywhere it would display
Clicking ‘No’ will ensure the message is displayed again in the future.
Clicking ‘Cancel’ will just close the confirmation dialog, and return the user back to the wizard.
You might be wondering why I subclassed Sitecore.Web.UI.Pages.WizardForm. I had to do this in order to get access to its EndWizard() method which is a protected method. This method closes the wizard form.
I plugged it all in via a patch include configuration file:
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<processors>
<closeWizard>
<processor mode="on" patch:instead="processor[@type='Sitecore.Web.UI.Pages.WizardForm, Sitecore.Kernel' and @method='Confirmation']" type="Sitecore.Sandbox.Web.UI.Pages.SuppressConfirmationWizardForm, Sitecore.Sandbox" method="CloseWizard"/>
</closeWizard>
</processors>
<settings>
<setting name="SuppressConfirmationYesNoCancelDialog.Prompt" value="You are about to close the wizard. Would you also like to avoid this message in the future?" />
<setting name="SuppressConfirmationYesNoCancelDialog.Width" value="100" />
<setting name="SuppressConfirmationYesNoCancelDialog.Height" value="100" />
</settings>
</sitecore>
</configuration>
I tested the above pipeline processor on the wizard for creating a new data template:
I decided to omit screenshots after clicking ‘Yes’, ‘No’ and ‘Cancel’ — there really isn’t much to show since all close the confirmation dialog, with the ‘Yes’ and ‘No’ buttons also closing the wizard.
I also did a little research to see what wizard forms in Sitecore might be impacted by the above, and compiled the following list of wizard form classes — this list contains classes from both Sitecore.Kernel.dll and Sitecore.Client.dll:
- Sitecore.Shell.Applications.Analytics.Lookups.RunLookupForm
- Sitecore.Shell.Applications.Analytics.Reports.Summary.UpdateForm
- Sitecore.Shell.Applications.Analytics.SynchronizeDatabase.SynchronizeDatabaseForm
- Sitecore.Shell.Applications.Analytics.VisitorIdentifications.RunVisitorIdentificationsForm
- Sitecore.Shell.Applications.Databases.CleanUp.CleanUpForm
- Sitecore.Shell.Applications.Dialogs.ArchiveDate.ArchiveDateForm
- Sitecore.Shell.Applications.Dialogs.FixLinks.FixLinksForm
- Sitecore.Shell.Applications.Dialogs.Publish.PublishForm
- Sitecore.Shell.Applications.Dialogs.RebuildLinkDatabase.RebuildLinkDatabaseForm
- Sitecore.Shell.Applications.Dialogs.Reminder.ReminderForm
- Sitecore.Shell.Applications.Dialogs.TransferToDatabase.TransferToDatabaseForm
- Sitecore.Shell.Applications.Dialogs.Upload.UploadForm
- Sitecore.Shell.Applications.Globalization.AddLanguage.AddLanguageForm
- Sitecore.Shell.Applications.Globalization.DeleteLanguage.DeleteLanguageForm
- Sitecore.Shell.Applications.Globalization.ExportLanguage.ExportLanguageForm
- Sitecore.Shell.Applications.Globalization.ImportLanguage.ImportLanguageForm
- Sitecore.Shell.Applications.Globalization.UntranslatedFields.UntranslatedFieldsForm
- Sitecore.Shell.Applications.Install.Dialogs.AddFileSourceForm
- Sitecore.Shell.Applications.Install.Dialogs.AddItemSourceForm
- Sitecore.Shell.Applications.Install.Dialogs.AddStaticFileSourceDialog
- Sitecore.Shell.Applications.Install.Dialogs.AddStaticItemSourceDialog
- Sitecore.Shell.Applications.Install.Dialogs.BuildPackage
- Sitecore.Shell.Applications.Install.Dialogs.InstallPackage.InstallPackageForm
- Sitecore.Shell.Applications.Install.Dialogs.UploadPackageForm
- Sitecore.Shell.Applications.Layouts.IDE.Wizards.NewFileWizard.IDENewFileWizardForm
- Sitecore.Shell.Applications.Layouts.IDE.Wizards.NewMethodRenderingWizard.IDENewMethodRenderingWizardForm
- Sitecore.Shell.Applications.Layouts.IDE.Wizards.NewUrlRenderingWizard.IDENewUrlRenderingWizardForm
- Sitecore.Shell.Applications.Layouts.IDE.Wizards.NewWebControlWizard.IDENewWebControlWizardForm
- Sitecore.Shell.Applications.Layouts.Layouter.Wizards.NewLayout.NewLayoutForm
- Sitecore.Shell.Applications.Layouts.Layouter.Wizards.NewSublayout.NewSublayoutForm
- Sitecore.Shell.Applications.Layouts.Layouter.Wizards.NewXMLLayout.NewXMLLayoutForm
- Sitecore.Shell.Applications.Layouts.Layouter.Wizards.NewXSL.NewXSLForm
- Sitecore.Shell.Applications.MarketingAutomation.Dialogs.ForceTriggerForm
- Sitecore.Shell.Applications.MarketingAutomation.Dialogs.ImportVisitorsForm
- Sitecore.Shell.Applications.ScheduledTasks.NewSchedule.NewScheduleForm
- Sitecore.Shell.Applications.ScheduledTasks.NewScheduleCommand.NewScheduleCommandForm
- Sitecore.Shell.Applications.Search.RebuildSearchIndex.RebuildSearchIndexForm
- Sitecore.Shell.Applications.Templates.ChangeTemplate.ChangeTemplateForm
- Sitecore.Shell.Applications.Templates.CreateTemplate.CreateTemplateForm
If you can think of any other ways of customizing this client pipeline, please drop 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:
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:
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:
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.
Swap Out Sitecore Layouts and Sublayouts Dynamically Based on a Theme
Out of the box, cloned items in Sitecore will retain the presentation components of their source items, and teasing these out could potentially lead to a mess.
I recently worked on a project where I had to architect a solution to avoid such a mess — this solution would ensure that items cloned across different websites in the same Sitecore instance could have a different look and feel from their source items.
Though I can’t show you what I did specifically on that project, I did go about creating a different solution just for you — yes, you — and this post showcases the fruit of that endeavor.
This is the basic idea of the solution:
For every sublayout (or the layout) set on an item, try to find a variant of that sublayout (or the layout) in a subfolder with the name of the selected theme. Otherwise, use the original.
For example, if /layouts/sublayouts/MySublayout.ascx is mapped to a sublayout set on the item and our selected theme is named “My Cool Theme”, try to find and use /layouts/sublayouts/My Cool Theme/MySublayout.ascx on the file system. If it does not exist, just use /layouts/sublayouts/MySublayout.ascx.
To start, I created some “theme” items. These items represent folders on my file system, and these folders house a specific layout and sublayouts pertinent to the theme. I set the parent folder of these items to be the source of the Theme droplist on my home page item:
In this solution, I defined objects as detectors — objects that ascertain particular conditions, depending on encapsulated logic within the detector class for the given source. This is the contract for all detectors in my solution:
namespace Sitecore.Sandbox.Utilities.Detectors.Base
{
public interface IDetector<T>
{
T Source { get; set; }
bool Detect();
}
}
In this solution, I am leveraging some string detectors:
namespace Sitecore.Sandbox.Utilities.Detectors.Base
{
public interface IStringDetector : IDetector<string>
{
}
}
The first string detector I created was one to ascertain whether a file exists on the file system:
using System.IO;
using Sitecore.Diagnostics;
using Sitecore.Sandbox.Utilities.Detectors.Base;
namespace Sitecore.Sandbox.Utilities.Detectors
{
public class FileExistsDetector : IStringDetector
{
private string _Source;
public string Source
{
get
{
return _Source;
}
set
{
Assert.ArgumentNotNull(value, "Source");
_Source = value;
}
}
private FileExistsDetector()
{
}
private FileExistsDetector(string source)
{
SetSource(source);
}
private void SetSource(string source)
{
Source = source;
}
public bool Detect()
{
Assert.ArgumentNotNull(Source, "Source");
return File.Exists(Source);
}
public static IStringDetector CreateNewFileExistsDetector()
{
return new FileExistsDetector();
}
public static IStringDetector CreateNewFileExistsDetector(string source)
{
return new FileExistsDetector(source);
}
}
}
The detector above is not web server aware — paths containing forward slashes will not be found. This led to the creation of the following decorator to convert server to file system paths:
using System.IO;
using System.Web;
using Sitecore.Diagnostics;
using Sitecore.Sandbox.Utilities.Detectors.Base;
namespace Sitecore.Sandbox.Utilities.Detectors
{
public class ServerFileExistsDetector : IStringDetector
{
private HttpServerUtility HttpServerUtility { get; set; }
private IStringDetector Detector { get; set; }
public string Source
{
get
{
return Detector.Source;
}
set
{
Assert.ArgumentNotNull(value, "Source");
Detector.Source = HttpServerUtility.MapPath(value);
}
}
private ServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector)
{
SetHttpServerUtility(httpServerUtility);
SetDetector(detector);
}
private ServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source)
{
SetHttpServerUtility(httpServerUtility);
SetDetector(detector);
SetSource(source);
}
private void SetHttpServerUtility(HttpServerUtility httpServerUtility)
{
Assert.ArgumentNotNull(httpServerUtility, "httpServerUtility");
HttpServerUtility = httpServerUtility;
}
private void SetDetector(IStringDetector detector)
{
Assert.ArgumentNotNull(detector, "detector");
Detector = detector;
}
private void SetSource(string source)
{
Source = source;
}
public bool Detect()
{
Assert.ArgumentNotNull(Source, "Source");
return Detector.Detect();
}
public static IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector)
{
return new ServerFileExistsDetector(httpServerUtility, detector);
}
public static IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source)
{
return new ServerFileExistsDetector(httpServerUtility, detector, source);
}
}
}
I thought it would be best to control the instantiation of the string detectors above into a factory class:
using System.Web;
namespace Sitecore.Sandbox.Utilities.Detectors.Base
{
public interface IStringDetectorFactory
{
IStringDetector CreateNewFileExistsDetector();
IStringDetector CreateNewFileExistsDetector(string source);
IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility);
IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector);
IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source);
}
}
using System.Web;
using Sitecore.Sandbox.Utilities.Detectors.Base;
namespace Sitecore.Sandbox.Utilities.Detectors
{
public class StringDetectorFactory : IStringDetectorFactory
{
private StringDetectorFactory()
{
}
public IStringDetector CreateNewFileExistsDetector()
{
return FileExistsDetector.CreateNewFileExistsDetector();
}
public IStringDetector CreateNewFileExistsDetector(string source)
{
return FileExistsDetector.CreateNewFileExistsDetector(source);
}
public IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility)
{
return ServerFileExistsDetector.CreateNewServerFileExistsDetector(httpServerUtility, CreateNewFileExistsDetector());
}
public IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector)
{
return ServerFileExistsDetector.CreateNewServerFileExistsDetector(httpServerUtility, detector);
}
public IStringDetector CreateNewServerFileExistsDetector(HttpServerUtility httpServerUtility, IStringDetector detector, string source)
{
return ServerFileExistsDetector.CreateNewServerFileExistsDetector(httpServerUtility, detector, source);
}
public static IStringDetectorFactory CreateNewStringDetectorFactory()
{
return new StringDetectorFactory();
}
}
}
I then needed a way to ascertain if an item is a sublayout — we shouldn’t be messing with presentation components that are not sublayouts (not including layouts which is handled in a different place):
using Sitecore.Data.Items;
namespace Sitecore.Sandbox.Utilities.Detectors.Base
{
public interface IItemDetector : IDetector<Item>
{
}
}
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Sandbox.Utilities.Detectors.Base;
namespace Sitecore.Sandbox.Utilities.Detectors
{
public class SublayoutDetector : IItemDetector
{
private Item _Source;
public Item Source
{
get
{
return _Source;
}
set
{
Assert.ArgumentNotNull(value, "Source");
_Source = value;
}
}
private SublayoutDetector()
{
}
private SublayoutDetector(Item source)
{
SetSource(source);
}
private void SetSource(Item source)
{
Source = source;
}
public bool Detect()
{
Assert.ArgumentNotNull(Source, "Source");
return Source.TemplateID == TemplateIDs.Sublayout;
}
public static IItemDetector CreateNewSublayoutDetector()
{
return new SublayoutDetector();
}
public static IItemDetector CreateNewSublayoutDetector(Item source)
{
return new SublayoutDetector(source);
}
}
}
I then reused the concept of manipulators — objects that transform an instance of an object into a different instance — from a prior blog post. I forget the post I used manipulators in, so I have decided to re-post the base contract for all manipulator classes:
namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
public interface IManipulator<T>
{
T Manipulate(T source);
}
}
Yes, we will be manipulating strings. 🙂
namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
public interface IStringManipulator : IManipulator<string>
{
}
}
I also defined another interface containing an accessor and mutator for a string representing the selected theme:
namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
public interface IThemeFolder
{
string ThemeFolder { get; set; }
}
}
I created a class that will wedge in a theme folder name into a file path — it is inserted after the last forward slash in the file path — and returns the resulting string:
namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
public interface IThemeFilePathManipulator : IStringManipulator, IThemeFolder
{
}
}
using Sitecore.Diagnostics;
using Sitecore.Sandbox.Utilities.Manipulators.Base;
namespace Sitecore.Sandbox.Utilities.Manipulators
{
public class ThemeFilePathManipulator : IThemeFilePathManipulator
{
private const string Slash = "/";
private const int NotFoundIndex = -1;
private string _ThemeFolder;
public string ThemeFolder
{
get
{
return _ThemeFolder;
}
set
{
_ThemeFolder = value;
}
}
private ThemeFilePathManipulator()
{
}
private ThemeFilePathManipulator(string themeFolder)
{
SetThemeFolder(themeFolder);
}
public string Manipulate(string source)
{
Assert.ArgumentNotNullOrEmpty(source, "source");
int lastIndex = source.LastIndexOf(Slash);
bool canManipulate = !string.IsNullOrEmpty(ThemeFolder) && lastIndex > NotFoundIndex;
if (canManipulate)
{
return source.Insert(lastIndex + 1, string.Concat(ThemeFolder, Slash));
}
return source;
}
private void SetThemeFolder(string themeFolder)
{
ThemeFolder = themeFolder;
}
public static IThemeFilePathManipulator CreateNewThemeFilePathManipulator()
{
return new ThemeFilePathManipulator();
}
public static IThemeFilePathManipulator CreateNewThemeFilePathManipulator(string themeFolder)
{
return new ThemeFilePathManipulator(themeFolder);
}
}
}
But shouldn’t we see if the file exists, and then return the path if it does, or the original path if it does not? This question lead to the creation of the following decorator class to do just that:
using Sitecore.Diagnostics;
using Sitecore.Sandbox.Utilities.Detectors.Base;
using Sitecore.Sandbox.Utilities.Manipulators.Base;
namespace Sitecore.Sandbox.Utilities.Manipulators
{
public class ThemeFilePathIfExistsManipulator : IThemeFilePathManipulator
{
private IThemeFilePathManipulator InnerManipulator { get; set; }
private IStringDetector FileExistsDetector { get; set; }
private string _ThemeFolder;
public string ThemeFolder
{
get
{
return InnerManipulator.ThemeFolder;
}
set
{
InnerManipulator.ThemeFolder = value;
}
}
private ThemeFilePathIfExistsManipulator(IThemeFilePathManipulator innerManipulator, IStringDetector fileExistsDetector)
{
SetInnerManipulator(innerManipulator);
SetFileExistsDetector(fileExistsDetector);
}
private void SetInnerManipulator(IThemeFilePathManipulator innerManipulator)
{
Assert.ArgumentNotNull(innerManipulator, "innerManipulator");
InnerManipulator = innerManipulator;
}
private void SetFileExistsDetector(IStringDetector fileExistsDetector)
{
Assert.ArgumentNotNull(fileExistsDetector, "fileExistsDetector");
FileExistsDetector = fileExistsDetector;
}
public string Manipulate(string source)
{
Assert.ArgumentNotNullOrEmpty(source, "source");
string filePath = InnerManipulator.Manipulate(source);
if(!DoesFileExist(filePath))
{
return source;
}
return filePath;
}
private bool DoesFileExist(string filePath)
{
FileExistsDetector.Source = filePath;
return FileExistsDetector.Detect();
}
private void SetThemeFolder(string themeFolder)
{
ThemeFolder = themeFolder;
}
public static IThemeFilePathManipulator CreateNewThemeFilePathManipulator(IThemeFilePathManipulator innerManipulator, IStringDetector fileExistsDetector)
{
return new ThemeFilePathIfExistsManipulator(innerManipulator, fileExistsDetector);
}
}
}
Next, I defined a contract for manipulators that transform instances of Sitecore.Layouts.RenderingReference:
using Sitecore.Layouts;
namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
public interface IRenderingReferenceManipulator : IManipulator<RenderingReference>
{
}
}
I then created a class to manipulate instances of Sitecore.Layouts.RenderingReference by swapping out their sublayouts with new instances based on the path returned by the IThemeFilePathManipulator instance:
namespace Sitecore.Sandbox.Utilities.Manipulators.Base
{
public interface IThemeRenderingReferenceManipulator : IRenderingReferenceManipulator, IThemeFolder
{
}
}
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Layouts;
using Sitecore.Web.UI.WebControls;
using Sitecore.Sandbox.Utilities.Detectors.Base;
using Sitecore.Sandbox.Utilities.Manipulators.Base;
namespace Sitecore.Sandbox.Utilities.Manipulators
{
public class ThemeRenderingReferenceManipulator : IThemeRenderingReferenceManipulator
{
private IItemDetector SublayoutDetector { get; set; }
private IThemeFilePathManipulator ThemeFilePathManipulator { get; set; }
public string ThemeFolder
{
get
{
return ThemeFilePathManipulator.ThemeFolder;
}
set
{
ThemeFilePathManipulator.ThemeFolder = value;
}
}
private ThemeRenderingReferenceManipulator()
: this(CreateDefaultThemeFilePathManipulator())
{
}
private ThemeRenderingReferenceManipulator(IThemeFilePathManipulator themeFilePathManipulator)
: this(CreateDefaultSublayoutDetector(), themeFilePathManipulator)
{
}
private ThemeRenderingReferenceManipulator(IItemDetector sublayoutDetector, IThemeFilePathManipulator themeFilePathManipulator)
{
SetSublayoutDetector(sublayoutDetector);
SetThemeFilePathManipulator(themeFilePathManipulator);
}
private void SetSublayoutDetector(IItemDetector sublayoutDetector)
{
Assert.ArgumentNotNull(sublayoutDetector, "sublayoutDetector");
SublayoutDetector = sublayoutDetector;
}
private void SetThemeFilePathManipulator(IThemeFilePathManipulator themeFilePathManipulator)
{
Assert.ArgumentNotNull(themeFilePathManipulator, "themeFilePathManipulator");
ThemeFilePathManipulator = themeFilePathManipulator;
}
public RenderingReference Manipulate(RenderingReference source)
{
Assert.ArgumentNotNull(source, "source");
Assert.ArgumentNotNull(source.RenderingItem, "source.RenderingItem");
if (!IsSublayout(source.RenderingItem.InnerItem))
{
return source;
}
RenderingReference renderingReference = source;
Sublayout sublayout = source.GetControl() as Sublayout;
if (sublayout == null)
{
return source;
}
renderingReference.SetControl(CreateNewSublayout(sublayout, ThemeFilePathManipulator.Manipulate(sublayout.Path)));
return renderingReference;
}
private bool IsSublayout(Item item)
{
Assert.ArgumentNotNull(item, "item");
SublayoutDetector.Source = item;
return SublayoutDetector.Detect();
}
private static Sublayout CreateNewSublayout(Sublayout sublayout, string path)
{
Assert.ArgumentNotNull(sublayout, "sublayout");
Assert.ArgumentNotNullOrEmpty(path, "path");
return new Sublayout
{
ID = sublayout.ID,
Path = path,
Background = sublayout.Background,
Border = sublayout.Border,
Bordered = sublayout.Bordered,
Cacheable = sublayout.Cacheable,
CacheKey = sublayout.CacheKey,
CacheTimeout = sublayout.CacheTimeout,
Clear = sublayout.Clear,
CssStyle = sublayout.CssStyle,
Cursor = sublayout.Cursor,
Database = sublayout.Database,
DataSource = sublayout.DataSource,
DisableDebug = sublayout.DisableDebug,
DisableDots = sublayout.DisableDots,
DisableSecurity = sublayout.DisableSecurity,
Float = sublayout.Float,
Foreground = sublayout.Foreground,
LiveDisplay = sublayout.LiveDisplay,
LoginRendering = sublayout.LoginRendering,
Margin = sublayout.Margin,
Padding = sublayout.Padding,
Parameters = sublayout.Parameters,
RenderAs = sublayout.RenderAs,
RenderingID = sublayout.RenderingID,
RenderingName = sublayout.RenderingName,
VaryByData = sublayout.VaryByData,
VaryByDevice = sublayout.VaryByDevice,
VaryByLogin = sublayout.VaryByLogin,
VaryByParm = sublayout.VaryByParm,
VaryByQueryString = sublayout.VaryByQueryString,
VaryByUser = sublayout.VaryByUser
};
}
private static IThemeFilePathManipulator CreateDefaultThemeFilePathManipulator()
{
return Manipulators.ThemeFilePathManipulator.CreateNewThemeFilePathManipulator();
}
private static IItemDetector CreateDefaultSublayoutDetector()
{
return Utilities.Detectors.SublayoutDetector.CreateNewSublayoutDetector();
}
public static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator()
{
return new ThemeRenderingReferenceManipulator();
}
public static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator(IThemeFilePathManipulator themeFilePathManipulator)
{
return new ThemeRenderingReferenceManipulator(themeFilePathManipulator);
}
public static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator(IItemDetector sublayoutDetector, IThemeFilePathManipulator themeFilePathManipulator)
{
return new ThemeRenderingReferenceManipulator(sublayoutDetector, themeFilePathManipulator);
}
}
}
I created a new httpRequestBegin pipeline processor to use a theme’s layout if found on the file system — such is done by delegating to instances of classes defined above:
using System;
using System.Web;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.HttpRequest;
using Sitecore.Sandbox.Utilities.Manipulators;
using Sitecore.Sandbox.Utilities.Manipulators.Base;
using Sitecore.Sandbox.Utilities.Detectors;
using Sitecore.Sandbox.Utilities.Detectors.Base;
namespace Sitecore.Sandbox.Pipelines.HttpRequest
{
public class ThemeLayoutResolver : HttpRequestProcessor
{
public override void Process(HttpRequestArgs args)
{
if (!CanProcess())
{
return;
}
string themeFilePath = GetThemeFilePath();
bool swapOutFilePath = !AreEqualIgnoreCase(Context.Page.FilePath, themeFilePath);
if (swapOutFilePath)
{
Context.Page.FilePath = themeFilePath;
}
}
private static bool CanProcess()
{
return Context.Database != null
&& !IsCore(Context.Database);
}
private static bool IsCore(Database database)
{
return database.Name == Constants.CoreDatabaseName;
}
private static string GetThemeFilePath()
{
IThemeFilePathManipulator themeFilePathManipulator = CreateNewThemeFilePathManipulator();
themeFilePathManipulator.ThemeFolder = GetTheme();
return themeFilePathManipulator.Manipulate(Context.Page.FilePath);
}
private static bool DoesFileExist(string filePath)
{
Assert.ArgumentNotNullOrEmpty(filePath, "filePath");
IStringDetector fileExistsDetector = CreateNewServerFileExistsDetector();
fileExistsDetector.Source = filePath;
return fileExistsDetector.Detect();
}
private static IThemeFilePathManipulator CreateNewThemeFilePathManipulator()
{
return ThemeFilePathIfExistsManipulator.CreateNewThemeFilePathManipulator
(
ThemeFilePathManipulator.CreateNewThemeFilePathManipulator(),
CreateNewServerFileExistsDetector()
);
}
private static IStringDetector CreateNewServerFileExistsDetector()
{
IStringDetectorFactory factory = StringDetectorFactory.CreateNewStringDetectorFactory();
return factory.CreateNewServerFileExistsDetector(HttpContext.Current.Server);
}
private static string GetTheme()
{
Item home = GetHomeItem();
return home["Theme"];
}
private static Item GetHomeItem()
{
string startPath = Factory.GetSite(Sitecore.Context.GetSiteName()).StartPath;
return Context.Database.GetItem(startPath);
}
private static bool AreEqualIgnoreCase(string stringOne, string stringTwo)
{
return string.Equals(stringOne, stringTwo, StringComparison.CurrentCultureIgnoreCase);
}
}
}
The above code should not run if the context database is the core database — this could mean we are in the Sitecore shell, or on the Sitecore login page (I painfully discovered this earlier today after doing a QA drop, and had to act fast to fix it).
I then created a RenderLayout pipeline processor to swap out sublayouts for themed sublayouts if they exist on the file system:
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Layouts;
using Sitecore.Pipelines;
using Sitecore.Pipelines.InsertRenderings;
using Sitecore.Pipelines.RenderLayout;
using Sitecore.Sandbox.Utilities.Manipulators;
using Sitecore.Sandbox.Utilities.Manipulators.Base;
using Sitecore.Sandbox.Utilities.Detectors;
using Sitecore.Sandbox.Utilities.Detectors.Base;
namespace Sitecore.Sandbox.Pipelines.RenderLayout
{
public class InsertThemeRenderings : RenderLayoutProcessor
{
public override void Process(RenderLayoutArgs args)
{
Assert.ArgumentNotNull(args, "args");
if (Context.Item == null)
{
return;
}
using (new ProfileSection("Insert renderings into page."))
{
IEnumerable<RenderingReference> themeRenderingReferences = GetThemeRenderingReferences(GetRenderingReferences());
foreach (RenderingReference renderingReference in themeRenderingReferences)
{
Context.Page.AddRendering(renderingReference);
}
}
}
private static IEnumerable<RenderingReference> GetRenderingReferences()
{
InsertRenderingsArgs insertRenderingsArgs = new InsertRenderingsArgs();
CorePipeline.Run("insertRenderings", insertRenderingsArgs);
return insertRenderingsArgs.Renderings.ToList();
}
private static IEnumerable<RenderingReference> GetThemeRenderingReferences(IEnumerable<RenderingReference> renderingReferences)
{
Assert.ArgumentNotNull(renderingReferences, "renderingReferences");
IList<RenderingReference> themeRenderingReferences = new List<RenderingReference>();
IThemeRenderingReferenceManipulator themeRenderingReferenceManipulator = CreateNewRenderingReferenceManipulator();
foreach (RenderingReference renderingReference in renderingReferences)
{
themeRenderingReferences.Add(themeRenderingReferenceManipulator.Manipulate(renderingReference));
}
return themeRenderingReferences;
}
private static IThemeRenderingReferenceManipulator CreateNewRenderingReferenceManipulator()
{
IThemeRenderingReferenceManipulator themeRenderingReferenceManipulator = ThemeRenderingReferenceManipulator.CreateNewRenderingReferenceManipulator(CreateNewThemeFilePathManipulator());
themeRenderingReferenceManipulator.ThemeFolder = GetTheme();
return themeRenderingReferenceManipulator;
}
private static IThemeFilePathManipulator CreateNewThemeFilePathManipulator()
{
return ThemeFilePathIfExistsManipulator.CreateNewThemeFilePathManipulator
(
ThemeFilePathManipulator.CreateNewThemeFilePathManipulator(),
CreateNewServerFileExistsDetector()
);
}
private static IStringDetector CreateNewServerFileExistsDetector()
{
IStringDetectorFactory factory = StringDetectorFactory.CreateNewStringDetectorFactory();
return factory.CreateNewServerFileExistsDetector(HttpContext.Current.Server);
}
private static string GetTheme()
{
Item home = GetHomeItem();
return home["Theme"];
}
private static Item GetHomeItem()
{
string startPath = Factory.GetSite(Sitecore.Context.GetSiteName()).StartPath;
return Context.Database.GetItem(startPath);
}
}
}
I baked everything together in a patch include configuration file:
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<renderLayout>
<processor patch:instead="processor[@type='Sitecore.Pipelines.RenderLayout.InsertRenderings, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.RenderLayout.InsertThemeRenderings, Sitecore.Sandbox" />
</renderLayout>
<httpRequestBegin>
<processor patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.HttpRequest.ThemeLayoutResolver, Sitecore.Sandbox"/>
</httpRequestBegin>
</pipelines>
</sitecore>
</configuration>
Here are the “out of the box” presentation components mapped to my test page:
The above layout and sublayouts live in /layouts from my website root.
For my themes, I duplicated the layout and some of the sublayouts above into themed folders, and added some css for color — I’ve omitted this for brevity (come on Mike, this post is already wicked long :)).
The “Blue Theme” layout and sublayout live in /layouts/Blue Theme from my website root:
I chose “Blue Theme” in the Theme droplist on my home item, published, and then navigated to my test page:
The “Inverse Blue Theme” sublayout lives in /layouts/Inverse Blue Theme from my website root:
I chose “Inverse Blue Theme” in the Theme droplist on my home item, published, and then reloaded my test page:
The “Green Theme” layout and sublayouts live in /layouts/Green Theme from my website root:
I chose “Green Theme” in the Theme droplist on my home item, published, and then refreshed my test page:
Do you know of or have an idea for another solution to accomplish the same? If so, please drop 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:
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 > * {
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:
Time for some fun.
I created a new item for testing:
I published this item, and made some changes to it:
I clicked the Database Compare button to launch our dialog form:
As expected, we see differences in this item across the master and web databases:
Here are those differences in the two column layout:
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.




















































