SP OOB Approval Workflow: Change Request &Reassignment stop working

I noticed in several occasions that whenever I customize the OOB approval workflow or modify its task form, the Request Change and Reassignment (Reassign Task) stop working. Before we are going into the workaround, I want to outline how the OOB Approval workflow interacts with its Initiation Form. The task page (WrkTaskIP.aspx) contains an XmlFormView control. The page’s code behind will parse the task form’s infopath xml data into a Hashtable that will be passed into the Approval workflow (OfficeTask) workflow through this SPWorkflow.AlterTask method. Then the workflow will access the hashtable during its process via SPWorkflowTaskProperties.ExtendedProperties, and it relies on the correct combination of keys-values of the hashtable.

The most important key in the Hastable is the TaskStatus, with value equals to the submit data connection of the associated button as shown in the picture. So for example the Request Change button is clicked the task status will be set as ChangeRequest.

if(Request Change clicked)
hash["TaskStatus"] = "ChangeRequest"

Another interesting fact is that the hash table’s key will be changed to the field’s Guid of the Task list where the workflow task resides if it has a field with internal name or static name equal to the key. So in this case if the associated task list’s Status field has static name equals to TaskStatus, the hashtable key will be replaced with the Guid of the Status field.

So if we have a custom form with custom submit connection and a custom workflow, our workflow need to handle extedendProperties["TaskStatus"] = “our submit connection’s name” and extendendProperties[SPBuiltInFieldId.TaskStatus] =”our submit connection’s name”.

With this knowledge we can troubleshoot some of the OOB Approval workflow issues:

  • The Change Request and Reassign Task are not executed at all. Symptom: You click the Request Change or Reassign Task button with Request Change From/Reassign Task To field empty (means reassign the task to the originator). Nothing happen to the workflow and it just stays at current approval process. The Request From and Reassign Task To field are represented by FieldName_RequestTo element and FieldName_DelegateTo element respectively. And they have infopath Person xml in their inner xml. In the unmodified task form the inner xml is literaly empty string when the fields are empty, but whenever we modified the task form the inner xml is not empty but an empty Person element. Somehow the OOB approval workflow fails whenever the value is an empty Person xml. To workaround this we need to set the default values of this two fields to empty.
    1. Go to Data Ribbon tab of the Infopath Designer and choose Default Values in the Form Data section.
    2. Expand the dataFields elements and untick the elements below to set the default  values as empty.
    3. Save and re publish the form
  • The Change Request and Reassign Task are executed but instead of creating a new task it change the current status to ChangeRequest or ReassignTask. This is something to do with the TaskStatus ExtendedProperties as I mentioned in a previous paragraph that the TaskStatus key might be replaced with a GUID if in the task list’s Status field has static name equals to TaskStatus. To fix this one, the Status field’s static name needs to be changed. The best practise is changed it to the same as the internal name. How do we know/change the static name of the field? The answer is Powershell, open the SharePoint administrator and check the static name of the Status field, if it is equals to TaskStatus, change it. Below is short powershell code.
       #$list is the workflow task list
       $field = $list.fields["Status"]
       #if static name = TaskStatus
       $field.staticname = $field.internalname
       $field.update()
    
  • When a user has assigned a Change Request task, the Request field on the task form is empty even though there was a comment. The request field is associated with the Description field (Body is its internal name) of the task list. The field is a Note field (Multiple lines of text) and the infopath form expects it as a plain text. Somehow when we modify the OOB task form, the Description field is provisioned as a Rich Text field. We can change this setting to Plain Text from the list settings as below.

SharePoint 2010 dynamic approvers for OOB Approval Workflow part 2

This Article Series:

  1. SharePoint 2010 dynamic approvers for OOB Approval Workflow part 1
  2. SharePoint 2010 dynamic approvers for OOB Approval Workflow part 2

In previous post in this series, I have explained the 2 approaches to have dynamics approvers for OOB Approval workflow. Responding to on a quite number of requests, in this post I’ll show some sample codes how to accomplish them. Let’s assume we have an one stage, serial approval workflow and we maintain its approvers in a list’s multivalues user field called Approvers as shown below:

The approval workflow would get the approvers from the field’s values. The first thing that we need to have is a routine to convert the SPFieldUserValueCollection into the Approver xml, the xml format is used by the OOB Approval Workflow I explained in my previous post in this series. The end result of the approval xml for this example would be as shown below. As seen in the xml that there would be 3 approvers, the first 2 are SharePoint Groups and the last approver is a user.

 <d:Assignment xmlns:d="http:=//schemas.microsoft.com/office/infopath/2009/WSSList/dataFields">
   <d:Assignee>
     <pc:Person xmlns:pc="http:=//schemas.microsoft.com/office/infopath/2007/PartnerControls">
       <pc:AccountId>Approvers 1</pc:AccountId>
       <pc:AccountType>SharePointGroup</pc:AccountType>
     </pc:Person>
     <pc:Person xmlns:pc="http:=//schemas.microsoft.com/office/infopath/2007/PartnerControls">
       <pc:DisplayName>Approvers 2</pc:DisplayName>
       <pc:AccountId>Approvers 2</pc:AccountId>
       <pc:AccountType>SharePointGroup</pc:AccountType>
     </pc:Person>
     <pc:Person xmlns:pc="http:=//schemas.microsoft.com/office/infopath/2007/PartnerControls">
      <pc:DisplayName>Daniel Laksana</pc:DisplayName>
      <pc:AccountId>AUSDOM\aulaksanada</pc:AccountId>        
      <pc:AccountType>User</pc:AccountType>
     </pc:Person>
   </d:Assignee>
   <d:Stage>0</d:Stage>   
  <d:AssignmentType>Serial</d:AssignmentType>  
