Extending Sitecore Approval Workflow

Life at Apollo Division
Life at Apollo Division
6 min readAug 4, 2020

Why customize the default Sitecore workflow?

Sitecore is by default shipped with Simple Workflow covering simple draft — approval — publish scenario.

In this scenario, publish state of the workflow is set as final. This fact causes that when the content author needs to edit item fields, a new version of the item is created automatically, and changed content is subject to the approval process.

Some customers may need to extend this workflow by an additional state to control the moment when the item is published (and they don’t want to use time-based publishing) by adding the additional step between approval and published states.

The resulting workflow then looks like

draft — approving — approved — publish

Workflow in Sitecore

with publish state as the only with final flag.

Everything looks fine, but…

… there is one unpleasant side effect. It allows content editors to change values of items in an approved state without enforcing of creation of the new version.

How is the creation of new item versions handled?

If Sitecore needs to know if the new version of the item should be created, it asks the workflow implementation class to get the value indicating how to act during the checkout of the item.

The key method is IsApproved method on the workflow implementation. In standard Sitecore workflow implementation, it (a little bit simplified) checks if the current item’s state has IsFinal set to true. If IsFinal is set to true, the item is considered as published and the new version is created.

Unfortunately, this method is used for two situations:

  • It is used for check if new item version should be created (called by item:checkout command)
  • Check if the item is eligible to be published (as part of publishing pipeline)

Which means:

If “approved” state will not have IsFinal flag

  • Item will not be published (this is OK)
  • If the content editor will check out this item, a new version will not be created (this is not OK)

If “approved” state will have IsFinal flag:

  • Item will be published (this is not OK)
  • The new version will be created (this is OK)

So, overriding of this method is not enough for our expected behavior (we want to force users to create a new version, but not publish the item).

How to takeover item creation

To implement own logic for item version creation, we need to start at a different place — in item:checkout event handler. It is the place, where the logic if the new version will be created, resides.

One of the methods called from item:checkout event handler resides in the WorkflowContext, and is called StartEditing.

1. public Item StartEditing(Item item, SaveArgs.SaveItem saveItem)

2. {

3. Error.AssertObject(item, “item”);

4. if (Context.User.IsAdministrator)

5. {

6. return item;

7. }

8. if (_context.IsAdministrator)

9. {

10. return Lock(item);

11. }

12. if (StandardValuesManager.IsStandardValuesHolder(item))

13. {

14. return Lock(item);

15. }

16. if (!HasWorkflow(item) && !HasDefaultWorkflow(item))

17. {

18. return Lock(item);

19.v}

20. if (!IsApproved(item))

21. {

22. return Lock(item);

23. }

24. if (saveItem != null)

25. {

26. return IncreaseVersionWithSaveItem(item, saveItem);

27. }

28. Item item2 = IncreaseVersion(item);

29. if (item2 != null)

30. {

31. return Lock(item2);

32. }

33. return null;

34. }

And here, the key point is IsApproved method — it calls workflow’s implementation method IsApproved (and checks the IsFinal flag).

For our purpose, we need to take over all of this mechanism, and customize it, because WorkflowContext class cannot be inherited or replaced by any suitable way.

Customizing Event handler

To achieve the desired functionality, we were forced to mix several methods for several places and create the following checkout command.

Check the function headers, it has the information from which this function was taken, and how it was modified.

The most important method here is RunClient, which was taken from the Run method from the original CheckOut implementation. It assures that if the item is in approved state and the current user does not have an approval role, the new version is created, otherwise original behavior is executed.

Methods CreateNewVersionInApprovedState, IsItemInApprovedState and IsContentApprover are service methods for internal checks or for the creation of the new version of the item, and other methods are taken from original implementations in 1:1, because they were private, or there was not possible to call them directly.

/// <summary>

2. /// This class overrides the default checkout mechanism. The reason is that standard sitecore enforce version creation only in final state, but we need to enforce

3. /// version creation also in “approved” state. This function is taken from original Execute method, just on the last line it calls RunClient method instead of Run method.

4. /// </summary>

5. public class CheckoutCommand : CheckOut

