Store custom Sitecore Workflows within a folder and displaying correctly within the Workbox.

As most of you know reading my blog, I am a big fan of Unicorn, and my preference for developing is to enable Transparent Sync so you never have to worry about sync’ing your custom Sitecore items.

However I came across an interesting scenario while creating a custom workflow for a client. Initially, without thinking about it, I put the entire workflow folder ( /sitecore/system/Workflows ) under Unicorn’s control. With a Sitecore 7 solution, there are only three workflows. I know I forgot my golden rule about only putting custom items under Unicorn’s control.

I excluded the built-in workflows, but doing this prevents you from using Unicorn’s Transparent Sync. Next step was to create a folder within the workflow folder, and then move the custom workflow to that folder. So my workflow folder looked similar to

Workflow

Next was to update my Unicorn configuration file, and have Transparent Sync work again. This step completed everything initially looked good.

I went and set an item to use the new workflow item created

workflow_03

Everything looked good. Then I opened the Workbox

workflow_04

Oops. The custom workflow is not listed, but the folder I created is. Selecting the folder does nothing. I expected that but I had to try to see what would happen.

As Sitecore is customizable, I started looking around, and came across the IWorkflowProvider interface, which is what is being used by the Workbox to display the workflows.

Then I did a stupid thing. Looking for what is used by default, I started searching the config files for IWorkflowProvider, when I should have been searching for WorkflowProvider. Big thanks to Alex Washtell for pointing me in the right direction If I had searched for WorkflowProvider, I would have come across the default WorkflowProvider that is supplied within Sitecore.Kernal.dll

Using Jetbrain’s dotPeek, this is the default WorkflowProvider supplied by Sitecore.