</d:Assignment>

Firstly, I created a constant class that contains all the xml templates used in the code as shown below. The first template is for the assignment element, the second template is for each person element. This approach might not be sufficient if we more than 1 stages.

class Constant
{
  public const string WorkflowAssignmentTemplate = @"
            <d:Assignment xmlns:d=""http://schemas.microsoft.com/office/infopath/2009/WSSList/dataFields"">
                 <d:Assignee>{0}</d:Assignee><d:Stage>0</d:Stage><d:AssignmentType>Serial</d:AssignmentType>
            </d:Assignment>";

  public const string IfsUserXmlTemplate = @"
        <pc:Person xmlns:pc=""http://schemas.microsoft.com/office/infopath/2007/PartnerControls"">
           <pc:DisplayName>{0}</pc:DisplayName><pc:AccountId>{1}</pc:AccountId><pc:AccountType>{2}</pc:AccountType>
        </pc:Person>";

 public const string AddAssignmentsElement =
    @"<d:Assignments xmlns:d=""http://schemas.microsoft.com/office/infopath/2009/WSSList/dataFields"">{0}</d:Assignments>";
}

Next steps is the actual code to convert the SPUserFieldValueCollection to the assignment xml, this part of code will call another routine that convert its SPUserFieldValue to the Person elements part of the xml.

//an extension method to serialize SPFieldUserValueCollection to the assignment xml
public static string ToWorkflowAssignmentXml(this SPFieldUserValueCollection users)
{
   StringBuilder sb = new StringBuilder();
   users.ForEach(u => sb.Append(u.ToInfopathUserXml()));
   return string.Format(Constant.WorkflowAssignmentTemplate, sb.ToString());
}
//An extension method for serializing SPFieldUserValue to the Person element
//It also determines the user type either User, SecurityGroup or SharePointGroup
public static string ToInfopathUserXml(this SPFieldUserValue userField)
{
  string groupType = string.Empty;
  if (userField.User != null)
  {
    groupType = userField.User.IsDomainGroup ? "SecurityGroup" : "User";
  }
  else groupType = "SharePointGroup";

 if (groupType == "SharePointGroup")
 {
  return string.Format(Constant.IfsUserXmlTemplate, 
    userField.LookupValue, userField.LookupValue, groupType);
 }
 return string.Format(Constant.IfsUserXmlTemplate, 
   userField.User.Name, userField.User.LoginName, groupType);
}

Let’s apply the code above to my 2 approaches from previous post. First go to any library or list that require the dynamic approver workflow, and setup and approval workflow with no approvers as shown below.

  • Code behind approach. We are using StartWorkflow of the SPWorkflowManager, and we replace the eventData argument with our xml. The code below shows how it works.
    //This routine will construct the workflow association data given the approvers data.
    //The approver data is stored in a list's multi users field called Approvers. As shown at the first picture at the beginning of this post.
    public static string ConstructApproversXml(string associationData, SPListItem approverItem)
    {
      string approversXml = (approverItem["Approvers"] as SPFieldUserValueCollection).ToWorkflowAssignmentXml();
      int startIndex = associationData.IndexOf("<d:Assignment>");
      int lastIndex = associationData.IndexOf("</d:Approvers>");
      string assignmentElementString = associationData.Substring(startIndex, lastIndex - startIndex);
      return associationData.Replace(assignmentElementString, approversXml);
    }
    

    We use the routine above, and pass it to the SPWorkflowManager.StartWorklow as shown below. The workflow will use the approvers defined in the the approverItem. Code below show how to use the routine above. Note. ApproverItem and CurrentItem can be different, as you can have the approvers list separated from the list that has the workflow association.

    //pass the workflow assocition and the approver Item to get the approverXml
    //list and approverItem instantiation are not shown
    var approverXml = WorkflowApproverHelper.ConstructApproversXml(list.WorkflowAssociations[0].AssociationData, approverItem);
    
    //call startWorkflow by passing the item in which the workflow run, the wf association
    //and the approverXml above. 
    //site, list, and currentItem instantiations are not shown. 
    site.WorkflowManager.StartWorkflow(currentItem, list.WorkflowAssociations[0], approverXml);
    				
    				
    
  • Wokflow Approach. This approach requires creating a custom activity as shown in this post. The is a little change in the assignment xml to have it works in a workflow. The Assignment elements needs to be enclosed inside Assignments (with s) element. This short code below adding the parent Assignments element.
    public static string ConstructApproverXmlForWorkflow(SPFieldUserValueCollection userValues)
    {
      return string.Format(Constant.AddAssignmentsElement, userValues.ToWorkflowAssignmentXml());
    }
    

    To use the code above you can change the line 29 of my post here with 2 lines of code below.

     var users = val[approverFieldName] as SPFieldUserValueCollection
     this.ReturnValue = WorkflowApproverHelper.ConstructApproverXmlForWorkflow(users);
    

In this post I have shown you how to implement dynamic workflow’s approvers for OOB SharePoint approval workflow. The sample code above is for one stage, serial approver. But you can extend easily for more complex workflow such as more stages of serial and parallel approvals. You can find the source code here.

SharePoint 2010: Perform complex operation in the WorkflowCompleted Event Receiver

In some of my projects I need to perform complex operations after the SharePoint Approval Workflow finished such as perform complex site provisioning when the status is approved or send an email when status is rejected. In SharePoint 2010 I can use workflow event receivers, one of them is WorkflowCompleted event. It has SPWorkflowEventProperties as its parameter. There is an issue with the parameter is that some its properties are null such as ActivationProperties & AssociationData. So there is no simple way to retrieve SPListItem on which the workflow runs.

