You are on page 1of 45

All about customizing

workflow in Sitecore
DEVELOPER’S REFERENCE GUIDE

By Surendra Sharma
TECHNICAL ARCHITECT | HTTP://SURENDRASHARMADOTNET.BLOGSPOT.IN/

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
In almost all the Sitecore projects, developers have to create, sometimes customize and
need to apply workflow to items. This guide covers all the possible scenarios and
customization technic with code used for workflow. It includes what is workflow, how to
create custom workflow, customizing comment window, customizing workbox and
customizing items in workbox.
What is workflow?
According to Sitecore “Workflows ensure that items move through a predefined set of states
before they become publishable, usually intended to ensure that content receives the
appropriate reviews and approvals before publication to the live Web site.”
All workflow are resides in “/sitecore/system/Workflows/” folder.
A workflow have states, commands and actions.
Every workflow start with draft state and end with publish or final state.
Most of the workflow looks indicated in below diagram

Let’s deep dive into understanding Sitecore workflow with one real life requirement and its
implementation.
Requirement:
Client will need the following roles:
Editor – Can edit content but cannot publish content.
Reviewer – Can edit and publish content
An editor needs to be able to assign content to an individual reviewer to review and
publish.
Below is a typical scenario:
Editor A creates/updates to an item for example product item. Editor A saves the
item content then assigns it to Reviewer A to review and publish.

1|Page

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
Reviewer A gets an email that Editor A wants a product reviewed and published.
Reviewer A reviews the product detail and publishes it.
Other reviewers can only view the items those are waiting state for approval but
can’t take any action on items however admin can take any action along with
Reviewer A.

The scenario above will be the most common scenario for reviewing and publishing
content.
For a multilingual website, client need language groups with scenario:
Editor X in Germany makes an update to a German-language product. Editor X saves
the content but isn't sure what individual reviewer to assign it to. Editor X thus
assigns it to the German Review Group to review and publish. The German Review
Group includes several members of the German marketing team (Reviewer X,
Reviewer Y and Reviewer Z).

2|Page

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

Reviewer X, Reviewer Y and Reviewer Z each get an email that a piece of content is
available for review. Reviewer Z agrees to review the content and publish it.

3|Page

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

Solution:
We will implement this requirement with below steps
1. Create Editor and Reviewer Roles
2. Assign permissions on items to roles
3. Create editor and reviewer users
4. Map users with roles
5. Create individual/group reviewer items in content tree
6. Create Custom comment window
7. Apply validation rules on comment windows field
8. Create Custom workflow
9. Apply custom workflow on items
10. Customizing workbox
11. Customizing items comments in Awaiting Approval section
12. Test all the scenarios

So let’s start and cover each step one by one.

4|Page

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

1. Create Editor and Reviewer Roles
We will create two role in Sitecore namely – Editor Group and Reviewer Group
Logged in into Sitecore desktop and select Security Tools -> Role Manager

Create two new role - Editor Group, Reviewer Group
Assign editor role to Editor Group as

5|Page

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
Assign reviewer role to Reviewer Group. Technically Reviewer is one who have
editor as well as publishing permissions. So instead of assigning individual roles to
Reviewer group you can directly assign roles as
Reviewer = Editor Group Role + Sitecore Client Publishing

6|Page

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

2. Assign permissions on items to roles
Assign permissions to these newly created role on content item.
First assign permissions to Editor Group Role. Note that we are NOT providing
Delete Descendants permission to this role.

Similarly assign permissions to Reviewer Group Role. Note that we are providing
Delete Descendants as well as Administer level permission to this role.

Tips: - If you are not assigning item access permission to Roles/Users, then that
user will not able to see workflow pending items in workbox.

7|Page

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

3. Create editor and reviewer users
Once we have created roles, now it’s time to create users under these roles.
Logged in into Sitecore desktop and select Security Tools -> User Manager

Create some new users who play part of editors and reviewers.
For example, we created 4 users like
 Editor1
 Editor2
 Reviewer1
 Reviewer2

8|Page

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

4. Map users with roles
Assign user Editor1 and Editor2 to Editor Group.

Similarly assign user Reviewer 1 and Reviewer 2 in Reviewer Group.

9|Page

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

5. Create custom multilist for showing all user who are reviewers and admin
We need to show all admin and reviewer into multilist. However Sitecore don’t have
any way to show all these user (more specifically all Reviewer). So we have to create
custom multilist control and fill all the reviewers and admin.
Steps to create custom multilist
 Select CORE database in Sitecore desktop mode
 Add new item as “Custom Reviewer” in “/sitecore/system/Field types/List
Types/” as below


