Extending Sitecore Workbox and Bulk Publish for Workflow Items

This is for those Sitecore Content Authors and Developers that use Workflow and Workbox for hundreds of items. I'm guessing you've experienced performance issues and you're just tired of it.

Published:

Recently, I got tasked with finding a way to improve performance in the Sitecore Workbox while approving and publishing items to higher steps in the workflow. As may you know, Sitecore provides a default Publish Action for the Approved state.

Sitecore.Workflows.Simple.PublishAction, Sitecore.Kernel

It works fine... Right until you have hundreds of items being approved in the workbox at the same time and you start to see hundreds of publish actions in your job list. These publish jobs take forever to finish and cause performance issues on the CM server.

To accomplish this, I extended the default Workbox form code and created a new template in Sitecore for Command items called Extended Command. Additionally, I made use of the following gist for bulk publishing code. All credit for that goes to jraps20

All right, let's begin!

First, let's look at the Sitecore itemsI created for this. I created a template inheriting from /sitecore/templates/System/Workflow/Command and I called it Extended Command. I added a new section called Action and a Single-Line Text field called Parameters, like so:

extended command template fields

Then I created a new command for a workflow state based on the previously shown template:

sitecore extended command item

As you can see, the parameters are for the publishing task and need to have a query string format.

deep -> 0 or 1 | related -> 0 or 1 | targets -> target Sitecore database name | alllanguages -> 0 or 1 

One important thing to notice is that the Extended Command doesn't need any children actions related to publishing. The decision for this was that the original ExecutCommand (amazingly, the typo is part of the Sitecore codebase) method will iterate over the whole list of items that are changing state in the Workflow and run the child actions for each one of the items.

This resulted in the case we wanted to avoid as explained at the beginning of this blog post. So, to be able to improve performance, we needed to delete any child action related to publishing, move the parameters to the Extended Command and override the ExecutCommand method to do a bulk publish task at the command level.

Moving on!

Now that we have everything set in Sitecore, let's look at the code for the Workbox application. It's a Web Forms page with a code behind coming from:

Sitecore.Shell.Applications.Workbox.WorkboxForm, Sitecore.Client

I decompiled the DLL, grabbed the code for ExecutCommand and added a couple of lines. You can see those highlighted in the following snippet as well as utility methods used for setting the correct parameters to use with the Publishing Utils mentioned before.

///Executes specific command on multiple selected items
///A list of ItemUris
///The workflow
///Fileds dictionary
///The command
///The Workflow State ID
protected override void ExecutCommand(
    List itemUris,
    IWorkflow workflow,
    Sitecore.Collections.StringDictionary fields,
    string command,
    string workflowStateId)
{
    bool flag = false;
    if (fields == null)
        fields = new Sitecore.Collections.StringDictionary();
    foreach (ItemUri itemUri in itemUris)
    {
        Item obj = Context.ContentDatabase.GetItem(itemUri.ItemID);
        if (obj == null)
        {
            flag = true;
        }
        else
        {
            WorkflowState state = workflow.GetState(obj);
            if (state != null && (string.IsNullOrWhiteSpace(workflowStateId) || state.StateID == workflowStateId))
            {
                if (fields.Count >= 1)
                {
                    if (fields.ContainsKey("Comments"))
                        goto label_10;
                }
                string str = string.IsNullOrWhiteSpace(state.DisplayName) ? string.Empty : state.DisplayName;
                fields.Add("Comments", str);
            label_10:
                try
                {
                    if (itemUris.Count == 1)
                    {
                        Processor completionCallback = new Processor("Workflow complete state item count", (object)this, "WorkflowCompleteStateItemCount");
                        workflow.Execute(command, obj, fields, true, completionCallback);
                    }
                    else
                        workflow.Execute(command, obj, fields, true);
                }
                catch (WorkflowStateMissingException ex)
                {
                    flag = true;
                }
            }
        }
    }

    if (Context.ContentDatabase.GetItem(ID.Parse(command)).Fields["Parameters"] != null)
    {
        var parameters =
            WebUtil.ParseUrlParameters(Context.ContentDatabase.GetItem(ID.Parse(command)).Fields["Parameters"]
                .Value);
        var targetDatabases = this.GetTargets(parameters).ToArray();
        var targetLanguages = this.GetLanguages(parameters).ToArray();

        PublishUtils.CreateAndPublishQueue(Context.ContentDatabase, targetDatabases, targetLanguages,
            itemUris.Select(i => i.ItemID));
    }

    if (!flag)
        return;
    SheerResponse.Alert("One or more items could not be processed because their workflow state does not specify the next step.");
}

///Gets the targets.
///The parameters.
/// The targets.
private IEnumerable GetTargets(NameValueCollection parameters)
{
    using (new SecurityDisabler())
    {
        IEnumerable targetNames = this.GetEnumerableValue("targets", parameters);
        foreach (string name in targetNames)
        {
            Database database = Factory.GetDatabase(name, false);
            if (database != null)
                yield return database;
            else
                Log.Warn("Unknown database in PublishAction: " + name, (object)this);
        }
    }
}

///Gets the languages.
///The parameters.
/// An enumerable of discovered languages.
private IEnumerable GetLanguages(NameValueCollection parameters)
{
    using (new SecurityDisabler())
    {
        IEnumerable languageNames = Enumerable.Empty();
        bool allLanguages = this.GetStringValue("alllanguages", parameters) == "1";
        if (allLanguages)
        {
            Item obj = Context.ContentDatabase.Items["/sitecore/system/languages"];
            if (obj != null)
                languageNames = obj.Children.Where((Func<Item, bool>)(child => child.TemplateID == TemplateIDs.Language)).Select<Item, string>((Func<Item, string>)(child => child.Name));
        }
        foreach (string name in languageNames)
        {
            Language language = (Language)null;
            if (Language.TryParse(name, out language))
                yield return language;
            else
                Log.Warn("Unknown language in PublishAction: " + name, (object)this);
        }
    }
}

///Gets a string value.
///The name.
///The parameters.
/// The discovered value or null.
private string GetStringValue(string name, NameValueCollection parameters)
{
    string parameter = parameters[name];
    return !string.IsNullOrEmpty(parameter) ? parameter : (string)null;
}

///Gets an enumerable value.
///The name.
///The parameters.
/// An enumerable of resulting items.
private IEnumerable GetEnumerableValue(string name, NameValueCollection parameters)
{
    string parameter = parameters[name];
    if (string.IsNullOrEmpty(parameter))
        return Enumerable.Empty();
    return ((IEnumerable)parameter.Split(new char[1]
    {
        ','
    }, StringSplitOptions.RemoveEmptyEntries)).AsEnumerable();
}

 

What this code does is create a list of publishing candidates based on the parameters you set in your Extended Command item and execute only one publish task for the whole list.

After having done these changes in your codebase and Sitecore instance, you should be able to go to your workbox and execute the newly created command for a selection of items or all items as you would do before.

This way you will avoid having hundreds of publish tasks clogging your CM server processes and the Workbox approval flow will improve in scenarios with lots of items.

Happy coding!