Luckily, the InstanceId property is not null, and we can use this property to get the workflow task item from the Workflow Tasks list. And from the workflow task item we can extract listId, itemId and even the workflow name. Below are the internal name of the columns in the workflow task item that I use to retrieve the list item info:

  • WorkflowInstanceID (used this one in the CAML query)
  • WorkflowListId (GUID of the List on which the workflow associated)
  • WorkflowItemId (List item id on which the workflow runs)
  • WorkflowName (Workflow association name as also the workflow field name)

This code shows how to extract the workflow status information and run an operation based on the status during WorkflowCompleted event. Note: the status is based on SharePoint 2010 Approval Workflow Status.

public class SiteRequestListItemEvent : SPWorkflowEventReceiver
{
  //CAML query to retrieve task item based by WF instanceId
  private const string WfInstanceQuery = @"<Where><Eq>
          <FieldRef Name='WorkflowInstanceID'/><Value Type='GUID'>{0}</Value>
          </Eq></Where>";

  private const string WorkflowTaskList = "Workflow Tasks";

  public override void WorkflowCompleted(SPWorkflowEventProperties properties)
  {
    using (SPSite site = new SPSite(properties.WebUrl))
    using (SPWeb web = site.OpenWeb())
    {
      //Query Workflow Task List
      SPList wfTaskList = web.Lists[WorkflowTaskList];
      var items = wfTaskList.GetItems(new SPQuery()
      {
        Query = string.Format(WfInstanceQuery, properties.InstanceId.ToString())
      });
      if (items.Count > 0)
      {

        Guid listId = new Guid(items[0]["WorkflowListId"].ToString());
        int itemId = (int)items[0]["WorkflowItemId"];
        string workflowField = items[0]["WorkflowName"] as string;

        //Get the List item on which the workflow run
        SPListItem item = web.Lists[listId].GetItemById(itemId);

        //get the workflow status
        int status = Convert.ToInt32(item[workflowField]);

        //do something when approved
        if (status == 16)
        {
          //do something such as provision a site, apply permission, etc.
        }
      }

    }

  }

}

Below are the internal value sof the SPWorkflowStatus. SharePoint 2010 Approval workflow add some additional statuses such as Approved, Cancelled & Reject:

  • NotStarted = 0
  • FailedOnStart = 1
  • InProgress = 2
  • ErrorOccurred = 3
  • StoppedByUser = 4
  • Completed = 5
  • FailedOnStartRetrying = 6
  • ErrorOccurredRetrying = 7
  • ViewQueryOverflow = 8
  • Cancelled = 15
  • Approved = 16
  • Rejected = 17

SharePoint 2010 dynamic approvers for OOB Approval Workflow part 1

This Article Series:

  1. SharePoint 2010 dynamic approvers for OOB Approval Workflow part 1
  2. SharePoint 2010 dynamic approvers for OOB Approval Workflow part 2

Microsoft has improved SharePoint’s Approval Workflow in SharePoint 2010 that it is now customizable where its completion condition, behavior of individual task and behavior of overall task processes can be changed on design time using SharePoint Designer 2010. The customization allow us to build more complex approval workflows such as updating list items/documents during the approval process, modify the permissions of the item, modify the email content, etc. This video shows you how to customize OOB Approval workflow.

The approvers of the worklow are defined on the Association or Initiation form. But in some cases we might want to have approvers determine dynamically based on the meta data of the workflow’s item. There are 2 approaches to inject approvers into the Approval Workflow:

  1. Inject the approvers to the Association Data from code behind. This approach can be achieved by starting the workflow in the code behind by calling StartWorkflow method of the SPWorkflowManager class and passing Association data parameter containing the dynamic approvers.
  2. Pass the approvers into the Approvers parameter of the Approval Workflow  during design time (in SharePoint Designer 2010). The picture shows the Approvers parameter.

To be able to inject the approvers data we need to know its data format. As we know, the approvers of the workflow are defined in its association form on the Approvers field as picture below. The association form is an Infopath form so that the approvers field on the form are represented by an xml data. Below is the xml data of the approvers (Approvers xml) as on the picture.

<d:Approvers
   xmlns:d="http://schemas.microsoft.com/office/infopath/2009/WSSList/dataFields"
   xmlns:pc="http://schemas.microsoft.com/office/infopath/2007/PartnerControls">
     <d:Assignment>
       <d:Assignee>
         <pc:Person>
           <pc:DisplayName>Daniel Laksana</pc:DisplayName>
           <pc:AccountId>aDomain\danielLogin</pc:AccountId>
           <pc:AccountType>User</pc:AccountType>
         </pc:Person>
         <pc:Person>
           <pc:DisplayName>Northwind Members</pc:DisplayName>
           <pc:AccountId>Northwind Members</pc:AccountId>
           <pc:AccountType>SharePointGroup</pc:AccountType>
         </pc:Person>
       </d:Assignee>
       <d:Stage xsi:nil="true" />
       <d:AssignmentType>Serial</d:AssignmentType>
      </d:Assignment>
      <d:Assignment>
       <d:Assignee>
         <pc:Person>
           <pc:DisplayName>aDomain\aGroup</pc:DisplayName>
           <pc:AccountId>aDomain\aGroup</pc:AccountId>
           <pc:AccountType>SecurityGroup</pc:AccountType>
         </pc:Person>
         <pc:Person>
           <pc:DisplayName>Northwind Visitors</pc:DisplayName>
            <pc:AccountId>Northwind Visitors</pc:AccountId>
            <pc:AccountType>SharePointGroup</pc:AccountType>
         </pc:Person>
         </d:Assignee>
          <d:Stage xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
         <d:AssignmentType>Parallel</d:AssignmentType>
     </d:Assignment>
  </d:Approvers>