Fill entry “CustomReviewerList:ReviewerMultilist” in Control field as
prefix:classname
where CustomReviewerList is prefix in CustomReviewerList.config file and
ReviewerMultilist is class name for Multilist in below code file.
Add reference of DLL “Sitecore.Client” in your Visual Studio web project
Create one “CustomReviewerList.config” file in
“Website\App_Config\Include” folder as

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<controlSources>
<source mode="on" namespace="SitecoreWebApp.UI" assembly="SitecoreWebApp"
prefix="CustomReviewerList"/>
</controlSources>
<settings>
<!-- Parameter name for Sitecore domain and role -->
<setting name="UsersField.DomainParameterName" value="sitecore" />
<setting name="UsersField.RoleParameterName" value="sitecore\Reviewer Group" />

10 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
</settings>
</sitecore>
</configuration>

Add a class in your UI solution and copy-paste below code for creating custom
multilist which get Reviewer Group role and domain name from config file and
get all users under reviewer role and admin

using
using
using
using
using
using
using
using
using
using
using
using
using

System;
System.Collections;
System.Collections.Generic;
System.Linq;
Sitecore.Text;
Sitecore.Security.Accounts;
System.Web.Security;
Sitecore.Configuration;
Sitecore.Diagnostics;
System.Web.UI;
Sitecore.Resources;
Sitecore.Globalization;
Sitecore.Shell.Applications.ContentEditor;

