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

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>

Workflow notifications in Sitecore (Roles & users)

Objective

To notify user(s) when an item moves into a certain state of an associated workflow in sitecore (Sitecore 6.5).

Introduction

Sitecore allows us to create workflows which can be associated with content items. In most scenarios, these workflows are a necessity seeing as any content seeking to appear in the live site, must undergo various steps of reviews and approval.

Sitecore lets you create a workflow with any number of states, each state can have a collection of commands, and each command can trigger a collection of actions.

Sitecore does provide multiple type of actions that can be added to execute when a command is triggered.

If you want to add an email action to notify users when an item is submitted / approved / rejected etc, you could sure try to use the out of the box email action (/sitecore/templates/System/Workflow/Email action). This works perfectly well when you have an email address as your recipient. When you have a role (all users in a role) or multiple roles or email addresses or a combination there of as your recipients, you will need to turn to a custom action (/sitecore/templates/System/Workflow/Action) for now.

Implementation

Add a custom action (/sitecore/templates/System/Workflow/Action) to the command which you want to add the notification functionality (Notify):

2014-04-26_013621

Available workflows are found at: /sitecore/system/Workflows.

A custom action takes a fully qualified class name, assembly name and optional parameters (available in the WorkflowPipelineArgs object)

A command (template: /sitecore/templates/System/Workflow/Command) has a Supress Comment field in addition to the Next State field. Left unchecked, the user is asked for a comment when this command is triggered. This comment appears in the item history:

2014-04-26_025107

And also note that this is available in the WorkflowPipelineArgs and is used in the highlighted line below.

Sample code of the custom action code:

using System.Configuration;
using System.Net.Mail;
using Mysite.Helpers;
using Sitecore.Workflows.Simple;

namespace Mysite.WorkflowActions
{
    public class NotifyReject
    {
        public void Process(WorkflowPipelineArgs args)
        {
            var contentItem = args.DataItem;

            var emailMessage = new MailMessage
            {
                From = new MailAddress(ConfigurationSettings.AppSettings["ContentSubmissionEmailFromId"]),
                Subject = "Content submitted: " + contentItem.Name,
                Body = "The following content item has been submitted for review: '" + contentItem.Name + "' (" + contentItem.Paths.Path + ")"
                       + " with the comment: '" + args.Comments + "'"
            };

            var userRolesSetting = ConfigurationSettings.AppSettings["ContentSubmissionEmailToRole"];
            emailMessage = WorkflowHelper.AddRecipientsToMail(emailMessage, userRolesSetting);

            WorkflowHelper.SendMail(emailMessage);
        }
    }
}

And here is the helper method which will allow you to take a configuration string of the like of ‘sitecore\Custom Content Authors|sitecore\Content Admin|admin@example.com’ and build a list of recipients to send out the email. In this snippet, the list of roles / email addresses has been stored in the web.config file. This could also be stored in a sitecore settings item, in which case it would be available to the content authors to update.

Note the highlighted lines to get the users from a role.


public static MailMessage AddRecipientsToMail(MailMessage mailMessage, string recipientsFromConfig)
        {
            var userRoleNames = recipientsFromConfig.Split('|');
            foreach (var userRoleName in userRoleNames)
            {

                if (Role.Exists(userRoleName))
                {
                    var role = Role.FromName(userRoleName);
                    var users = RolesInRolesManager.GetUsersInRole(role, true);
                    foreach (var user in users.Where(x => x.IsInRole(role)).Where(user => !string.IsNullOrEmpty(user.Profile.Email)))
                    {
                        mailMessage.To.Add(user.Profile.Email);
                    }
                }
                else
                {
                    var rgxEmail = new Regex(@"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}" +
                            @"\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\" +
                            @".)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$");
                    if (rgxEmail.IsMatch(userRoleName))
                        mailMessage.To.Add(userRoleName);
                }
            }

            return mailMessage;
        }

You might also want to send out an email to the content author that last edited the item in question:

// Get user who last edited the item
var contentWorkflow = contentItem.Database.WorkflowProvider.GetWorkflow(contentItem);
var contentHistory = contentWorkflow.GetHistory(contentItem);

if (contentHistory.Length > 0)
{
	var lastUser = contentHistory.Last().User;
	var submittingUser = User.FromName(lastUser, false);
	emailMessage.To.Add(submittingUser.Profile.Email);
}