There are 3 possible values of the AccountType:

  1. User
  2. SharePointGroup
  3. SecurityGroup for Active Directory group.

To inject dynamic approvers to the workflow, we can transform any user data from SPUser, SPFieldUserValue , UserProfile or any other data types to the xml format and passing it as part of the Association data of the workflow. You can find the sample implementation in the 2nd post of this series. Referring back to the 2 approaches I mentioned in the beginning of this post:

  1. With the code behind approach, StartWorkflow method of the SPWorklowManager, we can replace the Approvers part of the AssociationData property of the SPWorkflowAssociation parameter.
  2. For the workflow approach, we need to build a custom activity that has output format the same as the Approvers xml, and place the activity before the Approval workflow as shown in the picture.  I posted an article about the custom activity here.  The output xml must exclude the Approvers element as shown below as an example:
<d:Assignments xmlns:d=""http:="//schemas.microsoft.com/office/infopath/2009/WSSList/dataFields">
  <d:Assignment xmlns:d="http:=//schemas.microsoft.com/office/infopath/2009/WSSList/dataFields">
    <d:Assignee>
       <pc:Person
         xmlns:pc="http:=//schemas.microsoft.com/office/infopath/2007/PartnerControls">
        <pc:DisplayName>Approvers 1</pc:DisplayName>
        <pc:AccountId>Approvers 1</pc:AccountId>
        <pc:AccountType>SharePointGroup</pc:AccountType>
      </pc:Person>
       <pc:Person
         xmlns:pc="http:=//schemas.microsoft.com/office/infopath/2007/PartnerControls">
        <pc:DisplayName>Approvers 2</pc:DisplayName>
        <pc:AccountId>Approvers 2</pc:AccountId>
        <pc:AccountType>SharePointGroup</pc:AccountType>
      </pc:Person>
    </d:Assignee>
    <d:Stage>0</d:Stage>
    <d:AssignmentType>Serial</d:AssignmentType>
  </d:Assignment>
</d:Assignments>

With these mechanisms we can have dynamics approvers  for OOB Approval Workflow.

Microsoft SharePoint Workflow, Infopath, ASP.NET

SPD 2010 Custom Workflow Activity

In a recent project, I needed to build a custom workflow activity that would be used in SharePoint Designer. In my workflow I need to retrieve a list of users based on certain criteria. So I need a custom Activity that query a custom list as shown below containing a user field collection column called Users and other fields: Category and Range where it will be used for rule of determining the users.

Category Range Users

Before going into implementation detail, the picture below shows the end result of my custom activity.

SharePoint Designer Custom ActivityIts name is Lookup Worklow Approvers. It has 3 parameters:

  • List name: is the custom list where the activity lookups up for the users
  • Rule: is telling the activity how to retrieve users from the list. The rule is simple, for the custom list above it would be Field=Category, Field=Range, Result=Users. The activity would build a CAML query based on the rule.
  • the return values is coma separated user accounts.