namespace SitecoreWebApp.UI
{
public interface IUsersField
{
IEnumerable<User> GetSelectedUsers();
IEnumerable<User> GetUnselectedUsers();
string GetProviderUserKey(User user);
}
/// <summary>
/// Class to get admin and user from reviewer group
/// </summary>
public class UsersField : IUsersField
{
private static readonly string DomainParameterName =
Settings.GetSetting("UsersField.DomainParameterName");
private static readonly string RoleParameterName =
Settings.GetSetting("UsersField.RoleParameterName");
private ListString _SelectedUsers;
private ListString SelectedUsers
{
get
{
if (_SelectedUsers == null)
{
_SelectedUsers = new ListString(Value);
}
return _SelectedUsers;
}
}

11 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
private IEnumerable<User> _UsersInDomain;
private IEnumerable<User> UsersInDomain
{
get
{
if (_UsersInDomain == null)
{
_UsersInDomain = GetUsersInDomain();
}
return _UsersInDomain;
}
}
private IEnumerable<User> _Users;
private IEnumerable<User> Users
{
get
{
if (_Users == null)
{
_Users = GetUsers();
}
return _Users;
}
}
private string _Domain;
private string Domain
{
get
{
if (string.IsNullOrEmpty(_Domain))
{
_Domain = FieldSettings[DomainParameterName];
}
return _Domain;
}
}
private UrlString _FieldSettings;
private UrlString FieldSettings
{
get
{
if (_FieldSettings == null)
{
_FieldSettings = GetFieldSettings();
}
return _FieldSettings;
}
}
private string Source { get; set; }
private string Value { get; set; }

12 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

private UsersField(string source, string value)
{
SetSource(source);
SetValue(value);
}
private void SetSource(string source)
{
Source = source;
}
private void SetValue(string value)
{
Value = value;
}
private IEnumerable<User> GetUsersInDomain()
{
if (!string.IsNullOrEmpty(Domain))
{
return Users.Where(user => IsUserInDomain(user, Domain));
}
return Users;
}
/// <summary>
/// Get all user who are in Reviewer group or admin
/// </summary>
/// <returns></returns>
private static IEnumerable<User> GetUsers()
{
IEnumerable<User> users = UserManager.GetUsers();
var filteredUser = new List<Sitecore.Security.Accounts.User>();
if (users != null)
{
foreach (var tempUser in users)
{
if (tempUser.IsAdministrator)
{
//User is admin
filteredUser.Add(tempUser);
}
else
{
if (!string.IsNullOrEmpty(RoleParameterName))
{
foreach (string roleName in
RoleParameterName.Split(new char[] { ',' }))
{
if (tempUser.IsInRole(roleName))
{
//User is from reviewer group
filteredUser.Add(tempUser);
}

13 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
}
}
}
}
return filteredUser;
}
return new List<User>();
}
private static bool IsUserInDomain(User user, string domain)
{
Assert.ArgumentNotNull(user, "user");
Assert.ArgumentNotNullOrEmpty(domain, "domain");
string userNameLowerCase = user.Profile.UserName.ToLower();
string domainLowerCase = domain.ToLower();
return userNameLowerCase.StartsWith(domainLowerCase);
}
private UrlString GetFieldSettings()
{
try
{
if (!string.IsNullOrEmpty(Source))
{
return new UrlString(Source);
}
}
catch (Exception ex)
{
Log.Error(this.ToString(), ex, this);
}
return new UrlString();
}
public IEnumerable<User> GetSelectedUsers()
{
IList<User> selectedUsers = new List<User>();
foreach (string providerUserKey in SelectedUsers)
{
User selectedUser = UsersInDomain.Where(user =>
GetProviderUserKey(user) == providerUserKey).FirstOrDefault();
if (selectedUser != null)
{
selectedUsers.Add(selectedUser);
}
}
return selectedUsers;
}
public IEnumerable<User> GetUnselectedUsers()
{

14 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
IList<User> unselectedUsers = new List<User>();
foreach (User user in UsersInDomain)
{
if (!IsUserSelected(user))
{
unselectedUsers.Add(user);
}
}
return unselectedUsers;
}
private bool IsUserSelected(User user)
{
string providerUserKey = GetProviderUserKey(user);
return IsUserSelected(providerUserKey);
}
private bool IsUserSelected(string providerUserKey)
{
return SelectedUsers.IndexOf(providerUserKey) > -1;
}
public string GetProviderUserKey(User user)
{
Assert.ArgumentNotNull(user, "user");
MembershipUser membershipUser =
Membership.GetUser(user.Profile.UserName);
return membershipUser.ProviderUserKey.ToString();
}
public static IUsersField CreateNewUsersField(string source, string value)
{
return new UsersField(source, value);
}
}
/// <summary>
/// Render Multi List which showing all user who are in Reviewer group or
admin
/// </summary>
public class ReviewerMultilist : MultilistEx
{
private IUsersField _UsersField;
private IUsersField UsersField
{
get
{
if (_UsersField == null)
{
_UsersField = CreateNewUsersField();
}
return _UsersField;
}
}

15 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
public ReviewerMultilist()
{
}
protected override void DoRender(HtmlTextWriter output)
{
Assert.ArgumentNotNull(output, "output");
SetIDProperty();
string disabledAttribute = string.Empty;
if (ReadOnly)
{
disabledAttribute = " disabled=\"disabled\"";
}
output.Write(string.Format("<input id=\"{0}_Value\" type=\"hidden\"
value=\"{1}\" />", ID, Sitecore.StringUtil.EscapeQuote(Value)));
output.Write(string.Format("<table{0}>", GetControlAttributes()));
output.Write("<tr>");
output.Write(string.Format("<td
class=\"scContentControlMultilistCaption\" width=\"50%\">{0}</td>",
GetAllLabel()));
output.Write(string.Format("<td width=\"20\">{0}</td>",
Images.GetSpacer(20, 1)));
output.Write(string.Format("<td
class=\"scContentControlMultilistCaption\" width=\"50%\">{0}</td>",
GetSelectedLabel()));
output.Write(string.Format("<td width=\"20\">{0}</td>",
Images.GetSpacer(20, 1), "</td>"));
output.Write("</tr>");
output.Write("<tr>");
output.Write("<td valign=\"top\" height=\"100%\">");
output.Write(string.Format("<select id=\"{0}_unselected\"
class=\"scContentControlMultilistBox\" multiple=\"multiple\"{1} size=\"10\"
ondblclick=\"javascript:scContent.multilistMoveRight('{2}')\"
onchange=\"javascript:document.getElementById('{3}_all_help').innerHTML=this.selec
tedIndex>=0?this.options[this.selectedIndex].innerHTML:''\">", ID,
disabledAttribute, ID, ID));
IEnumerable<User> unselectedUsers = GetUnselectedUsers();
foreach (User unselectedUser in unselectedUsers)
{
output.Write(string.Format("<option value=\"{0}\">{1}</option>",
GetProviderUserKey(unselectedUser), unselectedUser.Profile.UserName));
}
output.Write("</select>");
output.Write("</td>");
output.Write("<td valign=\"top\">");
RenderButton(output, "Core/16x16/arrow_blue_right.png",
string.Format("javascript:scContent.multilistMoveRight('{0}')", ID));
output.Write("<br />");
RenderButton(output, "Core/16x16/arrow_blue_left.png",
string.Format("javascript:scContent.multilistMoveLeft('{0}')", ID));
output.Write("</td>");
output.Write("<td valign=\"top\" height=\"100%\">");

16 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
output.Write(string.Format("<select id=\"{0}_selected\"
class=\"scContentControlMultilistBox\" multiple=\"multiple\"{1} size=\"10\"
ondblclick=\"javascript:scContent.multilistMoveLeft('{2}')\"
onchange=\"javascript:document.getElementById('{3}_selected_help').innerHTML=this.
selectedIndex>=0?this.options[this.selectedIndex].innerHTML:''\">", ID,
disabledAttribute, ID, ID));
IEnumerable<User> selectedUsers = GetSelectedUsers();
foreach (User selectedUser in selectedUsers)
{
output.Write(string.Format("<option value=\"{0}\">{1}</option>",
GetProviderUserKey(selectedUser), selectedUser.Profile.UserName));
}
output.Write("</select>");
output.Write("</td>");
output.Write("<td valign=\"top\">");
RenderButton(output, "Core/16x16/arrow_blue_up.png",
string.Format("javascript:scContent.multilistMoveUp('{0}')", ID));
output.Write("<br />");
RenderButton(output, "Core/16x16/arrow_blue_down.png",
string.Format("javascript:scContent.multilistMoveDown('{0}')", ID));
output.Write("</td>");
output.Write("</tr>");
output.Write("<tr>");
output.Write("<td valign=\"top\">");
output.Write(string.Format("<div style=\"border:1px solid
#999999;font:8pt tahoma;padding:2px;margin:4px 0px 4px 0px;height:14px\"
id=\"{0}_all_help\"></div>", ID));
output.Write("</td>");
output.Write("<td></td>");
output.Write("<td valign=\"top\">");
output.Write(string.Format("<div style=\"border:1px solid
#999999;font:8pt tahoma;padding:2px;margin:4px 0px 4px 0px;height:14px\"
id=\"{0}_selected_help\"></div>", ID));
output.Write("</td>");
output.Write("<td></td>");
output.Write("</tr>");
output.Write("</table>");
}
protected void SetIDProperty()
{
ServerProperties["ID"] = ID;
}
protected static string GetAllLabel()
{
return GetLabel("All");
}
protected static string GetSelectedLabel()
{
return GetLabel("Selected");
}
protected static string GetLabel(string key)
{

17 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
return Translate.Text(key);
}
protected IEnumerable<User> GetSelectedUsers()
{
return UsersField.GetSelectedUsers();
}
protected IEnumerable<User> GetUnselectedUsers()
{
return UsersField.GetUnselectedUsers();
}
protected string GetProviderUserKey(User user)
{
return UsersField.GetProviderUserKey(user);
}
/// <summary>
/// Method "borrowed" from MultilistEx control
/// </summary>
/// <param name="output"></param>
/// <param name="icon"></param>
/// <param name="click"></param>
protected void RenderButton(HtmlTextWriter output, string icon, string
click)
{
Assert.ArgumentNotNull(output, "output");
Assert.ArgumentNotNull(icon, "icon");
Assert.ArgumentNotNull(click, "click");
ImageBuilder builder = new ImageBuilder
{
Src = icon,
Width = 0x10,
Height = 0x10,
Margin = "2px"
};
if (!ReadOnly)
{
builder.OnClick = click;
}
output.Write(builder.ToString());
}
private IUsersField CreateNewUsersField()
{
return SitecoreWebApp.UI.UsersField.CreateNewUsersField(Source,
Value);
}
}
}

