Data source lock and workflow notifications for Sitecore authors

The Sitecore Experience Editor and Content Editor are great tools to make the content editing and creation process streamlined for Sitecore users. It tells you, for a given page on your site, who has the page locked, and where the page is in the selected workflow.

However, it doesn’t always tell you everything that would be good to know. If you have multiple data sources contributing to your page, Sitecore doesn’t tell you if someone has one of those data sources locked, or if one of them is currently in the middle of workflow.

A great extension was written to mitigate this in 2015, by Brent Svac and Sheetal Jain of Horizontal Integration, called Data Source Workflow Module.

This module tells you if the data source of the rendering is in workflow, by adding notifications to the Content Editor and Experience Editor.

We can extend the functionality here by leveraging Glass Mapper to add code which scans through each field on the rendering’s data source item, to determine if that field is a Droplink, a Multilist, or any other kind of field that could point to another data source. This will add them to the list which is then used to generate the notifications. We'll also add notifications to tell the user if another user has the item locked.

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())    {        var renderingDataSource = reference.GetDataSourceItem();        list.AddUnqiueDataSourceItem(renderingDataSource);        list.AddRange(GetReferencedDatasourceItems(renderingDataSource));        ...}            private static IEnumerable<Item> GetReferencedDatasourceItems(Item dataSourceItem){    var list = new List<Item>();    try    {        if (dataSourceItem != null)        {            var db = Sitecore.Configuration.Factory.GetDatabase("master");            var context = new SitecoreContext(db);            var stronglyTypedItem = context.Cast<ISitecoreItem>(dataSourceItem, isLazy: false, inferType: true);            var properties = stronglyTypedItem.GetType().GetProperties();            foreach (var prop in properties)            {                //var propertyValue = prop.GetValue(stronglyTypedItem);                 if (typeof(IEnumerable<ISitecoreItem>).IsAssignableFrom(prop.PropertyType))                {                    var multilist = prop.GetValue(stronglyTypedItem) as IEnumerable<ISitecoreItem>;                    if (multilist == null) continue;                    foreach (var item in multilist)                    {                        list.Add(GetDataSourceItem(item.Id.ToString(), db));                    }                }                else if (typeof(ISitecoreItem).IsAssignableFrom(prop.PropertyType))                {                    var item = prop.GetValue(stronglyTypedItem) as ISitecoreItem;                    if (item == null) continue;                    list.Add(GetDataSourceItem(item.Id.ToString(), db));                }            }        }    }    catch (Exception ex)    {        Sitecore.Diagnostics.Log.Error("Error GetReferencedDatasourceItems", ex, typeof(ItemExtensions));    }    return list;}

To add our notifications to tell the user who has the item locked, we'll extend the item workflow model class. In ItemWorkflowModel.cs, we add a new property to tell us if the item is locked and should show a notification.

public bool ShowLockNotification{    get    {        return ContextItem.Locking.IsLocked() && !ContextItem.Locking.HasLock();    }}

We also add a method to get the notification text for a locked item.

public string GetLockNotificationDescription(){    //Thanks to    var message = string.Empty;    if (!ShowLockNotification)    {        return message;    }    var item = ContextItem;    if (item != null)    {        var itemDisplayName = item.Paths.ContentPath;        var locking = item.Locking;        if (locking.CanUnlock())        {            message = "'" + locking.GetOwner() + "' has locked the data source item '"                + itemDisplayName + "'.";        }        else        {            message = "You cannot edit the data source item '" + itemDisplayName + "' because '" +                        locking.GetOwner() + "' has locked it.";        }    }    return message;}

In GetContentEditorWarnings\GetDataSourceWorkflowNotification.cs:

We alter the GetNotifications method to add a lock notification.

public void GetNotifications(GetContentEditorWarningsArgs arguments, Item contextItem){    if (arguments == null) return;    if(contextItem == null)    {        Log.Error("ContextItem is null", this);        return;    }    var wfModel = new ItemWorkflowModel(contextItem);    if(wfModel == null)    {        Log.Error("Workflow model is null for: " + contextItem.Paths?.FullPath, this);        return;    }    if (wfModel.ShowNotification)    {        SetNotification(arguments, wfModel);    }    if (wfModel.ShowLockNotification)    {        SetLockNotification(arguments, wfModel);    }}private void SetLockNotification(GetContentEditorWarningsArgs arguments, ItemWorkflowModel wfModel){    if(wfModel == null)    {        Log.Error("wfModel is null", this);        return;    }    try    {        var editorNotification = arguments.Add();        editorNotification.Title = "Data Source Item Locked";        editorNotification.Text = wfModel.GetLockNotificationDescription();        editorNotification.Icon = wfModel.WorkflowState.Icon;    }    catch(Exception ex)    {        Log.Error("Error setting notificaiton", ex, this);    }}

In GetPageEditorNotifications\GetDataSourceWorkflowNotificaiton.cs:

We alter the GetNotifications method to add a lock notification.

public void GetNotifications(GetPageEditorNotificationsArgs arguments){    if (arguments == null) return;    if(arguments.ContextItem == null)    {        Log.Error("arguments.Context item is null", this);        return;    }    var wfModel = new ItemWorkflowModel(arguments.ContextItem);    if (wfModel.ShowNotification)    {        SetNotification(arguments, wfModel);    }    if (wfModel.ShowLockNotification)    {        SetLockNotification(arguments, wfModel);    }}private void SetLockNotification(GetPageEditorNotificationsArgs arguments, ItemWorkflowModel wfModel){    if(wfModel == null)    {        Log.Error("wfModel is null", this);        return;    }    if(wfModel.WorkflowState == null)    {        Log.Error(" wfModel.WorkflowState is null", this);        //No need to abort, just won't have an icon.    }    try    {        var editorNotification = new PageEditorNotification(wfModel.GetLockNotificationDescription(), PageEditorNotificationType.Warning)        {            Icon = wfModel.WorkflowState?.Icon        };        arguments.Notifications.Add(editorNotification);    }    catch(Exception ex)    {        Log.Error("Error setting editor notifications: " + wfModel?.ContextItem?.Paths?.FullPath, ex, this);    }}

Happy coding!