Here is the steps to build this custom Activity:

  1. First create an actions file, an xml file use by SharePoint for defining Workflow Actions/Activities used by SharePoint Designer. It must be deployed in {hive 14}\TEMPLATE\1033\Workflow. The picture shows how it structures in VS SharePoint project. In this example, I created MyCustomActivities.actions.
    Below is the content of my actions file.
    <?xml version="1.0" encoding="utf-8" ?>
    <WorkflowInfo>
      <Actions>
        <Action Name="Lookup Workflow Approvers"
            ClassName="MyCustom.Workflow.Actions.LookupApproversActivity"
            Assembly="MyCustom, Version=1.0.0.0, Culture=neutral, PublicKeyToken=accb8d1cf1b32c7f"
            Category="My Custom Activities"
            AppliesTo="all">
          <RuleDesigner Sentence="Get Approver from List %1 with rule %2 (output to %3)">
            <FieldBind DesignerType="ListNames" Id="1" Text="list name" Field="ListValue" />
            <FieldBind DesignerType="Stringbuilder" Id="2" Text="rules" Field="RuleValue" />
            <FieldBind DesignerType="ParameterNames" Id="3" Text="approvers" Field="ReturnValue" />
          </RuleDesigner>
          <Parameters>
            <Parameter Name="ListValue" Type="System.String, mscorlib" Direction="In" DesignerType="ListNames" Description="Approver matrix list name." />
            <Parameter Name="RuleValue" Type="System.String, mscorlib" Direction="In" DesignerType="Stringbuilder" Description="Approver matrix fields." />
            <Parameter Name="ReturnValue" Type="System.String, mscorlib" Direction="Out" DesignerType="WritableFieldNames" Description="Workflow variable output by this action." />
            <Parameter Name="__Context" Type="Microsoft.SharePoint.WorkflowActions.WorkflowContext, Microsoft.SharePoint.WorkflowActions" Direction="In" DesignerType="Hide" />
          </Parameters>
        </Action>
      </Actions>
    </WorkflowInfo>
    The explanation of the xml can be found here. The Rule Designer will be the text displayed on SPD where the variables are marked by %. The variables are mapped to Id attributes of the FieldBind elements. The FieldBind elements map the SPD designer variables to code behind variables defined in Parameters elements. One of the parameter element is __Context, this is the Workflow Context that would be passed into the codebehind.
  2. Next step, is the code behind. Please see this msdn article for step by step of creating a custom workflow activity.
         public class LookupApproversActivity : Activity
        {
            #region Fields
            public static DependencyProperty __ContextProperty = DependencyProperty.Register("__Context", typeof(WorkflowContext), typeof(LookupApproversActivity));
            public static DependencyProperty ReturnValueProperty = DependencyProperty.Register("ReturnValue", typeof(string), typeof(LookupApproversActivity));
            public static DependencyProperty ListValueProperty = DependencyProperty.Register("ListValue", typeof(string), typeof(LookupApproversActivity));
            public static DependencyProperty RuleValueProperty = DependencyProperty.Register("RuleValue", typeof(string), typeof(LookupApproversActivity));
            #endregion
    
        }
        

    After declaring the Dependency Properties, we defined the Properties, they must match Parameter elements defined in the action xml.

     #region Properties
            [ValidationOption(ValidationOption.Optional)]
            public WorkflowContext __Context
            {
                get
                {
                    return (WorkflowContext)base.GetValue(__ContextProperty);
                }
                set
                {
                    base.SetValue(__ContextProperty, value);
                }
            }
    
            [ValidationOption(ValidationOption.Optional)]
            public string ReturnValue
            {
                get
                {
                    return (string)base.GetValue(ReturnValueProperty);
                }
                set
                {
                    base.SetValue(ReturnValueProperty, value);
                }
            }
    
            [ValidationOption(ValidationOption.Optional)]
            public string ListValue
            {
                get
                {
                    return (string)base.GetValue(ListValueProperty);
                }
                set
                {
                    base.SetValue(ListValueProperty, value);
                }
            }
    
            [ValidationOption(ValidationOption.Optional)]
            public string RuleValue
            {
                get
                {
                    return (string)base.GetValue(RuleValueProperty);
                }
                set
                {
                    base.SetValue(RuleValueProperty, value);
                }
            }
    
            #endregion
    

    And then the implementation of Execute method: The __Context would be used to retrieve current web, current item as shown in the first 2 lines. Then it retrieves the custom List containing the users using Workflow Helper class GetListGuid method passing in ListValue variable and __Context. Next is parsing the RuleValue variable to retrieve the fields. As mentioned above the rule is field=<fieldname> for field and result=<fieldname> for the user field. At line 27, it performs CAML query to the rule list and then parse the result of the query to the ReturnValue variable. Note. There are several classes are my own utility classes such as QueryData and Utility.

    protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
    {
       SPWeb currentWeb = this.__Context.Web;
       SPListItem currentItem = currentWeb.GetListItem(this.__Context.CurrentItemUrl);
       SPList ruleList = currentWeb.Lists[Helper.GetListGuid(this.__Context,ListValue)];
       var rules = from rule in RuleValue.Split(new string[] { "\r\n", "\n", "\r", "," }, StringSplitOptions.RemoveEmptyEntries)
          let r = rule.Trim().Split(new char[] { '=' })
          select new
          {
             Type = r[0].ToLower().Trim(),
             Field = r[1].Trim(),
          };
    
      string approverFieldName = string.Empty;
      List qData = new List();
      foreach (var r in rules)
      {
         if (string.Compare(r.Type, "field", true) == 0)
         {
            SPField field = ruleList.Fields.GetField(r.Field);
            qData.Add(new QueryData() { Field = field.InternalName, Value = currentItem.GetItemProperty(r.Field), Type = field.TypeAsString });
          }
          if (string.Compare(r.Type, "result", true) == 0)
               approverFieldName = r.Field;
      }
    
      SPListItem val = ruleList.GetItems(new SPQuery() { Query = QueryData.BuildQuery(qData) }).Cast().FirstOrDefault();
    
      this.ReturnValue = Utility.ParseUserValues(val[approverFieldName] as SPFieldUserValueCollection));
    
      return ActivityExecutionStatus.Closed;
    }
    
  3. The last configuration that need to be done is registering the workflow as authorized type in the web.config under System.Workflow.ComponentModel.WorkflowCompiler element add this
    <authorizedType Assembly="MyCustom, Version=1.0.0.0, Culture=neutral, PublicKeyToken=accb8d1cf1b32c7f" Namespace="MyCustom.Workflow.Actions" TypeName="*" Authorized="True" />
    
    After deploying the solution, perform IIS Reset. Then our custom activity would be available in SPD Workflow Actions drop down.

In the next post I will show how to modify this custom activity to feed approvers to the SharePoint 2010 OOB Approval Workflow.

Microsoft SharePoint 2010,SharePoint Designer, SharePoint Workflow, ASP.NET

Adding Visio diagram to existing Visual Studio workflow – part 3

This article series

  1. Adding Visio diagram to existing Visual Studio workflow – part 1
  2. Adding Visio diagram to existing Visual Studio workflow – part 2
  3. Adding Visio diagram to existing Visual Studio workflow – part 3

In part 2 of this topic I explained how to decorate the existing workflow with ISPActivityDetailsSerializable and implement the GetActivityDetail method, as this method would be called by the WorkflowStatus control in WrkStat.aspx.

As mentioned the WorkflowStatus control is a delegate control with Id WorkflowStatusVisualization. As a delegate control it means we can replace the control with our own implementation using a feature. Looking at the implementation of the OOB WorkflowStatus UserControl is defined in ~/_controltemplates/workflowstatus.ascx, in only contains a VisioWebAccess web part and a hidden field.

Inspecting its code behind in reflector, the control would try to find ShowPreview & PreviewHref in the Workflow Association Data. As explained in part 1 these settings would tell the control to display the Visio Diagram, the control also called the GetActivityDetails method of the workflow instance and stores its value and  then it registers a javascript located in _layouts/VisioWebAccess/workflowstatus.js. The javascript contains functions to render the overlays on the Visio Diagram. The most important part of the control is a method to initialize the javascript variables using values return by the GetActivityDetails method.

Having this knowledge I prefer to extend the OOB WorkflowStatus control to be able to process my custom Activity Detail. Below is steps for implementing our custom WorkflowStatus Control

1. In the VS 2010 add reference to Microsoft.Office.Visio.Server.dll the project and add a new UserControl, this will add a new User Control under CONTROLTEMPLATES.

2. Create a Web Scope feature to provision the Delegate Control. below is the element xml of the feature.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Control Id="WorkflowStatusVisualization" Sequence="80" ControlSrc="~/_controltemplates/DL.WorkflowControl/CustomWorkflowStatus.ascx" />
</Elements>

3.Copy the content (anything under @Control directive tag) from OOB WorkflowStatus.ascx in the CONTROLTEMPLATES folder to the content to our CustomWorkflowStatus.ascx.  Also copy and @Register TagPrefix=VisioWebControls to our custom control. And put below code as its code behind

public partial class CustomWorkflowStatus : WorkflowStatus
{
    protected override void OnPreRender(EventArgs e)
    {
        this.VisioWebAccess1.DiagramPath = this.DiagramPath;
        if (IsCustomActivityDetail)
            this.AddActivityDetailsGenerationScript();
        else base.OnPreRender(e);
    }

    private string DomObjectID
    {
        get
        {
            return ("WebPart" + this.VisioWebAccess1.Qualifier);
        }
    }

    private bool IsCustomActivityDetail
    {
        get
        {
            if (this.ActivityDetailsArray != null &&
               ActivityDetailsArray.Length > 0 &&
               !string.IsNullOrEmpty(ActivityDetailsArray[0].ExtendedActivityDetails))
            {
                //get the first activityDetail
                XElement customActDetail = XElement.Parse(this.ActivityDetailsArray[0].ExtendedActivityDetails);
                return customActDetail.Name == "MyActivities";
            }
            return false;
        }
    }

    private void AddActivityDetailsGenerationScript()
    {
        string key = "WorkflowVWAActivityDetailsScript";
        if (!this.Page.ClientScript.IsClientScriptBlockRegistered(key))
        {
            StringBuilder script = new StringBuilder();
            script.Append("<script type=\"text/javascript\">\n");
            script.Append("//<![CDATA[\n");
            script.Append("vwaControlName= \"" + this.DomObjectID + "\"; \n");

            XElement customActDetail = XElement.Parse(this.ActivityDetailsArray[0].ExtendedActivityDetails);
            ActivityDetailsArray = (from act in customActDetail.Descendants("Activity")
                                    select new SPWorkflowActivityDetails()
                                    {
                                        Name = string.Empty,
                                        ShapeId = act.Attribute("Guid").Value,
                                        Status = (SPWorkflowActivityStatus)Enum.Parse(typeof(SPWorkflowActivityStatus), act.Attribute("Status").Value)
                                    }).ToArray();

            script.Append(" var statusDetails; \n");
            int num2 = 0;
            for (int i = 0; i < this.ActivityDetailsArray.Length; i++)
            {
                SPWorkflowActivityDetails ad = this.ActivityDetailsArray[i];
                if (ad != null)
                {
                    SPWorkflowActivityDetails ad = this.ActivityDetailsArray[i];
                    if (ad != null)
                    {
                        script.Append(string.Concat(new object[] { "statusDetails = new ActivityDetails('", ad.Name, "','", ad.Status, "','", ad.ShapeId, "'); \n" }));
                        script.Append("wfActivityDetails[" + num2 + "] = statusDetails; \n");
                        num2++;
                    }
                }
            }
            script.Append("var shapeCount = " + 1); //changed num3 to 2 as num3 sometimes return RTL(right to left) to true

            script.Append("//]]>\n");
            script.Append("</script>");
            this.Page.ClientScript.RegisterClientScriptBlock(base.GetType(), key, script.ToString());
        }

    }

}

Code explanation:

As we can see the CustomWorkflowStatus inherits from OOB WorkflowStatus class. In our custom class we override the OnPreRender method. In the method we set the DiagramPath of the VisioWebAccess web part, and then the most important thing is checking if the workflow has custom activity details, if not it just calls the OOB PreRender implementation.

Most of the class initialization has been done in the OnLoad event of the parent class, one of them is setting  this.ActivityDetailsArray = workflow.GetActivityDetails(). So in our IsCustomActivityDetail property in line 19, we check the ActivityDetailArray. As our workflow implements ISPActivityDetailSerializable, the first element of the array should have non-null ExtendedActivityDetail property. Then using LINQ we check if the value of the property’s root element is MyActivities (our custom xml element).

In line 35, we parse our custom xml from the ExtendedActivityDetail property and initialize the javascript used by workflowstatus.js to draw the status overlays on the Visio Diagram.

The result is that we can visualize our existing visual studio workflow using any Visio Diagram as below.

Summary:
To add visualization to existing visual studio workflow we need to decorate the existing workflow using ISPActivityDetailsSerializable and implement GetActivityDetails method to generate a custom xml that can be consumed by our custom WorkflowStatus control. The control would add Status overlay on the Visio diagram.

Microsoft SharePoint 2010,SharePoint Designer, SharePoint Workflow, Web Part

