Home » Commands » Make a Difference by Comparing Sitecore Items Across Different Databases

Make a Difference by Comparing Sitecore Items Across Different Databases

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

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:

compare-version

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 &gt; * {
          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:

database-compare-chunk-button-new

Time for some fun.

I created a new item for testing:

database-compare-new-item

I published this item, and made some changes to it:

database-compare-changed-item-in-master

I clicked the Database Compare button to launch our dialog form:

database-compare-launched-dialog

As expected, we see differences in this item across the master and web databases:

database-compare-one-column-comparison

Here are those differences in the two column layout:

database-compare-two-column-comparison

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.

Advertisement

13 Comments

  1. Jochan says:

    You have the best Sitecore deepdive technical blog ever !!!
    Keep the good work!

  2. dschoenberghzi says:

    This is some great stuff. Thanks for sharing it.

  3. Mike Shaw says:

    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.

  4. 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

      • Pradeep says:

        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

  5. Pradeep says:

    Really a good article

  6. Cecilia says:

    Hi Mike!

    Nice post 🙂

    Does the tool compare other language versions than “en”?

    /Cecilia

Comment

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 )

Facebook photo

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

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: