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.
You have the best Sitecore deepdive technical blog ever !!!
Keep the good work!
Thanks!
Hi Mike ,
I am trying to do so from an external application using sitecore web api,but unable to find and install the package.Can u pls help me out?
The Sitecore Item Web API comes out of the box with Sitecore.
This is some great stuff. Thanks for sharing it.
Great blog Mike! Hedgehog has a tool, we previewed at the US symposium that does just this. It should be releasing any day now so keep an eye out for it.
It has been Relesed now .. So called RAZl
http://razl.net
Hi Mike,
Great Post Mike. Velir built a very similar tool a little while back, check out the Published Item Comparer in the Sitecore Marketplace. We have it out on Github, maybe there is an opportunity to collaborate on future versions?
Hi Corey,
That would be great!
Mike
Hi Mike
My question this does the comparison for one item; can we do some thing in order to compare all the subitems..?
I don’t believe you could leverage any preexisting classes in Sitecore.Kernel.dll or Sitecore.Client.dll to easily do this — if anyone knows of a way to do this using existing classes in one of the Sitecore assemblies, please follow-up with a comment — although you most certainly could write your own classes to accomplish this.
I suspect the level of effort would be high though.
Mike
Really a good article
Hi Mike!
Nice post 🙂
Does the tool compare other language versions than “en”?
/Cecilia