Adding Visio diagram to existing Visual Studio workflow – part 2

This article series

  1. Adding Visio diagram to existing Visual Studio workflow – part 1
  2. Adding Visio diagram to existing Visual Studio workflow – part 2
  3. Adding Visio diagram to existing Visual Studio workflow – part 3

In my previous post I explained about how SharePoint maps Visio Diagram’s shapes to the Workflow’s Activities. In this post I will outline how to implement ISPActivityDetailsSerializable to the my existing workflow to enable visio diagram without breaking the existing running workflow.

1. The first step is to pass the visio shape information to the workflow. I created additional custom elements in the workflow.xml inside metadata element. The ShapeGuid are the ones retrieved in previous post, the Status possible values are NotStarted, InProgress or Succeeded. the values are used by the OOB workflowstatus control and workflowstatus.js to draw the status overlay.

<VisioDiagram>
  <ActivityDetails>
    <ActivityDetail ID="Stage1" ShapeGuid="{3E94E51E-3A9A-40CB-AE36-A3B641E3FF23}" Status="Succeeded"/>
   <ActivityDetail ID="Gate1" ShapeGuid="{the-shape-guid}" Status="NotStarted"/>
   <ActivityDetail ID="Stage2" ShapeGuid="{the-shape-guid}" Status="NotStarted"/>
   <ActivityDetail ID="Gate2" ShapeGuid="{the-shape-guid}" Status="NotStarted"/>
   <ActivityDetail ID="Stage3" ShapeGuid="{the-shape-guid}" Status="NotStarted"/>
    <ActivityDetail ID="Gate3" ShapeGuid="{the-shape-guid}" Status="NotStarted"/>
  </ActivityDetails>
</VisioDiagram>

2. I created another class called WFActivityDetails, it has the same properties as OOB SPWorkflowActivityDetails class but decorated with Serializable attribute. The status type is a enum type called SPWorkflowAcitivityStatus. This is OOB enum type used by workflowstatus control.
[Serializable]
public class WFActivityDetails
{       
        public string ExtendedActivityDetails { get; set; }
        public string Name { get; set; }
        public string ShapeId { get; set; }
        public SPWorkflowActivityStatus Status { get; set; }
}

3. In the OnWorkflowActivated method of the my custom workflow class, I retrieved the xml created in step 1 and transform into a Dictionary of WFActivityDetails, with Dictionary’s key values equals to ID values in the xml. To ensure that I don’t break the existing workflow instances during serialization. I stored the Dictionary into an existing Hashtable containing the workflow initiation data, in my code the hashtable variable name is initFormData.

private void OnWorkflowActivated(object sender, ExternalDataEventArgs e)
{
//Other code not displayed for clarity
//Store the dictionary into the existing hashtable containing Workflow Initiation data
    this.initFormData["ActivityDetails"] = GetActivityDataFromAssociationData();
}

private Dictionary<string,WFActivityDetails> GetActivityDataFromAssociationData()
{
  XNamespace ns = XNamespace.Get("http://schemas.microsoft.com/sharepoint/");
  XElement element = XElement.Parse(workflowProperties.Workflow.ParentAssociation["VisioDiagram"].ToString());
  return (from e in element.Descendants(ns + "ActivityDetail")
      select new 
     {
          Guid = e.Attribute("ShapeGuid").Value,
          Key = e.Attribute("ID").Value,
          Status = (SPWorkflowActivityStatus) Enum.Parse(typeof(SPWorkflowActivityStatus),e.Attribute("Status").Value),
       }).ToDictionary(
              dic => dic.Key, 
              dic => new WFActivityDetails()
              { 
                  ShapeId = dic.Guid, 
                  Status = dic.Status, ExtendedActivityDetails = string.Empty
               },
               StringComparer.OrdinalIgnoreCase);
}

4.To ensure the existing running workflow instances picks up the changes I did the following. As my workflow has replicator in it, and in the replicator there is an OnTaskChanged activity, in the activity event handler I put below code.
private void onTaskChanged_Invokedl(object sender, ExternalDataEventArgs e)
{
   //existing code not displayed for clarity
//Store the dictionary into the existing hashtable containing Workflow Initiation data
  if(this.initFormData["ActivityDetails"] == null)
    this.initFormData["ActivityDetails"] = GetActivityDataFromAssociationData();
}

5. Decorate the workflow with ISPActivityDetailsSerializable and implement the GetActivityDetails() method. In the code below I created a private property called ActivityDetails to retrieve the Dictionary created on step 3. And in the GetActivityDetails method, I converted the Dictionary back into an xml that would be consumed by WorkflowStatus control. Note. the xml structure can be anything
public sealed partial class MyApprovalWorkflow : SequentialWorkflowActivity, ISPActivityDetailsSerializable
{
  private Dictionary<string, WFActivityDetails> ActivityDetails
  {
   get
      { 
         return this.initFormData["ActivityDetails"] as (Dictionary<string,WFActivityDetails>) ;
       }
  }

  public string GetActivityDetails()
  {
    XElement activities = new XElement("MyActivities",
         from a in ActivityDetails.Values
            select new XElement("Activity",
                new XAttribute("Guid", a.ShapeId),
                    new XAttribute("Status", a.Status.ToString()),
                      new XAttribute("Approver", a.ExtendedActivityDetails)
                           ));
     return activities.ToString(SaveOptions.DisableFormatting);
  }
 }

