codeflood logo

Get your workflow in order

I really like workflow in Sitecore. It's simple but powerful. The workbox is a great view into workflow and anything that needs my attention. By default, the workbox displays items in workflow states ordered by name. At times it would be nice if I could order by some other attribute like the updated field. So let's add sorting functionality to the workbox.

We'll start by adding another ribbon chunk to hold the sorting controls. I want a drop list to change the attribute used to sort, and a checkbox to either sort ascending or descending on the selected attribute.

Create yourself a new project in .net to add code to.

Jump over into the core database and navigate to /sitecore/content/Applications/Workbox/Ribbon/Home. Here we want to create a new item based on the system/Ribbon/Chunk template and call it "Sort". Fill in the header and ID to reflect that this is the sort chunk. Create a child of the sort chunk item called "Field" based on the system/Ribbon/Panel template. This template allows us to specify a .net type which is used to create the controls of the panel. So in the project you created above, define a class to do this. It must inherit from Sitecore.Shell.Web.UI.WebControls.RibbonPanel. Enter this type in the "type" field of the "panel" item. Now I want to define the attributes that can be used for sorting. These will be children of the "field" item. But first I'll have to create a new template to base these items on.

Create a new "Field Option" template that contains 2 fields; Header and Field. The header will be displayed in the droplist control while the field will be used to sort the workbox on this selected field. Now go back and create the field options to sort on under the "field" item.

sortfields

Although these define a field to sort on, I also added an option to sort by name (the default) with a special token "@name". I also created sort options for the "__created", "__updated" and "priority" fields. Priority is a custom field for my implementation.

Now onto the UI.

I know I'm going to want some kind of persistent data storage to store our sorting options. And luckily Sitecore provides us with an option; the Sitecore.Web.UI.HtmlControls.Registry class. This class provides functions very much like the Windows Registry, where I can persistently store data (int, string, bool) for the user.

I'll make my life easier by creating a little Util class which will handle the storage and retrieval of the sorting options in the registry.

public static class Util
{
  public static string GetSortField()
  {
    return Registry.GetString("/Current_User/Workflow/Sort_Field",
      "@name");
  }

  public static void SetSortField(string field)
  {
    Registry.SetString("/Current_User/Workflow/Sort_Field", field);
  }

  public static bool GetSortOrder()
  {
    return Registry.GetBool("/Current_User/Workflow/Sort_Order", false);
  }

  public static void SetSortOrder(bool order)
  {
    Registry.SetBool("/Current_User/Workflow/Sort_Order", order);
  }
}

For our sort panel control, we need to override the "Render" method which is where we'll create our controls.