6. {

7. /// <inheritdoc/>

8. public override void Execute(CommandContext context)

9. {

10. Assert.ArgumentNotNull(context, “context”);

11. if (context.Items.Length == 1)

12. {

13. var item = context.Items[0];

14. var nameValueCollection = new NameValueCollection();

15. nameValueCollection[“id”] = item.ID.ToString();

16. nameValueCollection[“language”] = item.Language.ToString();

17. nameValueCollection[“version”] = item.Version.ToString();

18. Context.ClientPage.Start(this, “RunClient”, nameValueCollection);

19. }

20. }

21. /// <summary>

22. /// Executes client side of the pipeline. Customized version of Checkout.Run method.

23. /// </summary>

24. /// <param name=”args”>Pipeline arguments.</param>

25. protected void RunClient(ClientPipelineArgs args)

26. {

27. Assert.ArgumentNotNull(args, “args”);

28. if (!SheerResponse.CheckModified())

29. {

30. return;

31. }

32. var itemPath = args.Parameters[“id”];

33. var name = args.Parameters[“language”];

34. var value = args.Parameters[“version”];

35. var item = Client.GetItemNotNull(itemPath, Language.Parse(name), Sitecore.Data.Version.Parse(value));

36. if (!item.Locking.IsLocked())

37. {

38. Log.Audit(this, “Start editing: {0}”, AuditFormatter.FormatItem(item));

39. if (Context.User.IsAdministrator)

40. {

41. item.Locking.Lock();

42. }

43. else

44. {

45. // here is customized code

46. if (this.IsItemInApprovedState(item) && !this.IsContentApprover(item))

47. {

48. item = this.CreateNewVersionInApprovedState(item);

49. }

50. else

51. {

52. item = Context.Workflow.StartEditing(item);

53. }

54. // here is not customized code

55. }

56. Context.ClientPage.SendMessage(this, “item:startediting(id=” + item.ID + “,version=” + item.Version + “,language=” + item.Language + “)”);

57. }

58. }

59. /// <summary>

60. /// Creates a new version of the item. This function is modified from Context.Workflow.StartEditing function.

61. /// </summary>

62. /// <param name=”item”>Item to be processed.</param>

63. /// <returns>Version of item to be created.</returns>

64. protected Item CreateNewVersionInApprovedState(Item item)

65. {

66. Error.AssertObject(item, “item”);

67. if (Context.User.IsAdministrator)

68. {

69. return item;

70. }

71. if (StandardValuesManager.IsStandardValuesHolder(item))

72. {

73. return this.Lock(item);

73. }

74. if (!Context.Workflow.HasWorkflow(item) && !Context.Workflow.HasDefaultWorkflow(item))

75. {

76. return this.Lock(item);

77. }

78. Item item2 = this.IncreaseVersion(item);

79. if (item2 != null)

80. {

81. return this.Lock(item2);

82. }

83. return null;

84. }

85. /// <summary>

86. /// Checks if the item is in approved state.

87. /// </summary>

88. /// <param name=”item”>Item to be checked.</param>

89. /// <returns>Value indicating whether item is in approved state.</returns>

90. protected bool IsItemInApprovedState(Item item)

91. {

92. var currentItemWorkflow = Context.Workflow.GetWorkflow(item);

93. if (currentItemWorkflow == null)

94. {

95. return false;

96. }

97. var currentItemState = currentItemWorkflow.GetState(item);

98. if (currentItemState == null)

99. {

100. return false;

101. }

102. var currentItemStateId = new ID(currentItemState.StateID);

103. var confirmationStateId = new ID(Consts.WorkflowStates.ApprovedState);

104. if (currentItemStateId == confirmationStateId)

105. {

106. return true;

107. }

108. return false;

109. }

110. /// <summary>

111. /// Locks the specified item. This version is 1:1 copy of Sitecore.Workflows.WorkflowContext.Lock.

112. /// </summary>

113. /// <param name=”item”>Item to be locked.</param>

114. /// <returns>Locked item.</returns>

115. protected Item Lock(Item item)

116. {

117. if (TemplateManager.IsFieldPartOfTemplate(FieldIDs.Lock, item) && !item.Locking.Lock())

118. {

119. return null;

120. }

121. return item;

122. }

123. /// <summary>

124. /// Increase the version, then response to the UI. This version is 1:1 copy of Sitecore.Workflows.WorkflowContext.IncreaseVersion.

125. /// </summary>

126. /// <param name=”item”>Item to be edited.</param>

127. /// <returns>Created item.</returns>

128. private Item IncreaseVersion(Item item)

129. {

130. var item2 = item.Versions.AddVersion();

131. Context.ClientPage.Dispatch($”item:load(id={item2.ID},language={item2.Language},version={item2.Version})”);

132. return item2;

133. }

134. /// <summary>

135. /// Returns a value indicating whether user is in approver role.

136. /// </summary>

137. /// <param name=”item”>Item which is processed in command handler.</param>

138. /// <returns>True if current user is in content approver role.</returns>

139. private bool IsContentApprover(Item item)

140. {

141. var user = Context.User;

142. if (user.IsInRole(Authorization.Consts.Roles.ApprovingEditor))

143. {

144. return true;

145. }

146. return false;

147. }

148. }

We were not so happy with this way of implementation, but it was the only way we found to implement customers’ requirements.

We are ACTUM Digital and this piece was written by Tomas Knaifl, Senior .NET Developer of Apollo Division. Feel free to get in touch.

--

--