18 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

6. Create individual/group reviewer items in content tree

Select master database.

Create “Reviewer” Templates with two fields – Name and Reviewer. Note
Reviewer field is of “Custom Reviewer” type.

Now we have to create individual and group reviewers items in content tree.

For this, create folder “Content Reviewers” in content tree and restrict insert
option for “Reviewer” template items only

Add some individual reviewer item as

Note we are getting all user who are reviewer and admin in our custom multi list
for this newly added item.

For creating Reviewer Group, select more than one reviewer from multilist as

19 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

We created 2 individual reviewer and one group reviewer in this step.

20 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

7. Create Custom comment window
When editor submit some item changes to reviewer, at that moment we need to
show new Comment window with comment multiline textbox and preferred Reviewer
dropdown.
So for creating new comment window, we need to add new item “Reviewer
Comment Template” in “/sitecore/templates/System/Workflow” folder as

As shown in above image, we have standard Comments multiline textbox and newly
added field “Preferred Reviewer” which is of droplist type which pointed to all
reviewer from content tree folder “Content Reviewers”.

21 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

8. Apply validation rules on comment windows field
We need to warn editor if he is not selecting any reviewer while submitting item for
review. For this we have to apply validation rule to our “Preferred Reviewer” field.

Select “/sitecore/templates/System/Workflow/Reviewer Comment
Template/Comment Section/Preferred Reviewer” field
Select “Required” option in Quick Action Bar, Validate Button and Validator Bar
field as shown in below image

22 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

9. Create Custom workflow
We will create new workflow “Reviewer Workflow” for this example as below

Note instead of our new template in “Default Comment Template” field, we are
still using Standard Comment Template as new template should only use when editor
submit item to Reviewer.
Draft – This is first stage of all workflow. This state remain blank as below

23 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
Submit
This is the main step where we need to modify existing functionality as