using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Workflows.Simple
{
  /// <summary>Represents a workflow provider.</summary>
  public class WorkflowProvider : IWorkflowProvider
  {
    private Database _database;
    private readonly HistoryStore _store;
    private readonly string _databaseName;

    /// <summary>Gets the database.</summary>
    /// <value>The database.</value>
    public virtual Database Database
    {
      get
      {
        if (this._database == null)
          this._database = Factory.GetDatabase(this._databaseName);
        return this._database;
      }
    }

    /// <summary>Gets the history store.</summary>
    /// <value>The history store.</value>
    public virtual HistoryStore HistoryStore
    {
      get
      {
        return this._store;
      }
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="T:Sitecore.Workflows.Simple.WorkflowProvider" /> class.
    /// </summary>
    /// <param name="databaseName">Name of the database.</param>
    /// <param name="historyStore">The history store.</param>
    public WorkflowProvider(string databaseName, HistoryStore historyStore)
    {
      Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
      Assert.ArgumentNotNull((object) historyStore, "historyStore");
      this._databaseName = databaseName;
      this._store = historyStore;
    }

    /// <summary>Gets the workflow.</summary>
    /// <param name="item">The item.</param>
    /// <returns>The workflow.</returns>
    public virtual IWorkflow GetWorkflow(Item item)
    {
      Assert.ArgumentNotNull((object) item, "item");
      string workflowId = WorkflowProvider.GetWorkflowID(item);
      if (workflowId.Length > 0)
        return this.InstantiateWorkflow(workflowId, this);
      return (IWorkflow) null;
    }

    /// <summary>Gets the workflow.</summary>
    /// <param name="workflowID">The workflow ID.</param>
    /// <returns>The workflow.</returns>
    public virtual IWorkflow GetWorkflow(string workflowID)
    {
      Assert.ArgumentNotNullOrEmpty(workflowID, "workflowID");
      Error.Assert(ID.IsID(workflowID), "The parameter 'workflowID' must be parseable to an ID");
      if (this.Database.Items[ID.Parse(workflowID)] != null)
        return this.InstantiateWorkflow(workflowID, this);
      return (IWorkflow) null;
    }

    /// <summary>Gets the workflows.</summary>
    /// <returns>The workflows.</returns>
    public virtual IWorkflow[] GetWorkflows()
    {
      Item obj = this.Database.Items[ItemIDs.WorkflowRoot];
      if (obj == null)
        return new IWorkflow[0];
      Item[] array = obj.Children.ToArray();
      IWorkflow[] workflowArray = new IWorkflow[array.Length];
      for (int index = 0; index < array.Length; ++index)
        workflowArray[index] = this.InstantiateWorkflow(array[index].ID.ToString(), this);
      return workflowArray;
    }

    /// <summary>
    /// Initializes this instance from the specified config item.
    /// </summary>
    /// <param name="configItem">The config item.</param>
    public virtual void Initialize(Item configItem)
    {
    }

    /// <summary>Gets the workflow ID.</summary>
    /// <param name="item">The item.</param>
    /// <returns>The workflow ID.</returns>
    protected static string GetWorkflowID(Item item)
    {
      Assert.ArgumentNotNull((object) item, "item");
      WorkflowInfo workflowInfo = item.Database.DataManager.GetWorkflowInfo(item);
      if (workflowInfo != null)
        return workflowInfo.WorkflowID;
      return string.Empty;
    }

    /// <summary>Instantiates the workflow.</summary>
    /// <param name="workflowId">The workflow id.</param>
    /// <param name="owner">The owner.</param>
    /// <returns>The workflow.</returns>
    protected virtual IWorkflow InstantiateWorkflow(string workflowId, WorkflowProvider owner)
    {
      return (IWorkflow) new Workflow(workflowId, owner);
    }
  }
}


I didn’t want to create a new class, I only wanted to override one method IWorkflow[] GetWorkflows(). My code will still process each item within the folder, but there is now a check to see if the item is based on the workflow template, or the default folder template. If the item is created using the folder template, then perform a recursive class.

My inherited class is


    public class CustomWorkflowProvider : WorkflowProvider, IWorkflowProvider 
    {
        public CustomWorkflowProvider(string databaseName, HistoryStore historyStore) : base(databaseName, historyStore)
        {
        }

        public override IWorkflow[] GetWorkflows()
        {
            Item obj = this.Database.Items[ItemIDs.WorkflowRoot];
            if (obj == null)
                return new IWorkflow[0];
            var workflowArray = new List<IWorkflow>();
            workflowArray.AddRange(GetWorkflowsFor(obj));
            return workflowArray.ToArray();
        }

        private bool IsWorkflow(Item item)
        {
            return item.TemplateID.Equals(new ID("{1C0ACC50-37BE-4742-B43C-96A07A7410A5}"));
        }

        private bool IsFolder(Item item)
        {
            return item.TemplateID.Equals(new ID("{A87A00B1-E6DB-45AB-8B54-636FEC3B5523}"));
        }

        private IEnumerable<IWorkflow> GetWorkflowsFor(Item rootItem)
        {
            var result = new List<IWorkflow>();
            var childItems = rootItem.Children.ToList();
            foreach (var item in childItems)
            {
                if (IsWorkflow(item))
                {
                    result.Add(this.InstantiateWorkflow(item.ID.ToString(), this));
                }
                if (IsFolder(item))
                {
                    result.AddRange(GetWorkflowsFor(item));
                }
            }
            return result;
        }
    }

I hard coded the id’s because they are not going to change and thought it was an acceptable compromise. This code works for me. I could have spent some time cleaning it up, and using a more fluid syntax to remove the foreach, removing the if statements, but unless I have performance issues down the line, this code is acceptable. After updating my solution config file.


<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" >
  <sitecore>
    <databases>
      <database id="master" singleInstance="true" type="Sitecore.Data.Database, Sitecore.Kernel">
        <workflowProvider hint="defer" type="Sitecore.Workflows.Simple.WorkflowProvider, Sitecore.Kernel">
          <patch:delete />
        </workflowProvider>
        <workflowProvider hint="defer" type="Project.Sitecore.Workflow.CustomWorkflowProvider, Project">
          <param desc="database">$(id)</param>
          <param desc="history store" ref="workflowHistoryStores/main" param1="$(id)"/>      
        </workflowProvider>
      </database>
    </databases>
  </sitecore>
</configuration>

I checked the Workbox, and my custom workflow is now present. Everything now works as expected. So far I have tested this on Sitecore 7.2, and 8.0 and this appears to work with no side effects.

Just a note. I have not sorted the results, everything is displayed in the order processed. Therefore if you use the code above, you may want to perform a sort before you return the results.

Advertisements

My musing about anything and everything

Posted in Sitecore

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

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

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

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

Join 10 other followers

%d bloggers like this: