Add workflow notifications for all editable items on a Sitecore page

While editing a page in experience editor mode, when we edit the fields which are a part of the context item, if the item goes into the edit stage in the workflow, we see a notification on saving the item.
This notification tells us that the item needs to go through workflow to be published.

2016-11-09_212657

Now, if a module on the page gets data from a rendering datasource item, and we edit that content, there is no notification on the page on item save, even though the datasource item would infact need to go through the workflow to be able to be published.
We used this excellent module form Sitecore marketplace to resolve this issue: Datasource Workflow Module

Now we get additional notifications when we edit rendering datasource items.

2016-11-09_214522

However this doesn’t address reference items that the datasource item might refer to.

So what we did to resolve this issue, is marry the code of this module with code to collect all items which are editable on the current context page (Thanks to this post!). So while we render the page, we collect all editable items on the page and then run a check on this list, to find all items which are not in the final stage of workflow, and show notifications in the notification area for all these items.

Following are the config updates you will need:

<configuration>
  <sitecore>
      <pipelines>
        <renderField>
          <processor type="MySite.Sitecore.Pipelines.RenderField.CollectItemReferences, MySite.Sitecore"></processor>
        </renderField>
        <getPageEditorNotifications>
          <processor type="MySite.Sitecore.Pipelines.GetPageEditorNotifications.GetReferenceWorkflowNotification, MySite.Sitecore" />
        </getPageEditorNotifications>
        <getContentEditorWarnings>
          <processor type="MySite.Sitecore.Pipelines.GetContentEditorWarnings.GetReferenceWorkflowNotification, MySite.Sitecore" />
        </getContentEditorWarnings>
        <getChromeData>
          <processor type="MySite.Sitecore.Pipelines.GetChromeData.EditFrameCollectItemReferences, MySite.Sitecore"></processor>
        </getChromeData>
      </pipelines>
      <setting name="ReferenceWorkflowNotification.ShowWorkflowNoAccessMessage" value="true" />
      <setting name="ReferenceWorkflowNotification.UseHtmlInMessaging" value="true" />
    </settings>
    <referenceWorkflowNotificationItems>
      <webEdit>
        <itemReferences type="MySite.Sitecore.Models.ItemReferenceCollection, MySite.Sitecore" singleInstance="true"></itemReferences>
      </webEdit>
    </referenceWorkflowNotificationItems>
  </sitecore>
</configuration>

Following is the code in use:

using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Layouts;
using Sitecore.Rules.ConditionalRenderings;
using Sitecore.SecurityModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;
using MySite.Sitecore.Models;
using Sc = Sitecore;

namespace MySite.Sitecore.Extensions
{
    public static class ItemExtensions
    {	
        private const string ContentEditorUrlFormat = "{0}://{1}/sitecore/shell/Applications/Content Editor?id={2}&amp;sc_content=master&amp;fo={2}&amp;vs={3}&amp;la={4}";

        public static RenderingReference[] GetRenderingReferences(this Item i)
        {
            return i == null ? new RenderingReference[0] : i.Visualization.GetRenderings(Sc.Context.Device, false);
        }

        public static List<Item> GetAllUniqueDataSourceItems(this Item i, bool includePersonalizationDataSourceItems = true, bool includeMultiVariateTestDataSourceItems = true)
        {
            var list = new List<Item>();
            foreach (RenderingReference reference in i.GetRenderingReferences())
            {
                list.AddUnqiueDataSourceItem(reference.GetDataSourceItem());

                if (includePersonalizationDataSourceItems)
                {
                    foreach (var dataSourceItem in reference.GetPersonalizationDataSourceItem())
                    {
                        list.AddUnqiueDataSourceItem(dataSourceItem);
                    }
                }

                if (includeMultiVariateTestDataSourceItems)
                {
                    foreach (Item dataSourceItem in reference.GetMultiVariateTestDataSourceItems())
                    {
                        list.AddUnqiueDataSourceItem(dataSourceItem);
                    }
                }
            }

            list.AddRange(AddDatasourceReferenceItems(i));
            list = list.DistinctBy(l => l.ID).ToList();

            return list;
        }

        private static List<Item> AddDatasourceReferenceItems(Item context)
        {
            return ItemReferenceCollection.FromConfiguration().GetItemReferences(context.ID.Guid)
                .Select(x => context.Database.GetItem(new ID(x))).ToList();
        }

        public static List<Item> GetDataSourceItems(this Item i)
        {
            List<Item> list = new List<Item>();
            foreach (RenderingReference reference in i.GetRenderingReferences())
            {
                Item dataSourceItem = reference.GetDataSourceItem();
                if (dataSourceItem != null)
                {
                    list.Add(dataSourceItem);
                }
            }
            return list;
        }

        public static Item GetDataSourceItem(this RenderingReference reference)
        {
            if (reference != null)
            {
                return GetDataSourceItem(reference.Settings.DataSource, reference.Database);
            }
            return null;
        }

        public static List<Item> GetPersonalizationDataSourceItems(this Item i)
        {
            var list = new List<Item>();
            foreach (var reference in i.GetRenderingReferences())
            {
                list.AddRange(reference.GetPersonalizationDataSourceItem());
            }
            return list;
        }

        private static IEnumerable<Item> GetPersonalizationDataSourceItem(this RenderingReference reference)
        {
            var list = new List<Item>();
            if (reference != null && reference.Settings.Rules != null && reference.Settings.Rules.Count > 0)
            {
                list.AddRange(reference.Settings.Rules.Rules.SelectMany(r => r.Actions).OfType<SetDataSourceAction<ConditionalRenderingsRuleContext>>().Select(setDataSourceAction => GetDataSourceItem(setDataSourceAction.DataSource, reference.Database)).Where(dataSourceItem => dataSourceItem != null));
            }
            return list;
        }

        public static List<Item> GetMultiVariateTestDataSourceItems(this Item i)
        {
            var list = new List<Item>();
            foreach (var reference in i.GetRenderingReferences())
            {
                list.AddRange(reference.GetMultiVariateTestDataSourceItems());
            }
            return list;
        }

        private static IEnumerable<Item> GetMultiVariateTestDataSourceItems(this RenderingReference reference)
        {
            var list = new List<Item>();
            if (reference != null && !string.IsNullOrEmpty(reference.Settings.MultiVariateTest))
            {
                using (new SecurityDisabler())
                {
                    // Sitecore 7 to Sitecore 8:
                    var mvVariateTestForLang = Sc.Analytics.Testing.TestingUtils.TestingUtil.MultiVariateTesting.GetVariableItem(reference);
                    
                    // Sitecore 8.1 and above:
                    //var contentTestStore = new Sitecore.ContentTesting.Data.SitecoreContentTestStore();
                    //var mvVariateTestForLang =  contentTestStore.GetMultivariateTestVariable(reference, reference.Language);
                    
                    var variableItem = (mvVariateTestForLang != null) ? mvVariateTestForLang.InnerItem : null;
                    if (variableItem == null) return list;
                    foreach (Item mvChild in variableItem.Children)
                    {
                        var mvDataSourceItem = mvChild.GetInternalLinkFieldItem("Datasource");
                        if (mvDataSourceItem != null)
                        {
                            list.Add(mvDataSourceItem);
                        }
                    }
                }
            }
            return list;
        }

        private static bool ListContainsItem(IEnumerable<Item> list, Item dataSourceItem)
        {
            var uniqueId = dataSourceItem.GetUniqueId();
            return list.Any(e => e.GetUniqueId() == uniqueId);
        }

        private static void AddUnqiueDataSourceItem(this List<Item> list, Item dataSourceItem)
        {
            if (dataSourceItem != null && !ListContainsItem(list, dataSourceItem))
            {
                list.Add(dataSourceItem);
            }
        }

        private static Item GetDataSourceItem(string id, Database db)
        {
            Guid itemId;
            return Guid.TryParse(id, out itemId)
                                    ? db.GetItem(new ID(itemId))
                                    : db.GetItem(id);
        }

        public static string GetContentEditorUrl(this Item i)
        {
            var requestUri = HttpContext.Current.Request.Url;
            return string.Format(ContentEditorUrlFormat, requestUri.Scheme, requestUri.Host, WebUtility.HtmlEncode(i.ID.ToString()), i.Version.Number, i.Language.CultureInfo.Name);
        }

        public static Item GetInternalLinkFieldItem(this Item i, string internalLinkFieldName)
        {
            if (i == null) return null;
            InternalLinkField ilf = i.Fields[internalLinkFieldName];
            if (ilf != null && ilf.TargetItem != null)
            {
                return ilf.TargetItem;
            }
            return null;
        }

        public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
        {
            return items.GroupBy(property).Select(x => x.First());
        }
    }
}
using Sitecore.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using Sc = Sitecore;

namespace MySite.Sitecore.Models
{
    public class ItemReferenceCollection : IItemReferenceCollection
    {
        protected static object SyncRoot = new object();
        protected IDictionary<Guid, IDictionary<Guid, Guid>> ItemReferences = new Dictionary<Guid, IDictionary<Guid, Guid>>();

        ///
<summary>
        /// Adds the item reference.
        /// </summary>

        /// <param name="itemId">The item identifier.</param>
        public void AddItemReference(Guid itemId)
        {
            lock (SyncRoot)
            {
                var contextItem = Sc.Context.Item;

                if (contextItem == null)
                {
                    return;
                }

                if (ItemReferences.ContainsKey(contextItem.ID.Guid))
                {
                    if (!ItemReferences[contextItem.ID.Guid].ContainsKey(itemId))
                    {
                        ItemReferences[contextItem.ID.Guid].Add(itemId, itemId);
                    }
                }
                else
                {
                    ItemReferences.Add(contextItem.ID.Guid, new Dictionary<Guid, Guid> { { itemId, itemId } });
                }
            }
        }

        ///
<summary>
        /// Adds the item references.
        /// </summary>

        /// <param name="itemIds">The item ids.</param>
        public void AddItemReferences(IEnumerable<Guid> itemIds)
        {
            foreach (var itemId in itemIds)
            {
                AddItemReference(itemId);
            }
        }

        ///
<summary>
        /// Gets the item references.
        /// </summary>

        /// <param name="currentItemId">The current item identifier.</param>
        /// <returns></returns>
        public IEnumerable<Guid> GetItemReferences(Guid currentItemId)
        {
            lock (SyncRoot)
            {
                if (ItemReferences.ContainsKey(currentItemId))
                {
                    return ItemReferences[currentItemId].Values.ToArray();
                }
            }

            return new List<Guid>();
        }

        ///
<summary>
        /// Clears the specified item references.
        /// </summary>

        /// <param name="currentItemId">The current item identifier.</param>
        public void Clear(Guid currentItemId)
        {
            lock (SyncRoot)
            {
                if (ItemReferences.ContainsKey(currentItemId))
                {
                    ItemReferences.Remove(currentItemId);
                }
            }
        }

        ///
<summary>
        /// Gets the instance configured in Sitecore config using Sitecore.Configuration.Factory.
        /// </summary>

        /// <returns></returns>
        public static IItemReferenceCollection FromConfiguration()
        {
            return Factory.CreateObject("referenceWorkflowNotificationItems/webEdit/itemReferences", false) as IItemReferenceCollection;
        }
    }

    public interface IItemReferenceCollection
    {
        void AddItemReference(Guid itemId);

        void AddItemReferences(IEnumerable<Guid> itemIds);

        IEnumerable<Guid> GetItemReferences(Guid currentItemId);

        void Clear(Guid currentItemId);
    }
}
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Security.AccessControl;
using Sitecore.Workflows;
using Sc = Sitecore;

namespace MySite.Sitecore.Models
{
    public class ItemWorkflowModel
    { 
        #region Fields

        private readonly Item _contextItem;
        private readonly Database _database;
        private readonly IWorkflowProvider _workflowProvider;
        private readonly IWorkflow _workflow;
        private readonly WorkflowState _workflowState;
        WorkflowCommand[] _commands;
        private bool? _showWorkflowNoAccessMessage;
        private const string ShowWorkflowNoAccessMessageKey = "ReferenceWorkflowNotification.ShowWorkflowNoAccessMessage";

        #endregion

        #region Properties

        public Item ContextItem
        {
            get
            {
                return _contextItem;
            }
        }

        public Database Database
        {
            get
            {
                return _database;
            }
        }

        public IWorkflowProvider WorkflowProvider
        {
            get
            {
                return _workflowProvider;
            }
        }

        public IWorkflow Workflow
        {
            get
            {
                return _workflow;
            }
        }
        public WorkflowState WorkflowState
        {
            get
            {
                return _workflowState;
            }
        }

        public WorkflowCommand[] Commands
        {
            get
            {
                if (_commands == null)
                {
                    _commands = WorkflowFilterer.FilterVisibleCommands(Workflow.GetCommands(ContextItem));
                    if (_commands == null)
                    {
                        _commands = new WorkflowCommand[0];
                    }
                }
                return _commands;
            }
        }

        public bool ShowWorkflowNoAccessMessage
        {
            get
            {
                if (!_showWorkflowNoAccessMessage.HasValue)
                {
                    _showWorkflowNoAccessMessage = Sc.Configuration.Settings.GetBoolSetting(ShowWorkflowNoAccessMessageKey, false);
                }
                return _showWorkflowNoAccessMessage.Value;
            }
        }

        public bool ShowNotification
        {
            get
            {
                return NoNulls && !WorkflowState.FinalState && (Commands.Length > 0 && HasWriteAccess() || ShowWorkflowNoAccessMessage);
            }
        }

        public bool NoNulls
        {
            get
            {
                return (Database != null && WorkflowProvider != null && Workflow != null && WorkflowState != null);
            }
        }

        private bool UseHtmlMessaging
        {
            get
            {
                return Sc.Configuration.Settings.GetBoolSetting("ReferenceWorkflowNotification.UseHtmlInMessaging", false);
            }
        }

        #endregion

        #region Constructor

        public ItemWorkflowModel(Item i)
        {
            if (i != null)
            {
                _contextItem = i;
                _database = i.Database;
                if (_database != null)
                {
                    _workflowProvider = _database.WorkflowProvider;
                    if (_workflowProvider != null)
                    {
                        _workflow = _workflowProvider.GetWorkflow(ContextItem);
                        if (_workflow != null)
                        {
                            _workflowState = _workflow.GetState(ContextItem);
                        }
                    }
                }
            }
        }

        #endregion

        #region Methods

        public bool HasWriteAccess()
        {
            return AuthorizationManager.IsAllowed(ContextItem, AccessRight.ItemWrite, Sc.Context.User);
        }

        public string GetEditorDescription(bool isPageEditor = true)
        {
            string noAccessMessage = string.Empty;
            string workflowDisplayName = Workflow.Appearance.DisplayName;
            string workflowStateDisplayName = WorkflowState.DisplayName;
            string itemDisplayName = ContextItem.Paths.ContentPath;

            if (UseHtmlMessaging && !isPageEditor)
            {
                workflowDisplayName = BoldValue(workflowDisplayName);
                workflowStateDisplayName = BoldValue(workflowStateDisplayName);
                itemDisplayName = BoldValue(itemDisplayName);
            }
            else
            {
                workflowDisplayName = SingleQuoteValue(workflowDisplayName);
                workflowStateDisplayName = SingleQuoteValue(workflowStateDisplayName);
                itemDisplayName = SingleQuoteValue(itemDisplayName);
            }

            if (!HasWriteAccess())
            {
                noAccessMessage = " You cannot change the workflow because do not have write access to this item.";
            }

            return Translate.Text("The data source item {0} is in the {1} state of the {2} workflow.{3}", itemDisplayName, workflowStateDisplayName, workflowDisplayName, noAccessMessage);
        }

        private static string SingleQuoteValue(string value)
        {
            return string.Format("'{0}'", value);
        }

        private static string BoldValue(string value)
        {
            return string.Format("<b>{0}</b>", value);
        }

        #endregion
    }
}
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetChromeData;
using System;

namespace MySite.Sitecore.Pipelines.GetChromeData
{
    public class EditFrameCollectItemReferences : GetChromeDataProcessor
    {
        public override void Process(GetChromeDataArgs args)
        {
            try
            {
                if (CanCollectReferences(args))
                {
                    Models.ItemReferenceCollection.FromConfiguration().AddItemReference(args.Item.ID.Guid);
                }
            }
            catch (Exception ex)
            {
                Log.Error("Error in EditFrameCollectItemReferences pipeline processor.", ex, this);
            }
        }

        protected virtual bool CanCollectReferences(GetChromeDataArgs args)
        {
            return args != null && args.Item != null && !args.Aborted
                && args.ChromeType != null && args.ChromeData != null
                && "editFrame".Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase)
                && (Context.PageMode.IsPageEditor || Context.PageMode.IsPageEditorEditing);
        }
    }
}
using MySite.Sitecore.Extensions;
using MySite.Sitecore.Models;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetContentEditorWarnings;
using Sitecore.Shell.Framework.CommandBuilders;


namespace MySite.Sitecore.Pipelines.GetContentEditorWarnings
{
    public class GeReferenceWorkflowNotification
    {
        #region Methods

        public void Process(GetContentEditorWarningsArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            if (arguments.Item != null)
            {
                foreach (var ds in arguments.Item.GetAllUniqueDataSourceItems())
                {
                    GetNotifications(arguments, ds);
                }
            }
        }

        public void GetNotifications(GetContentEditorWarningsArgs arguments, Item contextItem)
        {
            if (arguments == null) return;
            var wfModel = new ItemWorkflowModel(contextItem);
            if (wfModel.ShowNotification)
            {
                SetNotification(arguments, wfModel);
            }
        }

        private void SetNotification(GetContentEditorWarningsArgs arguments, ItemWorkflowModel wfModel)
        {
            var editorNotification = arguments.Add();
            editorNotification.Title = "Datasource Item in Workflow";
            editorNotification.Text = wfModel.GetEditorDescription(false);
            editorNotification.Icon = wfModel.WorkflowState.Icon;
            if (wfModel.HasWriteAccess())
            {
                foreach (var command in wfModel.Commands)
                {
                    editorNotification.AddOption(command.DisplayName, new WorkflowCommandBuilder(wfModel.ContextItem, wfModel.Workflow, command).ToString());
                }
            }
        }

        #endregion
    }
}
using MySite.Sitecore.Extensions;
using MySite.Sitecore.Models;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetPageEditorNotifications;
using Sitecore.Shell.Framework.CommandBuilders;
using System.Linq;


namespace MySite.Sitecore.Pipelines.GetPageEditorNotifications
{
    public class GetReferenceWorkflowNotification : GetPageEditorNotificationsProcessor
    {
        #region Methods

        public override void Process(GetPageEditorNotificationsArgs arguments)
        {
            Assert.ArgumentNotNull(arguments, "arguments");
            if (arguments.ContextItem == null) return;
            foreach (var dataSourceArguments in arguments.ContextItem.GetAllUniqueDataSourceItems().Select(ds => new GetPageEditorNotificationsArgs(ds)))
            {
                GetNotifications(dataSourceArguments);
                arguments.Notifications.AddRange(dataSourceArguments.Notifications);
            }
        }

        public void GetNotifications(GetPageEditorNotificationsArgs arguments)
        {
            if (arguments == null) return;
            var wfModel = new ItemWorkflowModel(arguments.ContextItem);
            if (wfModel.ShowNotification)
            {
                SetNotification(arguments, wfModel);
            }
        }

        private void SetNotification(GetPageEditorNotificationsArgs arguments, ItemWorkflowModel wfModel)
        {
            var editorNotification = new PageEditorNotification(wfModel.GetEditorDescription(), PageEditorNotificationType.Warning)
            {
                Icon = wfModel.WorkflowState.Icon
            };
            if (wfModel.HasWriteAccess())
            {
                foreach (var notificationOption in wfModel.Commands
                    .Select(command => new PageEditorNotificationOption(command.DisplayName, new WorkflowCommandBuilder(wfModel.ContextItem, wfModel.Workflow, command).ToString())))
                {
                    editorNotification.Options.Add(notificationOption);
                }
            }
            arguments.Notifications.Add(editorNotification);
        }

        #endregion
    }
}
using MySite.Sitecore.Models;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;
using System;

namespace MySite.Sitecore.Pipelines.RenderField
{
    public class CollectItemReferences
    {
        public void Process(RenderFieldArgs args)
        {
            try
            {
                if (CanCollectReferences(args))
                {
                    ItemReferenceCollection.FromConfiguration().AddItemReference(args.Item.ID.Guid);
                }
            }
            catch(Exception ex)
            {
                Log.Error("Error in CollectItemReferences pipeline processor.", ex, this);
            }
        }

        protected virtual bool CanCollectReferences(RenderFieldArgs args)
        {
            return args != null && args.Item != null && !args.Aborted 
                && !args.DisableWebEdit && !args.DisableWebEditContentEditing
                && (Context.PageMode.IsPageEditor || Context.PageMode.IsPageEditorEditing);
        }
    }
}
using MySite.Sitecore.Extensions;
using MySite.Sitecore.Models;
using Sitecore.Data.Validators;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;

namespace MySite.Sitecore.Validators
{
    [Serializable]
    public class ComponentsinFinalWorkflow : StandardValidator
    {
        #region Properties

        public override string Name
        {
            get { return "Component Workflow State"; }
        }

        #endregion

        #region Constructor

        public ComponentsinFinalWorkflow(SerializationInfo info, StreamingContext context) : base(info, context)
        {
        }

        public ComponentsinFinalWorkflow()
        {
        }

        #endregion

        #region Methods

        protected override ValidatorResult Evaluate()
        {
            var item = GetItem();
            var result = ValidatorResult.Valid;
            var paths = new List<string>();
            foreach (var ds in item.GetAllUniqueDataSourceItems())
            {
                var wfModel = new ItemWorkflowModel(ds);
                if (wfModel.NoNulls && !wfModel.WorkflowState.FinalState)
                {
                    paths.Add(ds.Paths.ContentPath);
                    result = GetMaxValidatorResult();
                }
            }

            Text = string.Empty;
            if (paths.Count > 0)
            {
                Text += GetText("The item{0} {1} should be in a final workflow state.", paths.Count > 1 ? "s" : string.Empty, GetPathsString(paths));
            }

            return result;
        }

        private string GetPathsString(List<string> paths)
        {
            string formatString = "\"{0}\"{1}";
            if (paths.Count == 1)
            {
                return string.Format(formatString, paths[0], string.Empty);
            }
            else if (paths.Count == 2)
            {
                return string.Format(formatString, paths[0], " and ") + string.Format(formatString, paths[1], string.Empty);
            }

            string pathsString = string.Empty;
            string separator = ", ";
            for (int i = 0; i < paths.Count; i++)
            {
                if (i == paths.Count - 1)
                {
                    separator = string.Empty;
                }
                if (i == paths.Count - 2)
                {
                    separator = ", and ";
                }
                pathsString += string.Format(formatString, paths[i], separator);
            }
            return pathsString;
        }

        protected override ValidatorResult GetMaxValidatorResult()
        {
            return ValidatorResult.Warning;
        }

        #endregion
    }
}

Once you have these updates in place, you’ll see that there will be a notification for each editable item on the page which is not in the final stage of workflow in content editor and experience editor mode!

2016-11-09_215548

Advertisements

Editor to create image hotspots in Sitecore

We had the need to create a module which showed an image with hotspots on it – basically positional tooltips.

We created subitems of the module for each hotspot, and expected the content author to provide the position of the hotspot on the module image – in percentage – relative to the top / left of the image.
Now this posed an issue for the content authors since they had to either guess the position of each hotspot or upload the image onto a different online tool and then get the position from there to add into out hotspot sub items.

To remedy this we created a tool and integrated it with the Sitecore Content Editor – which would allow the content author to just click on the points on the image and create the corresponding hotspots.

To make this tool more versatile, we have made it configurable, so it can be used with any instance of Sitecore and any module, as long as it is configured in the hotspot specific configuration xml.

Here’s an example of the end result:

2016-03-22_191037

2016-03-22_190113

To achieve this, we needed to implement the following steps:

  1. Create an aspx page in your solution which Sitecore will load as a tab in the content editor.2016-03-22_210644
  2. Create an Editor item in Core DB, which will point to the aspx page you created.
  3. Set the Editor item on the standard values item of the template which would have the image on which the hotspots need to be set.
  4. Add the relevant code in the aspx page.

Create an aspx page in your solution

In our solution, we added the aspx page in the following location:

2016-03-22_220705

Create an Editor item in Core DB

2016-03-22_220900

Set the Editor item on the standard values item of the template

In this example, we have a POI module datasource template – which will have the image field for which we want to create the hotspots / points of interest. So we will set this editor in the standard values of this datasource template.

2016-03-22_221350.png

ASPX page code

We have the back end and front end code here. From a usability perspective, the current code only enables the content author to create new hotpot / point of interest items. Editing / deleting hotpot / point of interest items can be done directly at the individual hotspot level. Additionally, to make it convenient for a content author to identify which hotspot they want to edit / delete, on load of the tab, we have front end code, which shows the current set of hotspots that already exist as sub items.

While the top / left coordinate % is auto populated by this tool, we have an optional field for the tooltip description which a content author can choose to populate before creating the hotspot.

Futher, in our project, we have tried to make this tool as configurable as possible! So now, we can use this tool on any module, by making sitecore and config updates!

So here’s the code we used:

Aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Hotspots.aspx.cs" Inherits="shared.sitecore_modules.Shell.Editors.ProductResourceHotspot.Hotspots" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Hotspots</title>
    <script src="//code.jquery.com/jquery-1.10.2.min.js"></script>
    <script src="./hotspots.js"></script>
    <link type="text/css" rel="stylesheet" href="./hotspot.css" />
</head>
<body>
    <form id="formHotSpot" runat="server">
        <div data-hotspot='<%=HotSpotsJson %>' class="hotspot-image-wrapper">
            <asp:Image ID="imgResource" runat="server" CssClass="resource-image" />
            <br />
            <asp:Label ID="lblNoImageFound" runat="server" CssClass="not-available">
                Image not found.
            </asp:Label>
            <div id="existingHotspots"></div>
        </div>
        <table id="tableHotSpots">
        </table>
        <input type="hidden" id="hotspotCounter" name="hotspotCounter" />
        <asp:HiddenField runat="server" ID="hiddenHotspotTemplateId" />
        <asp:Button ID="btnSubmit" runat="server" Text="Create Hotspots" OnClick="btnSubmit_Click" CssClass="submit" />
    </form>