Note in “Comment Template” field, we now using Reviewer Comment Template
Also increase size of Comment Dialog Height by 350.
In Appearance Evaluator Type field, I want to customize the further action which I
am writing in SitecoreWebApp.UI.CommandAppearanceEvaluator, SitecoreWebApp
using
using
using
using
using

System.Linq;
Sitecore.Workflows;
System.Web.Security;
Sitecore.Data;
Sitecore.Data.Fields;

namespace SitecoreWebApp.UI
{
public class CommandAppearanceEvaluator : BasicWorkflowCommandAppearanceEvaluator
{
public override WorkflowCommandState EvaluateState(Sitecore.Data.Items.Item item,
Sitecore.Data.Items.Item workflowCommand)
{
if (Sitecore.Context.User.IsAdministrator)
{
return WorkflowCommandState.Visible;
}
if (item != null) //the item might be null!

24 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
{
var we =
Sitecore.Context.Workflow.GetWorkflow(item).GetHistory(item).OrderByDescending(i =>
i.Date).First();
if (we.CommentFields != null && we.CommentFields.Count > 1)
{
var preferredReviewer = we.CommentFields["Preferred Reviewer"];
if (!string.IsNullOrEmpty(preferredReviewer))
{
var reviewerFolderItem =
Database.GetDatabase("master").GetItem(new ID("{E13D8303-4514-4DE7-A11B-05E326EFE01D}"));
var reviewerItem = reviewerFolderItem.Children.Where(p =>
p.Name.ToLower() == preferredReviewer.ToLower()).FirstOrDefault();
if (reviewerItem != null)
{
var reviewerList =
(MultilistField)reviewerItem.Fields["Reviewer"];
bool canHide = true;
if (reviewerList.TargetIDs != null)
{
//reviewerList.List
string s = reviewerList.Items[0];
var membershipUser =
Membership.GetUser(Sitecore.Context.User.Name);
string providerUserKey =
membershipUser.ProviderUserKey.ToString();
foreach (var valServId in reviewerList.TargetIDs)
{
string id = valServId.ToString();
if
(id.ToString().ToLower().Contains(providerUserKey.ToLower()))
{
return WorkflowCommandState.Visible;
}
else
{
canHide = false;
}
}
}
if (!canHide)
{
return WorkflowCommandState.Hidden;
}
}
}
else
{
return WorkflowCommandState.Hidden;
}
}

25 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
} // End of item!=null
return base.EvaluateState(item, workflowCommand);
}
}
}

Validation For Reviewer
Here I am selecting “Max Result Allowed” as “FatalError” and just put error
messages in other fields.

Email To Reviewer
We want to send mail to Reviewer with message “There is an item for your approval
in your workbox & the item path is $itemPath$ in language $itemLanguage$ and
version $itemVersion$.”
Where $itemPath$, $itemLanguage$ and $itemVersion$ should be replaced by their
value in code specified in “Type” field as

26 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

Include below code for sending mail functionality
using
using
using
using
using
using
using
using
using
using
using
using
using
using

System;
System.Collections;
System.Collections.Generic;
System.Linq;
Sitecore.Data.Items;
Sitecore.Security.Accounts;
System.Web.Security;
Sitecore.Diagnostics;
Sitecore.Data;
Sitecore.Data.Fields;
Sitecore.Workflows.Simple;
System.Net.Mail;
System.Net;
Sitecore.Configuration;