public override void Render(System.Web.UI.HtmlTextWriter output,
  Sitecore.Web.UI.WebControls.Ribbons.Ribbon ribbon,
  Sitecore.Data.Items.Item button,
  Sitecore.Shell.Framework.Commands.CommandContext context)
{
  output.Write("<div class=\"scRibbonToolbarPanel\">");
  output.Write("<table class=\"scWorkboxPageSize\"><tr><td
    class=\"scWorkboxPageSizeLabel\">");
  output.Write("Field:</td><td>");
  output.Write("<select id=\"SortField\"
    class=\"scWorkboxPageSizeCombobox\"
    onchange='javascript:scForm.invoke(\"SortChange\")'>");

  var currentSortField = Util.GetSortField();
  var descOrder = Util.GetSortOrder();

  ChildList fields = button.GetChildren();
  foreach (Item field in fields)
  {
    string val = field["field"];
    output.Write("<option value=\"" + val + "\" " +
      (val == currentSortField ? "selected" : string.Empty) + ">" + 
      field["header"] + "</option>");
  }

  output.Write("</select>");
  output.Write("</td></tr></table></div>");

  // Descending checkbox
  ribbon.BeginSmallButtons(output);
  var descButton = new SmallCheckButton();
  descButton.Header = "Descending";
  descButton.ID = "SortDesc";
  descButton.Checked = descOrder;
  descButton.Command = "OrderChange";
  ribbon.RenderSmallButton(output, descButton);
  ribbon.EndSmallButtons(output);
}

In the code above I first create a droplist control and add entries to it based on the items for field sorting we created earlier. When the selection is changed Sitecore will invoke a method on the form called SortChange which takes no parameters and returns void. Note I use the Util methods to get the current options from the registry so I can make sure the UI is in the correct state when it loads. Then I create a checkbox field which will invoke a method called OrderChange on the form class.

So now we need to extend the workbox form class to include these methods. Create a new class in your VS project which inherits from Sitecore.Shell.Applications.Workbox.WorkboxForm and add the two methods. Inside these methods I'll make calls to the Util methods to store the new sorting options.

protected void SortChange()
{
  var value =
    Sitecore.Context.ClientPage.ClientRequest.Form["SortField"];
  Util.SetSortField(value);
}

protected void OrderChange()
{
  bool current = Util.GetSortOrder();
  Util.SetSortOrder(!current);
}

The workbox application uses an XML layout, so open this file which should be found at \Website\sitecore\shell\Applications\Workbox\Workbox.xml. The only thing we need to change in this file is the code beside class which is defined in the CodeBeside tag. Change the type attribute of this tag to the type of the extended form we created above.

sort workbox scaled

So, now we can actually perform the sorting! Checking the methods available on the WorkboxForm class, there is a GetItems method which is used to retrieve the items to display in the form. Unfortunately, this method is private so we can't override it. What other options do we have to affect the sorting?

Having a poke around the API, the DataProvider class has a public virtual method called GetItemsInWorkflowState which we could override. But I don't like the idea of adjusting such a fundamental core piece just to affect sorting in an application. Not to mention that if I did extend that class I would actually have to extend all the concrete DataProvider implementations (DataProvider is abstract). That sounds like a lot of work.

Further up the stack sits the workflow itself which is implemented through the Sitecore.Workflows.Simple.Workflow class. This class has a public virtual method GetItems which is used to get the items in a particular workflow state. Perfect! We'll go with this.

But if I extend the Workflow class, how can I have Sitecore create my workflow instead of the standard one? This is the responsibility of the WorkflowProvider which is defined in the web.config file. So we can extend the WorkflowProvider to create instances of our custom workflow class instead of the default one.

There are 3 methods from which workflows can be returned, so we'll override those. I don't want to have to reimplement every one of these methods though. I want to leverage the functionality of the base class as much as possible. So I can have the base WorkflowProvider class create the standard workflow, and then wrap this workflow with my custom workflow. So rather than subclass, I'm instead going to use the Decorator design pattern to wrap a workflow and alter some of the behavior of the methods. One of the aspects of the Decorator pattern is that my class has the same interface as the class it's wrapping. Luckily the methods we're dealing with here only requires an object which implements the IWorkflow interface.

So let's start with implementing the IWorkflow interface. And as we'll be wrapping the other workflow, we may as well create a single constructor which accepts the workflow to wrap as a parameter. We'll store the workflow as a member variable and leverage it for all the other methods.

class SortingWorkflow : IWorkflow
{
  IWorkflow m_innerWorkflow = null;

  public SortingWorkflow(IWorkflow innerWorkflow)
  {
    m_innerWorkflow = innerWorkflow;
  }

  public Appearance Appearance
  {
    get { return m_innerWorkflow.Appearance; }
  }

  public WorkflowCommand[] GetCommands(string stateID)
  {
    return m_innerWorkflow.GetCommands(stateID);
  }

  public WorkflowCommand[] GetCommands(Sitecore.Data.Items.Item item)
  {
    return m_innerWorkflow.GetCommands(item);
  }

  // Wrap the other methods in the same way
}

The one method we want to alter is the GetItems method. This method can use the wrapped classes method to get the items, then it needs to reorder the array before returning it. We've already stored the sort options in the registry which we can get to from anywhere.

string m_sortField = "@name";
bool m_descSort = false;

public DataUri[] GetItems(string stateID)
{
  if (m_innerWorkflow != null)
  {
    var items = m_innerWorkflow.GetItems(stateID);
    m_sortField = Util.GetSortField();
    m_descSort = Util.GetSortOrder();
    Array.Sort(items, new Comparison<DataUri>(CompareDataUri));

    return items;
  }

  return new DataUri[]{};
}

private int CompareDataUri(DataUri x, DataUri y)
{
  Item itemX = Sitecore.Context.ContentDatabase.GetItem(x);
  Item itemY = Sitecore.Context.ContentDatabase.GetItem(y);

  var res = 0;
  if(m_sortField == "@name")
    res = string.Compare(itemX.Name, itemY.Name);
  else
    res = string.Compare(itemX[m_sortField], itemY[m_sortField]);

  if (m_descSort)
  {
    if (res > 0)
      return -1;
    if (res < 0)
      return 1;
  }

  return res;
}

I'm using the Array.Sort method here with an external comparison method. Note in the comparison method we check for the name token (@name) and treat that case a bit different.

Now we have the decorated workflow done we can override the methods in the workflow provider.

public class WorkflowProvider :
  Sitecore.Workflows.Simple.WorkflowProvider
{
  public WorkflowProvider(string dbName, HistoryStore store) :
    base(dbName, store) { }

  public override IWorkflow GetWorkflow(Sitecore.Data.Items.Item item)
  {
    return new SortingWorkflow(base.GetWorkflow(item));
  }

  public override IWorkflow GetWorkflow(string workflowID)
  {
    return new SortingWorkflow(base.GetWorkflow(workflowID));
  }

  public override IWorkflow[] GetWorkflows()
  {
    var workflows = base.GetWorkflows();
    var toRet = new IWorkflow[ workflows.Length ];
    for (int i = 0; i < workflows.Length; i++)
      toRet[i] = new SortingWorkflow(workflows[i]);
    return toRet;
  }
}

Now the last thing to do is update the configuration to have Sitecore use the new sorting workflow provider. Open the web.config file and find the master database definition and the workflowProvider inside that. Change the type to the sorting workflow provider created above.

And there we have it. We've now added sorting functionality to the workbox.

That was a lot of code and changes to make that happen. So consider this an official call out to the Sitecore developers to PLEASE change the workbox form class methods to protected virtual.

Comments

Wow, this is both a hardcore and useful customization, well done!
On private methods: I hear you, others do as well. However I was fascinated about using custom workflow to customize sorting. Cool!

Lars Nielsen

As always, your posts goes beyond expectation.
Yours, and a few others blog posts are an absolute _must read._
Alexey, - an idea to the product roadmap?

Greg

Really great article, thank you so much for posting this. It saved us a tremendous amount of time!

Leave a comment

All fields are required.