</body>
</html>

You’ll see some additional labels etc above, to show alerts to the content author (clearly, I am way too used to MVC now and have completely forgotten how to make nice in asp.net! Please excuse!)

Code Behind

Sitecore actually loads up our aspx page in an iframe, passing in the item id in a querystring parameter, which we use below.

namespace shared.sitecore_modules.Shell.Editors.ProductResourceHotspot
{
    public partial class Hotspots : System.Web.UI.Page
    {
        public string HotSpotsJson { get; set; }

        public hotspot HotspotTemplateInfo { get; set; }

        protected void Page_Load(object sender, EventArgs e)
        {
            LoadHotspotTemplateInfo();

            string itemId = Request.QueryString["id"];
            if (itemId != null)
            {
                Item currentItem = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(itemId);

                hotspotHotspottemplate currentTemplateHotspotSetting = HotspotTemplateInfo.Items
                    .FirstOrDefault(x => x.sourcetemplate[0].Value == currentItem.TemplateID.ToString());

                if (currentTemplateHotspotSetting != null)
                {
                    Field rawField = currentItem.Fields[currentTemplateHotspotSetting.sourceimagefieldname[0].Value];
                    bool fieldIsValidAndHasContent = rawField != null && !string.IsNullOrEmpty(rawField.Value) &&
                                                    FieldTypeManager.GetField(rawField) is ImageField;
                    if (fieldIsValidAndHasContent)
                    {
                        ImageField imageField = rawField;
                        if (imageField.MediaItem != null)
                        {
                            var mediaUrlOptions = new MediaUrlOptions { UseItemPath = false, AbsolutePath = true, Database = Factory.GetDatabase("master") };
                            string imageUrl = MediaManager.GetMediaUrl(imageField.MediaItem, mediaUrlOptions);

                            if (!string.IsNullOrWhiteSpace(imageUrl))
                            {
                                imgResource.Visible = true;
                                lblNoImageFound.Visible = false;
                                imgResource.ImageUrl = Request.Url.Scheme + "://" + Request.Url.Host + imageUrl;
                            }
                            else
                            {
                                imgResource.Visible = false;
                                lblNoImageFound.Visible = true;
                            }
                        }
                        else
                        {
                            imgResource.Visible = false;
                            lblNoImageFound.Visible = true;
                        }
                    }
                    else
                    {
                        imgResource.Visible = false;
                        lblNoImageFound.Visible = true;
                    }

                    hiddenHotspotTemplateId.Value = currentTemplateHotspotSetting.hotspotitemtemplate[0].Value;

                    if (imgResource.Visible)
                    {
                        List<Item> currentHotspots = currentItem.HasChildren
                            ? currentItem.GetChildren().InnerChildren
                                .Where(x => x.TemplateID.ToString() == currentTemplateHotspotSetting.hotspotitemtemplate[0].Value).ToList()
                            : null;

                        if (currentHotspots != null && currentHotspots.Any())
                        {
                            HotSpotsJson = new JavaScriptSerializer().Serialize(new
                            {
                                HotSpots = currentHotspots.Select(x => new
                                {
                                    Description = x.Name + ": "
                                                  + SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Description", x, false),
                                    Left = SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Left Coordinate", x, false),
                                    Top = SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Right Coordinate", x, false)
                                })
                            });
                        }
                    }
                }
            }
        }

        protected void btnSubmit_Click(object sender, EventArgs e)
        {
            int? hotspotCounter = Request.Form["hotspotCounter"].SafeToInt();
            string itemId = Request.QueryString["id"];
            Item currentItem = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(itemId);

            if (hotspotCounter != null)
            {
                using (new SecurityDisabler())
                {
                    for (int i = 1; i <= hotspotCounter; i++)
                    {
                        Item hotspotItem = AddUpdateHelper.CreateSitecoreItemUsingTemplate(currentItem.Paths.FullPath,
                            hiddenHotspotTemplateId.Value, "Hotspot" + DateTime.Now.ToString("ddhhmmssfff"));

                        hotspotItem.Editing.BeginEdit();
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_Top_CoordinateFieldName].Value = Request.Form["txtTop_" + i];
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_Left_CoordinateFieldName].Value = Request.Form["txtLeft_" + i];
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_DescriptionFieldName].Value = Request.Form["txtDesc_" + i];
                        hotspotItem.Fields[IInteractive_Point_Of_InterestConstants.POI_TitleFieldName].Value = "Hotspot" + i;
                        hotspotItem.Editing.EndEdit();
                    }
                }
            }
        }

        private void LoadHotspotTemplateInfo()
        {
            if (Application["HotspotTemplateInfo"] != null)
            {
                HotspotTemplateInfo = (hotspot)Application["HotspotTemplateInfo"];
                return;
            }

            XmlSerializer ser = new XmlSerializer(typeof(hotspot));
            using (XmlReader reader = XmlReader.Create(AppDomain.CurrentDomain.BaseDirectory
                + "\\sitecore modules\\Shell\\Editors\\Hotspots\\HotspotsTemplateData.xml"))
            {
                HotspotTemplateInfo = (hotspot)ser.Deserialize(reader);
                Application["HotspotTemplateInfo"] = HotspotTemplateInfo;
            }
        }
    }
}

XML Configuration

In the above code, you’ll see that we are reading some configuration from an xml file. What we are essentially doing in this xml file, is specifying by template, which field will contain the image that we need to load in the hotspot tab, and the hotspot item template to use. We are using the same base template for hotspots, which is why you see the field names hardcoded in the code above. You could however include these field names also in the xml configuration if your field names between templates are likely to change!

Here’s a sample of the xml in use:

<?xml version="1.0" encoding="utf-8" ?>

<hotspot>

  <hotspottemplate>
    <sourcetemplate hint="/sitecore/templates/User Defined/MySite/Components/Modules/Enhanced POI Module">{85FB47EE-CFD4-405F-84A7-B53BF46CDA25}</sourcetemplate>
    <sourceimagefieldname hint="">Background Image</sourceimagefieldname>
    <hotspotitemtemplate hint="/sitecore/templates/User Defined/MySite/Components/Data/Point Of Interest">{43FB275D-9157-48B3-A68E-F143F044B7EE}</hotspotitemtemplate>
  </hotspottemplate>

</hotspot>

Auto generated cs file for the XML config

I used xsd.exe from Visual Studio developer tools command prompt to generate the class (hotspot) for this xml. You could alternatively do this manually!

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.18444
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System.Xml.Serialization;

// 
// This source code was auto-generated by xsd, Version=4.0.30319.33440.
// 


/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
[System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)]
public partial class hotspot {
    