namespace SitecoreWebApp.UI
{
public class EmailAction
{
// Methods
private string GetText(Item commandItem, string field, WorkflowPipelineArgs args)
{
string text = commandItem[field];
if (text.Length > 0)
{
return this.ReplaceVariables(text, args);
}
return string.Empty;
}
public void Process(WorkflowPipelineArgs args)
{
Assert.ArgumentNotNull(args, "args");

27 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
ProcessorItem processorItem = args.ProcessorItem;
if (processorItem != null)
{
Item innerItem = processorItem.InnerItem;
string fullPath = innerItem.Paths.FullPath;
string from = this.GetText(innerItem, "from", args);
string to = this.GetText(innerItem, "to", args);
//sitecore/system/Workflows/Reviewer Workflow/Draft/Submit/Email To
Reviewer
if (innerItem.ID.ToString().ToLower().Equals("{83ffff97-673c-444d-aec8c57acb871f0e}")) //Send mail to reviewer
{
to = GetReviewerMail(args.CommentFields);
}
else if (innerItem.ID.ToString().ToLower().Equals("{510cd88a-9749-4fc2b15b-1a6e935a2650}")
|| innerItem.ID.ToString().ToLower().Equals("{8D8EC9D2-C1EE-4026AD8F-BA16C73B65AB}")) //Send mail to Editor
{
//sitecore/system/Workflows/Reviewer Workflow/Awaiting
Approval/Reject/Email To Editor
//sitecore/system/Workflows/Reviewer Workflow/Awaiting
Approval/Approve/Email To Editor
to = GetEditorEmail(args.DataItem);
to += to.Contains(";") ? "" : ";";
}
if (!string.IsNullOrEmpty(to))
{
try
{

//Network Credentials
string host = Settings.GetSetting("MailServer");
var smtpClient = new SmtpClient(host);
smtpClient.Credentials = new
NetworkCredential(Settings.GetSetting("MailServerUserName"),
Settings.GetSetting("MailServerPassword"));
int port;
smtpClient.Port =
int.TryParse(Settings.GetSetting("MailServerPort"), out port) ? port : 25;
string mailSubject = this.GetText(innerItem, "subject", args);
string mailMessage = this.GetText(innerItem, "message", args);
Error.Assert(to.Length > 0, "The 'To' field is not specified in
the mail action item: " + fullPath);
Error.Assert(from.Length > 0, "The 'From' field is not specified
in the mail action item: " + fullPath);
Error.Assert(mailSubject.Length > 0, "The 'Subject' field is not
specified in the mail action item: " + fullPath);
Error.Assert(host.Length > 0, "The 'Mail server' field is not
specified in the mail action item: " + fullPath);
foreach (string tempTo in to.Split(new char[] { ';' }))

28 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
{
if (!string.IsNullOrEmpty(tempTo))
{
MailMessage message = new MailMessage(from, tempTo)
{
Subject = mailSubject,
Body = mailMessage
};
try
{
//Send mail
smtpClient.Send(message);
}
catch (Exception ex)
{
string err = ex.Message;
}
}
}
}
catch (Exception ex)
{
string err = ex.Message;
}
}
}
}
private string ReplaceVariables(string text, WorkflowPipelineArgs args)
{
text = text.Replace("$itemPath$", args.DataItem.Paths.FullPath);
text = text.Replace("$itemLanguage$", args.DataItem.Language.ToString());
text = text.Replace("$itemVersion$", args.DataItem.Version.ToString());
return text;
}
public string GetEditorEmail(Item item)
{
if (item == null)
return string.Empty;
if (item["__Updated by"] != null)
{
string userName = item["__Updated by"];
User user = User.FromName(userName, false);
if (user != null)
{
return user.Profile.Email;
}
}
return string.Empty;
}

29 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
private string GetReviewerMail(Sitecore.Collections.StringDictionary
CommentFields)
{
string reviewers = string.Empty;
if (CommentFields != null && CommentFields.Count > 1)
{
var preferredReviewer = CommentFields["Preferred Reviewer"];
if (!string.IsNullOrEmpty(preferredReviewer))
{
//Point to folder - /sitecore/content/Global Content/Reviewers
var reviewerFolderItem = Database.GetDatabase("master").GetItem(new
ID("{89058D11-56DB-48BC-87D3-F053B95FC7B8}"));
var reviewerItem = reviewerFolderItem.Children.Where(p =>
p.Name.ToLower() == preferredReviewer.ToLower()).FirstOrDefault();
if (reviewerItem != null)
{
var reviewerList =
(MultilistField)reviewerItem.Fields["Reviewer"];
if (reviewerList.TargetIDs != null)
{
IEnumerable<User> users = UserManager.GetUsers();
if (users != null)
{
foreach (var tempUser in users)
{
if (!string.IsNullOrEmpty(tempUser.Profile.Email) &&
!string.IsNullOrEmpty(tempUser.Identity.Name))
{
var membershipUser =
Membership.GetUser(tempUser.Identity.Name);
if (membershipUser != null)
{
string providerUserKey =
membershipUser.ProviderUserKey.ToString();
if (providerUserKey != null)
{
foreach (var valServId in
reviewerList.TargetIDs)
{
if
(Convert.ToString(valServId).ToLower().Contains(providerUserKey.ToLower()))
{
reviewers +=
tempUser.Profile.Email + ";";
}
}
}
}
}

30 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
}
}
}
}
}
}
return string.IsNullOrEmpty(reviewers) ? string.Empty :
reviewers.Substring(0, reviewers.Length - 1);
}
}
}

Awaiting Approval
This state remain blank like draft state.
Approve
Reviewer takes the action on item and approving for Publish.
In “Appearance Evaluator Type” field, I want to customize the same action as
mentioned in Submit Command code.

Email To Editor
Send approval mail message to editor. Functionality and setting is same as described
in “Email To Reviewer” section.
Reject
Reviewer takes the action on item and rejecting the item for some
correction/improvements. This step again send item for draft stage.
In “Appearance Evaluator Type” field, I want to customize the same action as
mentioned in Submit Command code.

31 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
Email To Editor
Send rejection mail message to editor. Functionality and setting is same as described
in “Email To Reviewer” section.
Approved
Set workflow state to Final. This is the last state of any workflow.
Auto Publish
This action publish the item. If Parameters field is set as “deep=1” which indicate
publish item and its children while “deep=0” mean publish only the item.

32 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

10. Apply custom workflow on items
Now we have create roles, users, assigned user to role, created reviewer, created
custom comment window and custom workflow. Now its time to apply this workflow
to content tree items.
Best way of applying workflow is on template’s standard value where one need to set
Default Workflow field to the name of workflow as follows

33 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

11. Submit to Reviewer for approval
Once workflow is assigned. Login as an editor and create some items and submit to
reviewer for review.
Editor can submit item to single reviewer. A reviewer should receive a review request
mail for this submitted item.

Editor can also submit item to group reviewer. All reviewer within a group should
receive a review request mail.

34 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

If editor will not select any reviewer, a warning message should display. Editor can
still submit the item which finally review only by reviewer who is admin.

35 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

12. Customizing workbox
We have to restrict reviewer to approve or reject all items at a time. So we want to
disabled Approve All, Reject All, Approve Selected and Reject Selected button
in workbox. However all these buttons are enabled only for Admin.
Copy “Website\sitecore\shell\Applications\Workbox\workbox.xml” file and
paste it into “Website\sitecore\shell\Override” folder.
Open this “Website\sitecore\shell\Override\Workbox.xml” and find below line
<CodeBeside
Type="Sitecore.Shell.Applications.Workbox.WorkboxForm,Sitecore.Client"/>
Replace this line with your Namespace.ClassName, AssemblyName as
<CodeBeside Type="SitecoreWebApp.UI.ReviewerWorkbox,SitecoreWebApp"/>
Include below code for disabling Approve All, Reject All, Approve Selected and
Reject Selected button in workbox
using Sitecore.Shell.Applications.Workbox;
namespace SitecoreWebApp.UI
{
public class ReviewerWorkbox : WorkboxForm
{
protected override void DisplayState(global::Sitecore.Workflows.IWorkflow
workflow, global::Sitecore.Workflows.WorkflowState state, global::Sitecore.Data.DataUri[]
items, System.Web.UI.Control control, int offset, int pageSize)
{
base.DisplayState(workflow, state, items, control, offset, pageSize);
if (!Sitecore.Context.User.IsAdministrator)
{
if (control.Controls.Count > 0)
{
var borderControls = control.Controls[control.Controls.Count 1].Controls;
for (var i = 0; i < borderControls.Count; i++)
{
borderControls[i].Visible = false;
}
}
}
}
}
}

36 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
Check below image for workbox action button are only available for Admin.

37 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

13. Customizing items comments in Awaiting Approval section
Along with comments, Reviewers wants Preferred Reviewer name and Item Path
in comments section.
Tips: - If you are unable to see pending items in workbox, assign access permission
to Roles/Users as explained in step 2 Assign permissions on items to roles.

For this, add below “CustomWorkflowField.config” config file
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
xmlns:set="http://www.sitecore.net/xmlconfig/set/">
<sitecore>
<pipelines>
<getWorkflowCommentsDisplay>
<processor type="Sitecore.Pipelines.GetWorkflowCommentsDisplay.ExtractFields,
Sitecore.Kernel">
<Fields hint="list:AddField">
<Reviewer>Preferred Reviewer</Reviewer>
</Fields>
</processor>
</getWorkflowCommentsDisplay>
</pipelines>
</sitecore>
</configuration>

38 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
Include below code for customizing comments in workflow
using Sitecore.Pipelines.GetWorkflowCommentsDisplay;
namespace SitecoreWebApp.UI
{
public class CommentsFieldExtractor
{
public void Process(GetWorkflowCommentsDisplayArgs args)
{
if (args.ProcessorItem != null)
{
var actionsRaw = args.WorkflowEvent.CommentFields["Preferred Reviewer"];
if (string.IsNullOrEmpty(actionsRaw))
{
return;
}
//Add item path in existing comment
args.Comment += " <BR> Item Path: " + args.Item.Paths.FullPath;
}
}
}
}

39 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

14. How to apply workflow on existing multiple items
Use below program to apply workflow on all the children of particular item.
Here is a code to do it. Just put Home item ID or start item ID
var FirstItem = Sitecore.Context.Database.GetItem(new ID("{A00A11B6-E6DB-45AB-8B54636FEC3B5523}")) ;
ApplyWorkflow(FirstItem , 1);

Rest of the item should be iterate by following recursive function and apply workflow on all
the items except folder item.
private Item ApplyWorkflow(Item mainItem, int icounter)
{
try
{
if (!mainItem.DisplayName.Equals("Homepage"))
{
//Put Workflow ID here
var workflow =
Factory.GetDatabase("master").WorkflowProvider.GetWorkflow("{7E9BC450-18EE-401E-892A1CEF27BF8D9B}");
workflow.Start(mainItem);
}
}
catch
{
}
if (mainItem.HasChildren)
{
icounter++;
foreach (var myInnerItem in mainItem.Children.ToList())
{
ApplyWorkflow(myInnerItem, icounter);
}
}
icounter--;
return null;
}

40 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

How to test all the scenarios
We have to test our workflow and note the finding. For this we need to create different
scenarios from testing point of view.
Create some editors, reviewers and group reviewers as
Editors:
Editor 1, Editor 2, Editor 3
Reviewer:
Reviewer 1, Reviewer 2
Reviewer Group:
Reviewer G1 (2 nos - G1 R1, G1 R2)
Reviewer G2 (2 nos - G2 R1, G2 R2)

Scenario 1: Editor edits any item and submits to selected reviewer
User Action:
Editor 1 - Item 1 version 1 submits for approval to Reviewer 2
Editor 1 - Item 3 version 1 submits - Reviewer G1
Expected Outcome:
1. Workbox of those reviewer (group) will show the link to approve that version of item
to whom it is assigned to.
2. Email notification triggered to the reviewer/ Reviewer group on item submit
3. Email notification triggered to the editor once item is approved and that approved
version of item is published and live

Scenario 2: Editor edits any item and submits for approval without any reviewer selection
User Action:
Editor 1 Item 2 version 1 submits - no one
Expected Outcome:
1. Only users having Admin access will be able to approve this version of item and no
one else.
2. Email notification triggered to the editor once item is approved and that approved
version of item is published and live

41 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
Scenario 3: Same item being edited by multiple editors and submitted for approval by each
editor
User Action:
Editor 2 - Item 1 Version 2 submit - Reviewer 1
Editor 3 Item 1 Version 3 submit - Reviewer G2
Reviewer 1 Item 1 edit and approve
Expected Outcome:
1. Workbox of those reviewer (group) will show the link to approve those versions of
item to whom it is assigned to.
2. Email notification triggered to the reviewer/ Reviewer group on item submit
3. Email notification triggered to the editor once item is approved and that approved
version of item is published and live
4. Only the latest approved version of item is published and will live.

Scenario 4: Item submitted to approver Group and multiple approver in Group try to
open/approve that item
User Action:
Reviewer G1 - Item 3 - opened by multiple reviewer of G1.
G1 R1 - Opens, edit then approve
G1 R2 - Opens, approve

Expected Outcome:
1. Workbox of those reviewer (group) will show the link to approve those versions of
item to whom it is assigned to.
2. Email notification triggered to the reviewer/ Reviewer group on item submit
3. Email notification triggered to the editor once item is approved and that approved
version of item is published and live
4. The Approval link/ button can be submitted once by the reviewer who clicked that
first. On refresh the workbox of the other user will not display the item event if he is
about to submit that.

42 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE
Scenario 5: Item send for approval by a reviewer is rejected.
User Action:
Editor 2 - Item 2 version 2 send for approval to Reviewer 1. Reviewer 1 Rejects
Expected Outcome:
1. Workbox of those reviewer (group) will show the link to approve that version of item
to whom it is assigned to.
2. Email notification triggered to the reviewer/ Reviewer group on item submit
3. Email notification triggered to the editor once item is rejected by the approver.

Scenario 6: Item submitted to approver Group and multiple approver in Group try to do
different action for that item
User Action:
Item 3 version 2 submitted for approval by Editor 2 to Reviewer G 1
G1 R1 - Opens, edit then approve
G1 R2 - Rejects
Expected Outcome:
1. Workbox of those reviewer (group) will show the link to approve those versions of
item to whom it is assigned to.
2. Email notification triggered to the reviewer/ Reviewer group on item submit.
3. Approve or Rejection by the approver depends on who clicks the link first and other
approvers browsers will be refreshed on page refresh.
Email notification triggered to the editor once item is approved/Rejected and that approved
version of item is published and live.

43 | P a g e

ALL ABOUT CUSTOMIZING WORKFLOW IN SITECORE

References


http://blog.rauljimenez.co.uk/a-comment-on-comments/
https://sitecorejunkie.com/2012/12/28/have-a-field-day-with-custom-sitecore-fields/
http://stackoverflow.com/questions/18754931/sitecore-workbox-approve-all-configure-security

44 | P a g e