6. During the course of the workflow I updated the status of the WFActivityDetails to match with the approval state in the workflow. My method called UpdateVisioDiagramState() and it is called each time the approval state has been changed.
private void UpdateVisioDiagramState()
{
 if (this.ActivityDetails.Count > 0)
 {
   switch (this.ApprovalState)
   {
    case State.Gate1:
      ActivityDetails["Gate1"].Status = SPWorkflowActivityStatus.InProgress;
      break;
    case State.Stage2:
       ActivityDetails["Gate1"].Status = SPWorkflowActivityStatus.Succeeded;
       ActivityDetails["Stage2"].Status = SPWorkflowActivityStatus.InProgress;
       break;
   //so on ....
  }
}

7.We’ve finished with the workflow. Now we need to look at the workflow status control. The workflow visualization displayed in WrkStat.aspx page is a delegate control with Id WorkflowStatusVisualization. The delegate control is implemented in OOB VisioWebAcces feature with a control source ~/_controltemplates/workflowstatus.ascx. We need to create our own implementation of the delegate control by extending the OOB WorkflowStatus control to read our xml returned by GetActivityDetails() method in step 4.

In next post, I will explain how to extend the OOB workflow status control to read our custom xml.

Microsoft SharePoint 2010,SharePoint Designer, SharePoint Workflow, Web Part

Adding Visio diagram to existing Visual Studio workflow – part 1

This article series

  1. Adding Visio diagram to existing Visual Studio workflow – part 1
  2. Adding Visio diagram to existing Visual Studio workflow – part 2
  3. Adding Visio diagram to existing Visual Studio workflow – part 3

One of my SharePoint migration project task is adding workflow visualization to the existing (visual studio workflow) workflow without breaking the current running worlflow.

Microsoft SharePoint 2010,SharePoint Designer, SharePoint Workflow, Web Part

1. Firstly we need to create workflow diagram in visio and save as Web Drawing (.vdw). Below is my worklow diagram.

Visio Workflow

2. We need to provision the visio diagram into SharePoint. I added a Module in the workflow feature. Below is the xml of the element. The visio diagram WorkflowDiagram.vsw is added into SharePoint _catalogs\wfpub  library.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns='http://schemas.microsoft.com/sharepoint/'>
  <Module Name='WorkflowDiagram' Url='_catalogs/wfpub/MyWorkflowProject' Path='WorkflowDiagram'>
  <File Url='WorkflowDiagram.vdw' Type='GhostableInLibrary' />
</Module>
</Elements>

3. Next step is adding these 2 elements (ShowPreview and PreviewHref) in the workflow.xml under Metadata element. This would instruct SharePoint workflowVisualization control in WrkStat.aspx to display the visio diagram.
<Metadata>
<!--Other workflow metadata such as initiation and modification form-->
<ShowPreview>true</ShowPreview>
<PreviewHref>/_catalogs/wfpub/MyWorkflowProject/WorkflowDiagram.vdw</PreviewHref>
</Metadata>

4. To map the Visio diagram with visual studio diagram we need to get the Shape Guid of each shape. The ID can be retrieve by exporting the workflow in Visio. Go back to Visio and from the ribbon choose Process and Export Workflow, this would the export the worklfow into a Visio Workflow interchange (vwi) file. Change the file type into zip, extract it and open workflow.xoml in notepad. In the file the Shape Guid of each activity can be found  under <SequenceActivity x:Name=”> element. Below is Stage 1 Activity element.
<ns0:TodoItemTask x:Name="ID4" ContentTypeId="{x:Null}" Title="{x:Null}" __Context="{ActivityBind ROOT,Path=__context}" Description="ShapeGuid={3E94E51E-3A9A-40CB-AE36-A3B641E3FF23};PageId=0;ShapeId=14;ShapeText=STAGE 1
Stage 1 Description" AssignedTo="{x:Null}" />

Note:  The easiest way to map Visio shapes to Visual Studio Activities is to copy the Description of the Visio workflow shapes to the Description property of the Visual Studio ones. In picture below, I mapped an Code Activity to Stage 1 shape by setting the Description property of the CodeActivity.

This approach has drawback that you need to deploy the workflow in different assembly version and provision it as a new workflow and set the old workflow to ‘No New Instance’. Another drawback this will not work if your activity is inside ReplicatorActivity or any loop activities as  they are running on different Workflow Execution Context meaning that their statuses would be always NotStarted.

5. Another approach is to decorate the existing workflow with ISPActivityDetailsSerializable. This interface has only one method called GetActivityDetails(). To understand this approach we need to know how the workflow visualization work. The control that render the workflow in the WrkStat.aspx is WorkflowStatus.ascx.Below is the steps how WorkflowStatus control renders the diagram.

  1. The control will check the workflow.xml if it contains ShowPreview and get the Visio diagram from PreviewHref.
  2. Then it will invoke GetActivityDetail method of the workflow instance. The method will return an array SPWorkflowActivityDetails instance, each of them belongs to each activity in the workflow that has ShapeID set in its Desciption property or it implements ISPActivityDetailsSerializable. SPWorkflowActivityDetails has several properties but the most important ones are ShapeId = the value of ShapeId in the Description of the activity we set above. Status = the workflow Status and ExtendedActivityDetails= the value is from calling the GetActivityDetails() of the ISPActivityDetailsSerializable.
  3. Then the control will create some JavaScript to mashup the Visio Diagram. The JavaScript will use the ShapeId to find the Visio shapes and use the Status to put status overlays on the shapes. It also uses the ExtendedActivityDetails to put richer overlays such as in the SharePoint 2010 approval workflow.

In the next post I will explain how to decorate our existing workflow with ISPActivityDetailsSerializable and implement the GetActivityDetails method to provide the ExtendedActivityDetails that can be used by WorkflowStatus control.