    private hotspotHotspottemplate[] itemsField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("hotspottemplate", Form=System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public hotspotHotspottemplate[] Items {
        get {
            return this.itemsField;
        }
        set {
            this.itemsField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplate {
    
    private hotspotHotspottemplateSourcetemplate[] sourcetemplateField;
    
    private hotspotHotspottemplateSourceimagefieldname[] sourceimagefieldnameField;
    
    private hotspotHotspottemplateHotspotitemtemplate[] hotspotitemtemplateField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("sourcetemplate", Form=System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable=true)]
    public hotspotHotspottemplateSourcetemplate[] sourcetemplate {
        get {
            return this.sourcetemplateField;
        }
        set {
            this.sourcetemplateField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("sourceimagefieldname", Form=System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable=true)]
    public hotspotHotspottemplateSourceimagefieldname[] sourceimagefieldname {
        get {
            return this.sourceimagefieldnameField;
        }
        set {
            this.sourceimagefieldnameField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlElementAttribute("hotspotitemtemplate", Form=System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable=true)]
    public hotspotHotspottemplateHotspotitemtemplate[] hotspotitemtemplate {
        get {
            return this.hotspotitemtemplateField;
        }
        set {
            this.hotspotitemtemplateField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplateSourcetemplate {
    
    private string hintField;
    
    private string valueField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string hint {
        get {
            return this.hintField;
        }
        set {
            this.hintField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string Value {
        get {
            return this.valueField;
        }
        set {
            this.valueField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplateSourceimagefieldname {
    
    private string hintField;
    
    private string valueField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string hint {
        get {
            return this.hintField;
        }
        set {
            this.hintField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string Value {
        get {
            return this.valueField;
        }
        set {
            this.valueField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true)]
public partial class hotspotHotspottemplateHotspotitemtemplate {
    
    private string hintField;
    
    private string valueField;
    
    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string hint {
        get {
            return this.hintField;
        }
        set {
            this.hintField = value;
        }
    }
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string Value {
        get {
            return this.valueField;
        }
        set {
            this.valueField = value;
        }
    }
}

Stylesheet

Following is the hotspot.css that was used (crude alert!)

.resource-image {
    max-height: 700px;
    max-width: 700px;
    border: 1px solid black;
    margin-left: 20px;
    margin-top: 20px;
}

.desc {
    width: 400px;
}

.dimension {
    width: 30px;
}

.submit {
    display: none;
}

.delete {
    width: 20px;
    height: 20px;
    background-repeat: no-repeat;
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABGdBTUEAALGeYUxB9wAAACBjSFJNAAB6JQAAgIMAAPn/AACA6AAAUggAARVYAAA6lwAAF2/XWh+QAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACoklEQVQ4T62V3UtTcRjHv8uXdNYUp2y+YJgQGGFYkgUqFXVldTWILvwLAm+CLIRICLwML7oJRLzzouiFUkMpnZSiOY/T1LmZ5Zpz5pxm0/m2p+ec83M69RCIH3g4v/M8z/me58fvyzk6YnCIHBFXhTDHpro8MJEJA55phCddWJ/3Y/NEHsxnCxCjtGizwRHo/oIVSYJzPoAr1dWqYMDtRoJjCIkba6BpNzZ4PbVGSHlSB2OSXn16B7LQbHs7FuueIpiZiT8lZSi2WJCUmKhu+ajRCNfMHILDA9AFZqBb/YuTji4sF2bj56hDbomwuLSE8Zvl8N+5je+FhUh7VIOrFRWKmII84RaOwSEau3+XFi9lUvDMMZrLiaFRbhl59UapuxobqZfvO5OP04emJiW3myhBmRWOsdZmsueZaTIG5NSDvrJIt8VCfXx9HxdLNptNbd6HPYJbLHD0P66hPh1ogIVsHG9zc2l8RX6lNlG22UkyR8aNcoRi45RDkO1k8HphWF+Xy5poCrrb2uAsKoKeBX4ZU+E3m5ERCmGw9LLo0EBMGsVwbS194lI/R4vBQA7e5irnrWWlFOSc9cE9tXEf9ghOvHxBHfyQfBCfOTpaWkSF/RpcpqEL+TSbCpqUJJGNJkrQ53TSRyE2zPG6slJUtvGFQuQygHrP5VNY5HYSJdhpMimnyVam1pws8on8bn5IA+TiHqn5nchsExGcslpJimcjJ3BjPPuuo0tU9sdeVUWDp/JoWdxvERGcqH9OnlvXyFuQRY5cPXl9WvOpbHJYkxLI3dOjJgQR21C6CSnF55FechHhLDP7TtNRCnI1u74B/pFvakKw/fniz8/vhmdI8zjQZzqN61UP/yOpIts8Tl2qKHMKFgILZJfsynYOyiH/AoB/WsAsVQ1Uw2EAAAAASUVORK5CYII=);
}

.hotspot-image-wrapper {
    position: relative;
    display: inline-block;
}

#existingHotspots span {
    position: absolute;
}

    #existingHotspots span:before {
        display: inline-block;
        background-color: yellow;
        width: 20px;
        height: 20px;
        content: "";
        border-radius: 50%;
    }

#tableHotSpots {
    margin-left: 20px;
    margin-top: 20px;
    font-family: sans-serif;
    font-size: 13px;
}

#btnSubmit {
    margin-top: 10px;
    margin-left: 210px;
    margin-bottom: 20px;
}

Javascript

And here’s the javascript – hotspot.js (another crude alert – using a template engine would be well advised here!)

$(document).ready(function () {
    var hotspotsobj = $("div[data-hotspot]").data("hotspot");

    if (hotspotsobj != "") {
        $.each(hotspotsobj.HotSpots, function(key, value) {
            $("#existingHotspots").append("<span style=\"left:" + value.Left + "%;top:" + value.Top + "%;\" title=\"" + value.Description + "\"></span>");
        });
    }

    $(document).on("click", ".delete", function() {
        if (confirm("Delete Hotspot?")) {
            $(this).closest("tr").remove();
            if ($("#tableHotSpots tr").length == 1) {
                $("#tableHotSpots tr").remove();
                $(".submit").hide();
            }
        }
    });

    $("#imgResource").click(function (e) {
        if ($(".not-available").length > 0) {
            return;
        }

        if ($(this).attr("src").value != "") {
            var offset_t = $(this).offset().top - $(window).scrollTop();
            var offset_l = $(this).offset().left - $(window).scrollLeft();

            var left = Math.round((e.clientX - offset_l));
            var top = Math.round((e.clientY - offset_t));

            var percentageLeft = Math.round((left / $(this).width()) * 100);
            var percentageTop = Math.round((top / $(this).height()) * 100);

            var hotspotCount = $("#tableHotSpots tr").length;

            if (hotspotCount == 0) {
                $("#tableHotSpots").append("<tr><td>Description</td><td>Left</td><td>Top</td><td></td></tr>");
                hotspotCount++;
                $(".submit").show();
            }

            $("#tableHotSpots").append("<tr><td><input class=\"desc\" name=\"txtDesc_" + hotspotCount + "\" type=\"text\"></input></td>"
                + "<td><input class=\"dimension\" type=\"text\" name=\"txtLeft_" + hotspotCount + "\" value=\"" + percentageLeft + "\"></input></td>"
                + "<td><input class=\"dimension\" type=\"text\" name=\"txtTop_" + hotspotCount + "\" value=\"" + percentageTop + "\"></input></td>"
                + "<td><div class=\"delete\"></div></td></tr>");

            $("#tableHotSpots tr:last-child td:first-child input").focus();

            $("#hotspotCounter").val(hotspotCount);
        }
    });
});

Using Sitecore Image field / Treelist with images in a multisite instance

To come up with a flexible solution to using shared templates across sites with image fields as well – while the sites have separate media repositories, we came up with a solution where the common Site Node (a common parent to all sites) has a field “Media Repository” where we select the corresponding media repository for each site node.

2015-11-01_053241

Using this setup, we are able to link a site node to its corresponding media repository and hence, point the image source of an item to its respective repository folder.

The only restriction here, is that the folder structure would need to be the same in every site media repository. But note – this will also apply to any solution we use for datasources with Sitecore queries in other link / list Sitecore fields.

The code to do this:

using Sitecore.Data.Items;
using System;
using SharedSource.Helpers;

namespace SharedSource.SitecoreFields
{
    public class SiteImage : Sitecore.Shell.Applications.ContentEditor.Image
    {
        public string ItemID
        {
            get;
            set;
        }

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            string source = ServerProperties["Source"].ToString();
            if (String.IsNullOrEmpty(ItemID) || String.IsNullOrEmpty(source) || !source.StartsWith("$sitemediapath")) return;

            SetSource(String.Empty);

            Item current = Sitecore.Context.ContentDatabase.GetItem(ItemID);
            if (current == null) return;

            string siteMediaPath = GetSiteMediaPath(current);
            if (siteMediaPath == null) return;

            source = source.Replace("$sitemediapath", siteMediaPath);
            SetSource(source);
        }

        protected void SetSource(string source)
        {
            ServerProperties["Source"] = source;
            Source = source;
        }

        public static string GetSiteMediaPath(Item item)
        {
            Item site = SitecoreHelper.GetAncestorOrSelfByTemplateId(item, new ID("{Site Node Template Id}"));
            Item path = SitecoreHelper.GetReferenceField(site, "Media Repository");
            return path.Paths.FullPath;
        }
    }
}

2 special custom sitecore helper methods are used here:

  • GetAncestorOrSelfByTemplateId: Basically goes recursively up the content tree to find an ancestor item match by template id.
  • GetReferenceField: This method gets the item referenced in say a droplink field, by fetching the item based on the guid stored in the field.

This content extension will need to be registered in sitecore configuration:

<configuration>
  <sitecore>
    <controlSources>
      <source mode="on" namespace="SharedSource.SitecoreFields" assembly="SharedSource" prefix="contentExtension" />
    </controlSources>
  </sitecore>
</configuration>

And we can add the extension to the simple Image field in the Core DB
2015-11-01_055554

Also, if you have a scenario where you’d want a content author to be able to select multiple images on an item from a treelist, then you’d want to add this support on the treelist field as well. We chose to go with a new custom field though 🙂

using Sitecore.Data.Items;
using Sitecore.Shell.Applications.ContentEditor;

namespace SharedSource.SitecoreFields
{
    public class CustomTreeList : TreeList
    {
        public new string Source
        {
            get { return base.Source; }
            set
            {
                if (!value.StartsWith("$sitemediapath"))
                {
                    base.Source = value;
                }
                else
                {
                    Item item = Sitecore.Context.ContentDatabase.Items[this.ItemID];
                    if (item != null)
                    {
                        string source = value;
                        string mediapath = SiteImage.GetSiteMediaPath(item);
                        source=source.Replace("$sitemediapath", mediapath);
                        base.Source = source;
                    }
                }
            }
        }
    }
}

2015-11-01_060147

2015-11-01_060354

Adding query support to datasources on renderings for a Sitecore multisite instance

Sitecore 8, I believe has inherent support for sitecore queries in the ‘Datasource Location’ field of renderings!
But for those of us waiting on client approvals to make the move from Sitecore 7.xx :(, here’s the way to add that support!
This will come in very handy especially while working with shared renderings to be used across multiple sites in your Sitecore instance.

2015-11-01_042131

We added the following code to the getRenderingDatasource pipeline:

using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetRenderingDatasource;

namespace MySite.Customizations.Customized_Sitecore
{
    public class GetMultisiteRenderingDatasource
    {
        public void Process(GetRenderingDatasourceArgs args)
        {
            Assert.IsNotNull(args, "args");

            string text = args.RenderingItem["Datasource Location"]; 
            if (!string.IsNullOrEmpty(text))
            {
                if (text.StartsWith("query:") && !string.IsNullOrEmpty(args.ContextItemPath))
                {
                    var contextItem = args.ContentDatabase.GetItem(args.ContextItemPath);

                    if (contextItem != null)
                    {
                        text = text.Remove(0, 6); 
                        var item = contextItem.Axes.SelectSingleItem(text);

                        if (item != null)
                        {
                            args.DatasourceRoots.Add(item);
                        }
                    }
                }
            }
        }  
    }
}

The following configuration added it to the desired pipeline

<sitecore>
  <configuration>
    <pipelines>
      <getRenderingDatasource>
        <processor type="MySite.Customizations.Customized_Sitecore.GetMultisiteRenderingDatasource,MySite"
                   patch:before="processor[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel']"/>
      </getRenderingDatasource>
    </pipelines>
  </configuration>
</sitecore>

Multisite configuration for shared Sitecore rendering parameters

On regular templates, we can use sitecore queries to accommodate for showing items in Sitecore link / list fields from respective site nodes in multi site instances.

For Eg:

query:./ancestor::*[@@templatename='Site Node']/Global/Components/Data/Tip Types/*

However, this will not apply to rendering parameter templates. Since while selecting the rendering parameter values in the presentation of an item, the context item is the rendering itself and not the item on which the presentation is being set.
So if we were using a common rendering parameters template for items in multiple sites, we need to rig something up, so that the options appearing in the link / list type rendering parameters show options from the respective site node.

As an example, consider a rendering parameter template, with a background color field.
2015-10-31_235421

This rendering parameter is set to a shared rendering which is used on multiple sites. However, each site has its own unique list of background color options.
To solve this issue, we needed to add support for the token like $sitenode in the example above.

The handler code we write, replaces this token with the right path, after identifying which item the presentation is being set on, and then identifying its sitenode ancestor.

using Sitecore.Data.Items;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Text;
using Sitecore.Web;
using System.Configuration;
using System.Linq;
using SharedSource.Helpers;

namespace MySite.Customizations.Customized_Sitecore
{
    public class MultisiteRenderingParamDroplink : LookupEx
    {
        public new string Source
        {
            get { return base.Source; }
            set
            {
                if (value.Contains("$sitenode"))
                {
                    Item contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];

                    if (contextItem.Template.BaseTemplates
                        .Any(x => x.ID == ItemTree.Templates.System.Layout.Rendering_Parameters.Standard_Rendering_Parameters.ItemID))
                    {
                        string url = WebUtil.GetQueryString();
                        if (!string.IsNullOrWhiteSpace(url) && url.Contains("hdl"))
                        {
                            FieldEditorParameters parameters = FieldEditorOptions.Parse(new UrlString(url)).Parameters;
                            var currentItemId = parameters["contentitem"];
                            if (!string.IsNullOrEmpty(currentItemId))
                            {
                                Sitecore.Data.ItemUri contentItemUri = new Sitecore.Data.ItemUri(currentItemId);
                                contextItem = Sitecore.Data.Database.GetItem(contentItemUri);

                                Item siteNode = SitecoreHelper.GetAncestorOrSelfByTemplateId
                                    (contextItem, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);

                                if (siteNode != null)
                                {
                                    base.Source = value.Replace("$sitenode", siteNode.Paths.FullPath);
                                    return;
                                }
                            }
                        }
                    }
                }
                base.Source = value;
            }
        }
    }
}

Helpers:

            public static Item GetAncestorOrSelfByTemplateId(Item item, string templateId)
            {
                if (item == null || string.IsNullOrEmpty(templateId))
                {
                    return null;
                }

                ID parsedTemplateId;
                return ID.TryParse(templateId, out parsedTemplateId)
                           ? GetAncestorOrSelfByTemplateId(item, parsedTemplateId)
                           : null;
            }

            public static Item GetAncestorOrSelfByTemplateId(Item item, ID templateId)
            {
                if (item == null || templateId == ID.Null || templateId == ID.Undefined)
                {
                    return null;
                }

                Item templateItem = item.Database.GetItem(templateId);
                return templateItem == null ? null : GetAncestorOrSelfByTemplateItem(item, templateItem);
            }

We then register an extension with this type in sitecore:

    <controlSources>
      <source mode="on" namespace="MySite.Customizations.Customized_Sitecore" assembly="MySite" prefix="contentExtension" />
    </controlSources>

This extension is then added to the regular Droplink field. The logic outlined above will kick in only when the current item has the template: /sitecore/templates/System/Layout/Rendering Parameters/Standard Rendering Parameters as its base template. Which would ideally apply to all rendering parameter templates.

2015-11-01_022532

Now the ‘$sitenode’ token would get translated as per the logic added in the above code to the respective site node.

Custom CSS for RTF field for a Multisite Sitecore solution

Sitecore uses the telerik rich text field for allowing rich text on the items. While this allows the content author to enter any styled html they please, we might also want to provide them with a finite list of pre-existing styles which adhere to the current site design.
These can be managed in a CSS file and the CSS file can be referenced by overriding the WebStyleSheet setting in sitecore configuration:

      <setting name="WebStylesheet">
        <patch:attribute name="value">/_CSS/WYSIWYGStyle.css</patch:attribute>
      </setting>

The styles will then appear in the rich text editor for any rich text field:

2015-10-31_233732

2015-10-31_233714

In this case, the css class names in the css file, are TEXT–STYLE-1, link–style-1 etc. If we want more user friendly names, say with spaces etc, we need to override them in the sitecore/shell/Controls/Rich Text Editor/ToolsFile.xml file that telerik uses:

Eg:

<root>
   <classes>
      <class name="Fancy Style Name" value=".TEXT--STYLE-1" />
   </classes>
</root>

Now, all this works perfectly well in a single site environment. If you want to have a different set of styles available to the content authors for each site in a multisite instance of sitecore, you’ll have to get a bit more creative 🙂

Here’s how we went about it.
We created a separate CSS file for each site.
Each site in our sitecore instance had a parent site node item. We added a field to enter the site specific css file path on the node.

2015-10-31_235421

On the site nodes, we would set paths relative to the website folder, eg: /_CSS/Site1/rtfstyles.css

Now for the fun part, in the code:

using Sitecore.Data.Items;
using Sitecore.Web;
using SharedSource.Helpers;

namespace MySite.Customizations.Customized_Sitecore
{
    public class RichTextEditorConfiguration : Sitecore.Shell.Controls.RichTextEditor.EditorConfiguration
    {
        public RichTextEditorConfiguration(Item profile)
            : base(profile)
        { }

        protected override void SetupStylesheets()
        {
            string id = WebUtil.GetQueryString("id");
            Item currentItem = SitecoreHelper.GetItemFromGUIDInMaster(id);
            Item siteNode = SitecoreHelper.GetAncestorOrSelfByTemplateId(currentItem, ISite_NodeConstants.TemplateIdString);

            if (siteNode != null && siteNode.Fields[ISite_NodeConstants.Rich_Text_Field_StylesheetFieldName] != null)
            {
                Editor.CssFiles.Add(siteNode.Fields[ISite_NodeConstants.Rich_Text_Field_StylesheetFieldName].Value);
            }
            base.SetupStylesheets();
        }
    }
}

Helpers:

            public static Item GetItemFromGUIDInMaster(string guid)
            {
                Item item = null;
                if (!string.IsNullOrEmpty(guid))
                {
                    ID ItemID = ID.Parse(guid);
                    Database masterDB = Factory.GetDatabase("master");
                    item = masterDB.GetItem(ItemID);
                }
                return item;
            }

            public static Item GetAncestorOrSelfByTemplateId(Item item, string templateId)
            {
                if (item == null || string.IsNullOrEmpty(templateId))
                {
                    return null;
                }

                ID parsedTemplateId;
                return ID.TryParse(templateId, out parsedTemplateId)
                           ? GetAncestorOrSelfByTemplateId(item, parsedTemplateId)
                           : null;
            }

We use glassmapper here, so ISite_NodeConstants.Rich_Text_Field_StylesheetFieldName contains “Rich Text Field Stylesheet”.

We then patch into the HtmlEditor.DefaultConfigurationType setting:

      <setting name="HtmlEditor.DefaultConfigurationType">
        <patch:attribute name="value">MySite.Customizations.Customized_Sitecore.RichTextEditorConfiguration,MySite</patch:attribute>
      </setting>

Note: This code is triggered when you click on the ‘Show Editor’ button above.

Sitecore Rich Text Field (RTF) length validator

In some cases, to maintain the integrity of the design, we might want to restrict the amount of text that the content author can add in the rich text field. For this purpose we created a custom validator and applied the same to rich text fields only.

For single line / multiline fields, this can be easily achieved by simply using regex patterns for validation, with rich text fields, the presence on html, requires more custom logic.

Please note – you can also disallow certain tags in the custom validation code here, like images / canvas / picture etc, if the design calls for it.

We first added a field for the maximum length, to the Template Field item (/sitecore/templates/System/Templates/Template field). which would make this field available in all template fields.

2015-09-18_014147

Following is the code used for the custom validation, which uses this field:

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Validators;
using System;
using System.Globalization;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;

namespace sc72.Customizations.Sitecore
{
    [Serializable]
    public class RichTextLengthValidator : StandardValidator
    {
        public RichTextLengthValidator(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {

        }

        public RichTextLengthValidator()
        {

        }

        public override string Name
        {
            get { return "RichTextLengthValidator"; }
        }

        protected override ValidatorResult Evaluate()
        {
            ItemUri itemUri = ItemUri;

            if (itemUri == null)
                return ValidatorResult.Valid;

            Field field = GetField();
            if (field == null)
                return ValidatorResult.Valid;

            string str1 = field.Value;
            if (string.IsNullOrEmpty(str1))
                return ValidatorResult.Valid;

            Database database = Factory.GetDatabase(itemUri.DatabaseName);
            Item fieldItem = database.GetItem(field.ID, itemUri.Language);
            int maxLength;

            if (!(fieldItem.Fields["Maximum Length"] != null && !string.IsNullOrWhiteSpace(fieldItem.Fields["Maximum Length"].Value)
                && int.TryParse(fieldItem.Fields["Maximum Length"].Value, out maxLength) && maxLength > 0))
                return ValidatorResult.Valid;

            if (StripHtml(str1).Length > maxLength)
            {
                Text = GetText("Character count '{0}' exceeded the limit '{1}' for field \"{2}\".", new[]
                {
                    StripHtml(str1).Length.ToString(CultureInfo.InvariantCulture),
                    maxLength.ToString(CultureInfo.InvariantCulture),
                    field.DisplayName
                });
                return GetFailedResult(ValidatorResult.FatalError);
            }

            return ValidatorResult.Valid;
        }

        protected override ValidatorResult GetMaxValidatorResult()
        {
            return GetFailedResult(ValidatorResult.FatalError);
        }

        public static string StripHtml(string str)
        {
            return Regex.Replace(str, "<.*?>", string.Empty);
        }
    }
}

And we then created a new validation rule item in Sitecore and pointed it to this method in our assembly:

2015-09-18_014346

This rule then needs to be added to the rich text field item, depending upon what actions you would like to trigger the validations at, the rule will need to be added accordingly. Eg. Quick Action Bar / Validate Button / Validation Bar:

2015-09-18_014447

As an example, consider this field where the validations have been set at:

2015-09-18_014607

So exceeding the character limit (after stripping off html tags!), would give a fatal error.

2015-09-18_015123

2015-09-18_015109

Sitecore Image Size Validator

In most sites, by design we have some minimum image size requirements which the content authors need to adhere by while uploading content images to maintain the integrity of the design.

In our project we wanted to add a validation in place so that the content author wouldn’t be allowed to set images smaller than what minimum requirement was set by the front end designers.

In some cases, we also needed to enforce an exact dimensions match, so we included the provision for that as well.

Here’s how we went about this:

We created a new template to use here – Base Image Validation
2015-09-17_233234

And added this new template as a base for the Template Field template:

2015-09-17_233428

So now, these fields would be available on all template fields. Even though these fields will be available in all template fields, the validation logic we hook up, will be fired only for image field types.

So here’s the code we need to hook up the validation logic:

namespace mySite.Customizations.Customized_Sitecore
{
    [Serializable]
    public class ImageSizeValidator : StandardValidator
    {
        public ImageSizeValidator(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {

        }

        public ImageSizeValidator()
        {

        }

        public override string Name
        {
            get { return "ImageSizeValidator"; }
        }

        protected override ValidatorResult Evaluate()
        {
            ItemUri itemUri = ItemUri;

            if (itemUri == null)
                return ValidatorResult.Valid;

            Field field = GetField();
            if (field == null)
                return ValidatorResult.Valid;

            string str1 = field.Value;
            if (string.IsNullOrEmpty(str1) || string.Compare(str1, "&lt;image /&gt;", StringComparison.InvariantCulture) == 0)
                return ValidatorResult.Valid;
            XmlValue xmlValue = new XmlValue(str1, "image");
            string attribute = xmlValue.GetAttribute("mediaid");
            string str2;
            if (!str1.StartsWith("/"))
                str2 = string.Empty;
            else
                str2 = "/sitecore/media library/" + str1.TrimStart(new[]
                {
                    '/'
                });

            string path = str2;

            if (string.IsNullOrEmpty(attribute) &amp;&amp; string.IsNullOrEmpty(path) || !string.IsNullOrEmpty(xmlValue.GetAttribute("alt")))
                return ValidatorResult.Valid;

            Database database = Factory.GetDatabase(itemUri.DatabaseName);
            Assert.IsNotNull(database, itemUri.DatabaseName);
            Item mediaItem = !string.IsNullOrEmpty(attribute) ? database.GetItem(attribute, itemUri.Language) : database.GetItem(path, itemUri.Language);
            Item fieldItem = database.GetItem(field.ID, itemUri.Language);

            if (mediaItem == null || IsValidDimensions(mediaItem, fieldItem))
                return ValidatorResult.Valid;

            Text = GetText("Selected image in the field \"{0}\" does not match the size restrictions "
                + (SitecoreHelper.ItemRenderMethods.GetCheckBoxValueByFieldName("Exact Dimensions Match", fieldItem)
                ? "(Width:{1}, Height:{2})" : "(Minimum Width:{1}, Minimum Height:{2})"), new[]
            {
                field.DisplayName,
                fieldItem.Fields["Minimum Width"].Value,
                fieldItem.Fields["Minimum Height"].Value
            });
            return GetFailedResult(ValidatorResult.FatalError);
        }

        private bool IsValidDimensions(Item mediaItem, Item fieldItem)
        {
            int validationHeight, validationWidth, imageWidth, imageHeight;

            int.TryParse(fieldItem.Fields["Minimum Width"].Value, out validationWidth);
            int.TryParse(fieldItem.Fields["Minimum Height"].Value, out validationHeight);
            int.TryParse(mediaItem.Fields["Width"].Value, out imageWidth);
            int.TryParse(mediaItem.Fields["Height"].Value, out imageHeight);

            if (SitecoreHelper.ItemRenderMethods.GetCheckBoxValueByFieldName("Exact Dimensions Match", fieldItem))
            {
                if ((validationHeight == 0 &amp;&amp; validationWidth == 0)
                || (validationHeight == 0 &amp;&amp; validationWidth &gt; 0 &amp;&amp; validationWidth == imageWidth)
                || (validationHeight &gt; 0 &amp;&amp; imageHeight == validationHeight &amp;&amp; validationWidth == 0)
                || (validationWidth &gt; 0 &amp;&amp; validationHeight &gt; 0 &amp;&amp; validationWidth == imageWidth &amp;&amp; validationHeight == imageHeight))
                {
                    return true;
                }
            }
            else if ((validationHeight == 0 &amp;&amp; validationWidth == 0)
                || (validationHeight == 0 &amp;&amp; validationWidth &gt; 0 &amp;&amp; validationWidth &lt;= imageWidth) || (validationHeight &gt; 0 &amp;&amp; validationHeight &lt;= imageHeight &amp;&amp; validationWidth == 0) || (validationWidth &gt; 0 &amp;&amp; validationHeight &gt; 0 &amp;&amp; validationWidth &lt;= imageWidth &amp;&amp; validationHeight &lt;= imageHeight))
            {
                return true;
            }

            return false;
        }

        protected override ValidatorResult GetMaxValidatorResult()
        {
            return GetFailedResult(ValidatorResult.FatalError);
        }
    }
}

And we then created a new validation rule item in Sitecore and pointed it to this method in our assembly:

2015-09-17_234518

This rule then needs to be added to the image field item, depending upon what actions you would like to trigger the validations at, the rule will need to be added accordingly. Eg. Quick Action Bar / Validate Button / Validation Bar:

2015-09-18_001320

As an example, consider this field where the validations have been set at:

2015-09-18_002701

So uploading an image smaller than this would result in the error state:

2015-09-18_002909

Setting site specific workflows to items using shared templates in a multisite Sitecore solution

In a multisite solution, we might come across the need to have site specific workflows.
And if we use glassmapper with codegen – we might want to reuse as many templates as we possibly can with the basic goal of reusing as much code as possible.

In the regular scenario, we would set the desired workflow for an item in the ‘Default Workflow’ field of the standard values item of the template. But since we are sharing templates across sites – this doesn’t really work for us!

To resolve this, we came up with a solution – which basically sets the workflow on an item depending on its location in the content tree. We also added an option to turn on / off setting workflow based on the template used to create the item.

We added a field to the system template ‘Template’ which basically indicates if a given template is marked to go into the site workflow or not:

2015-08-30_223537

So this field would now be available in all templates – and we could check it for templates, items of which need to be added to the site workflow.

Now to associate a site with a workflow, we added a field to the SiteNode (A parent item of each site in our multisite instance). You could also choose to add this to your home node itself.

2015-08-30_225228

2015-08-30_224409

So now that we have a way to determine which workflow we need to set on an item based on its location in the content tree and the flag that tells us whether or not it needs to be added to a given workflow, all we need to do is add the event handler which will add the workflow to the item conditionally – when the item is created.

Here’s the code we used in the event handler:

using Sitecore.Configuration;
using Sitecore.Data.Events;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.SecurityModel;
using System;
using System.Configuration;
using Sitecore.Workflows;
using SharedSource.Helpers;
using Event = Sitecore.Events.Event;

namespace shared._Classes.Shared.Customizations.Customized_Sitecore
{
    public class SetItemWorkflow
    {
        public void OnItemCreated(object sender, EventArgs args)
        {
            using (new SecurityDisabler())
            {
                var arg = Event.ExtractParameter(args, 0) as ItemCreatedEventArgs;
                if (arg == null || (Sitecore.Context.Site != null && Sitecore.Context.Site.Name != "shell")) return;

                try
                {
                    if (!SitecoreHelper.ItemRenderMethods.GetCheckBoxValueByFieldName("Use Site Workflow", arg.Item.Template)) return;

                    Item currentSiteNode = SitecoreHelper.ItemMethods.GetAncestorOrSelfByTemplateId(arg.Item, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);
                    if (currentSiteNode == null) return;

                    string currentSiteNodeDefaultWorkflow = SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Default Workflow", currentSiteNode, false);

                    if (string.IsNullOrWhiteSpace(currentSiteNodeDefaultWorkflow)) return;

                    using (new EditContext(arg.Item, SecurityCheck.Disable))
                    {
                        IWorkflow workflow = Factory.GetDatabase("master").WorkflowProvider.GetWorkflow(currentSiteNodeDefaultWorkflow);

                        if (workflow == null) return;
                        workflow.Start(arg.Item);
                    }
                }
                catch (Exception ex)
                {
                    Log.Error("Error on creating item (SetItemWorkflow):" + arg.Item.ID + Environment.NewLine + "StackTrace:" + ex.StackTrace, ex);
                }
            }
        }
    }
}

Some of the helper methods used here:

  • GetCheckBoxValueByFieldName: Gets the value from a checkbox field and casts it into a boolean value
  • GetAncestorOrSelfByTemplateId: Recursively goes up the content tree from the current item upto the content node, and returns the first item matched by template id passed in
  • GetRawValueByFieldName: Gets the raw value stored in the given field

Additionally, we add the configuration required to add this event handler to the item:created event:

<sitecore>
    <events>
      <event name="item:created">
        <handler type="shared._Classes.Shared.Customizations.Customized_Sitecore.SetItemWorkflow, shared" method="OnItemCreated" />
      </event>
    </events>
</sitecore>

Custom Multisite Multilist with Search Sitecore field

We are currently working on a multisite sitecore 7.2 with MVC solution. During this time, we came across the need to allow dynamic datasources to Multilist with search sitecore fields.
While looking out for a solution, I came across this post which give a great way of inducing queries into the datasource of a multilist with search field: Dynamic Multilist with Search.

However, we also had instances in our site, where the repository lived outside of the site node itself. So we needed to build something custom here.

For example: Sitecore Commerce Connect’s product repository lives outside of the home node:
2015-07-06_005755
Similarly, we also had video repositories for each site which live inside the Media Library.

So we ended up creating a custom field to cater to our needs here. This new field, took as input in the search location parameter – certain special tokens, which we then resolved in the code. In addition, we also used filters to filter through multiple templates – since each site might have separate template for say products etc.

In our solution in sitecore, we created a common template – Site Node – which was used to create each site’s root, and the respective Home nodes were child items of these Site Nodes.
Any external repositories which were associated with respective sites, were associated using fields on the site node.

Example, for Site 1:
2015-07-06_011233

In line with the requirements we had, we created support for 3 special tokens

  • $sitenode
  • $siteproductsrepository
  • $sitevideorepository

So basically in the datasource of this new field, we would use values like:

*************************** PRODUCTS – Multisite Multilist with Search *****************************
StartSearchLocation=$siteproductrepository&Filter=_templatename:Site1 Product|_templatename:Site2 Product

************************* Product Category – Multisite Multilist with Search ***********************
StartSearchLocation=$sitenode/Home/Products&Filter=_templatename:Site1 Product Category|_templatename:Site2 Product Category

************************* Video – Multisite Multilist with Search ***********************
StartSearchLocation=$sitevideorepository&Filter=_templatename:Video File

The code used:

namespace MySite._Classes.SBDShared.Customizations.Customized_Sitecore
{
    class MultisiteMultilistWithSearchField : BucketList
    {
        public new string Source
        {
            get { return base.Source; }
            set
            {
                if (value.Contains("$siteproductrepository"))
                {
                    var contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];
                    var siteNode = SitecoreHelper.ItemMethods.GetAncestorOrSelfByTemplateId(contextItem, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);
                    if (siteNode != null)
                    {
                        var productRepository = SitecoreHelper.ItemRenderMethods.GetReferenceField(siteNode, ISite_NodeConstants.Product_RepositoryFieldName);
                        base.Source = productRepository != null ? value.Replace("$siteproductrepository", productRepository.ID.ToString()) : value;
                    }
                }
                else if (value.Contains("$sitevideorepository"))
                {
                    var contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];
                    var siteNode = SitecoreHelper.ItemMethods.GetAncestorOrSelfByTemplateId(contextItem, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);
                    if (siteNode != null)
                    {
                        var videoRepository = SitecoreHelper.ItemRenderMethods.GetReferenceField(siteNode, ISite_NodeConstants.Video_RepositoryFieldName);
                        base.Source = videoRepository != null ? value.Replace("$sitevideorepository", videoRepository.ID.ToString()) : value;
                    }
                }
                else if (value.Contains("$sitenode"))
                {
                    const string startSearchLocationText = "StartSearchLocation=";
                    var sourceString = value;
                    var contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];

                    int startSearchLocationIndex = sourceString.IndexOf(startSearchLocationText, System.StringComparison.OrdinalIgnoreCase);

                    string startSearchLocation = sourceString.Substring(startSearchLocationIndex + startSearchLocationText.Length,
                        sourceString.IndexOf("&", startSearchLocationIndex, System.StringComparison.Ordinal) - startSearchLocationText.Length);

                    var siteNode = SitecoreHelper.ItemMethods.GetAncestorOrSelfByTemplateId(contextItem, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);

                    if (siteNode != null)
                    {
                        var sourceItem = SitecoreHelper.ItemMethods.GetItemByPathInMaster(startSearchLocation.Replace("$sitenode", siteNode.Paths.FullPath));
                        base.Source = sourceItem != null ? value.Replace(startSearchLocation, sourceItem.ID.ToString()) : value;
                    }
                }
                else
                {
                    base.Source = value;
                }
            }
        }
    }
}

2 special custom sitecore helper methods are used here:

  • GetAncestorOrSelfByTemplateId: Basically goes recursively up the content tree to find an ancestor item match by template id.
  • GetReferenceField: This method gets the item referenced in say a droplink field, by fetching the item based on the guid stored in the field.

Finally, to make this new control available in sitecore – we need to add a corresponding item in the core database (Make a copy from the similar Multilist with Search for simplicity here).
2015-07-06_014516

A corresponding config update would be required to register the prefix used above:

<sitecore>
  <configuration>
    <controlSources>
      <source mode="on" namespace="MySite._Classes.SBDShared.Customizations.Customized_Sitecore" assembly="MySite" prefix="contentExtension" />
    </controlSources>
  </sitecore>
</configuration>