Wednesday, November 16, 2011

Custom Action for Workflow

Building the Custom Action
In the previous part of this blog I explained my approach and in this part I am going to show you how I created two custom actions that is needed to support my approach. I am first going to show you the code solution I created and then dive a bit into why I made the solution as is.
I can tell you honestly that I really wanted this solution to be a SharePoint 2010 Sandbox solution however I ran into a few challenges that prevented me from doing that. This forced me to build a full trust solution with Visual Studio 2010. There are basically two actions I needed to create. The first one is an action that can initiate another workflow. The second action I needed checks to see if 1 to N workflows are running on a list item, and if so, stop the running of that workflow.
It is pretty well documented on how to create a custom action, deploy it and then use that action in SharePoint Designer. The following are some good references:

http://msmvps.com/blogs/sundar_narasiman/archive/2010/12/26/develop-custom-workflow-activity-for-sharepoint-2010-workflow.aspx

Workflows in SharePoint Designer 2010 are very powerful. But it gets even better using the custom WorkFlow Activities.
It gives you the possibility to execute custom actions during the workflow.
In this example I will be making a custom acitivity containing 2 properties to help you understand how everything works.
You can add as many properties as you want.
Start by creating a Workflow Activity Library project in Visual Studio 2010 and add a New Activity and name it MyCustomActivity.
First you need to know that there are four things to keep in mind
  • Add Dependency properties to send values from the workflow inside the activity
  • Override the Execute method
  • Creating the .actions file
  • Register the assembly as an AuthorizedType
Adding Dependency properties
I will not explain what dependency properties are or what they do. A good explanation can be found here.
I will add 2 properties: Name and FirstName

---------------------------
public static DependencyProperty NameProperty = DependencyProperty.Register("Name", typeof(string), typeof(MyCustomActivity));
 
        [Category("Cross-Site Actions"), Browsable(true)]
        [DesignerSerializationVisibility
          (DesignerSerializationVisibility.Visible)]
        public string Name
        {
            get
            {
                return Convert.ToString(base.GetValue(NameProperty ));
            }
            set
            {
                base.SetValue(NameProperty , value);
            }
        }
 
public static DependencyProperty FirstNameProperty = DependencyProperty.Register("FirstName", typeof(string), typeof(MyCustomActivity));
 
        [Category("Cross-Site Actions"), Browsable(true)]
        [DesignerSerializationVisibility
          (DesignerSerializationVisibility.Visible)]
        public string FirstName
        {
            get
            {
                return Convert.ToString(base.GetValue(FirstNameProperty ));
            }
            set
            {
                base.SetValue(FirstNameProperty , value);
            }
        }
 -------------------------------------------
Override the Execute met
After adding the necesary properties we can now use these properties in the Execute method to do whatever we want
  • Creating a file on your system
  • Connect with a Service
-----------------
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
      //Custom code here
}
 
---------------------------
I haven’t add any code in here because the code inside the Execute method has nothing to do with understanding the Custom Activities.
The properties are accessable (as string) using this.Name and this.FirstName.
Now your activity file is ready.

Creating the .actions file
The last step is making an actions file to “register” the activity in SharePoint Designer.
Right click your project and add a nex XML document. Rename it to MyCustomActivity.actions
Edit this file so it looks like:

----------------------------------
<WorkflowInfo>
  <Actions Sequential="then" Parallel="and">
    <Action Name="Learning Custom Activities"
   ClassName="Custom.WorkFlow.Activities.MyCustomActivity"
   Assembly="Custom.WorkFlow.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f70579c3812423d8"
   AppliesTo="all" Category="Cross Site Actions">
      <RuleDesigner
           Sentence="Learning custom activities with parameter Name = %1 and parameter FirstName = %2">
        <FieldBind Field="Name" DesignerType="TextArea"
           Id="1"/>
        <FieldBind Field="FirstName" DesignerType="TextArea" Id="2" />
      </RuleDesigner>
      <Parameters>
        <Parameter Name="Name" Type="System.String, mscorlib"
          Direction="In" />
        <Parameter Name="FirstName" Type="System.String, mscorlib"
          Direction="In" />
      </Parameters>
    </Action>
  </Actions>
</WorkflowInfo>
-----------------------

First you have to specify a name for your action:
 
Action Name="Learning Custom Activities"
 
Next thing is to retrieve your full class name and assembly details. This assembly has to be deployed to the GAC.
To do this: Right click your project and select properties. Click the Signing tab and check “Sign the assembly”.
In the “choose a strong name key file” list choose for new and specify a name without a password!
Build your project and copy the .dll file into c:\Windows\Assembly
Now open this file with reflector to get the classname and assembly name. Your result should be something like:

ClassName="Custom.WorkFlow.Activities.MyCustomActivity"
Assembly="Custom.WorkFlow.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f70579c3812423d8"
 
The category name should be equal to what you have entered as Category attribute in you activity code file.
The sentence inside the RuleDesigner tag is the sentence you will see in SharePoint designer. %n will be the corresponding parameter where n is the ID.
You can create a sentence as you wish, just make sure to include all the parameters you want to use.
The last thing to do is making sure the parameter bindings are working:

<RuleDesigner
           Sentence="Learning custom activities with parameter Name = %1 and parameter FirstName = %2">
        <FieldBind <strong>Field="Name"</strong> DesignerType="TextArea"
           Id="1"/>
        <FieldBind <strong>Field="FirstName"</strong> DesignerType="TextArea" Id="2" />
      </RuleDesigner>
      <Parameters>
        <Parameter <strong>Name="Name"</strong> Type="System.String, mscorlib"
          Direction="In" />
        <Parameter <strong>Name="FirstName"</strong> Type="System.String, mscorlib"
          Direction="In" />
      </Parameters>
 
Make sure both the FieldBind’s Field property and the Parameter’s Name property are equal.
Inside the parameter tag, the type should be equal to your DependencyProperty type (which you have created inside the activity file).
I have two strings so I am using “System.String, mscorelib”.
The DesignerType inside the FieldBind is set to TextArea in this example. This TextArea supports (as far as I know) almost everykind of input (plaintext, Workflow variables, Current Item Properties, …). For a full list of DesignerTypes I refer to MSDN.
Register the assembly as an AuthorizedType
You must register the assembly that contains the custom activity as an AuthorizedType in the web.config file that is at the root of the Internet Information Services (IIS) server Web Application folder. Following is the entry to add in the authorizedTypes section.

<authorizedType Assembly="AssemblyName"
   Namespace="NameSpace" TypeName="*" Authorized="True" />
 
Now all the coding is done.
Copy the .actions file to the 14\TEMPLATES\1033\WorkFlows folder and do a IISRESET
Open SharePoint Designer create a new workflow on a list. If you followed these steps you should now see a category named Cross Site Actions when clicking the Actions button on top.

Workflow Helper Class
First thing I did was create a simple helper class where I stored logic to initiate the workflow and to check to see if there are any existing workflows running. The ItemHasActiveWorkflows method is really straight forward as it is searching workflows that are running for the specific item. If it finds a workflow is running, it will return false. Why is this important? Well because a document or list item can only have one of a particular type running at a time. But since we broke the larger business process up into five smaller ones, we need to check to see if any of the five are running on the item. If any one of them is running, we need to block any new workflows from being started. This method will take a comma delimited list of workflow names that must be checked.
The second method is StartWorkflow which will initiate a workflow on a specified item. This code is very straight forward.

public class WorkflowHelper
{

    private string siteCollectionUrl;
    private string webUrl;
    private string listName;
    private int itemID;
    private string workflowName;

    public WorkflowHelper(string siteCollectionUrl, string webUrl, string listName, int itemID)
    {
        this.siteCollectionUrl = siteCollectionUrl;
        this.webUrl = webUrl;
        this.listName = listName;
        this.itemID = itemID;
    }

    public WorkflowHelper(string siteCollectionUrl, string webUrl, string listName, int itemID, string workflowName) : this(siteCollectionUrl, webUrl, listName, itemID)
    {
        this.workflowName = workflowName;
    }

    public bool ItemHasActiveWorkflows(string workflowNames)
    {
        string[] seperator = new string[] {","};
        string[] workflowNamesSplit = workflowNames.Split(seperator, StringSplitOptions.RemoveEmptyEntries);

        //Get the site collection
        using (SPSite site = new SPSite(siteCollectionUrl))
        {
            //Get the web
            using (SPWeb web = site.OpenWeb(webUrl))
            {
                SPList list = web.Lists[listName];
                SPListItem workflowItem = list.GetItemById(itemID);

                //Loop over the workflows to look for
                foreach (string workflowName in workflowNamesSplit)
                {
                    //Loop over the workflows on the item
                    foreach (SPWorkflow workflow in workflowItem.Workflows)
                    {
                        //Check the state of the workflows to see if any are running
                        if (workflow.InternalState == SPWorkflowState.Running ||
                            workflow.InternalState == SPWorkflowState.Expiring ||
                            workflow.InternalState == SPWorkflowState.Faulting)
                        {
                            if (list.WorkflowAssociations[workflow.AssociationId].Name.ToLower() == workflowName.Trim().ToLower())
                            {
                                return true;
                            }
                        }
                    }
                }
            }
        }

        return false;
    }

    public void StartWorkflow()
    {
        //Open the site collection.  Cannot go across site collections in 
        //sandbox so easier to just pull of workflow context
        using (SPSite site = new SPSite(siteCollectionUrl))
        {
            //Just open web because we cannot go across site collections
            //with a sandbox solution.
            using (SPWeb web = site.OpenWeb(webUrl))
            {
                //Get the manager that will initiate the workflow
                SPWorkflowManager workflowMgr = site.WorkflowManager;

                //Get the available workflows on the list
                SPWorkflowAssociationCollection workflowAssocs = web.Lists[listName].WorkflowAssociations;

                //Get the list item that the workflow will be initiated on
                SPListItem workflowItem = web.Lists[listName].GetItemById(itemID);

                //Get the workflow that has been associated
                SPWorkflowAssociation workflowAssoc = workflowAssocs.GetAssociationByName(workflowName, System.Threading.Thread.CurrentThread.CurrentCulture);

                //Start the workflow
                workflowMgr.StartWorkflow(workflowItem, workflowAssoc, workflowAssoc.AssociationData, true);
            }
        }
    }
}
 
Start Workflow Action


Next I created an action that would start a second workflow. I created the following code.

public class InitWorkflowAction : Activity
{

    #region Properties

    public static DependencyProperty SiteCollectionUrlProperty = DependencyProperty.Register("SiteCollectionUrl", typeof(string), typeof(InitWorkflowAction));

    [Description("Site Collection URL")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public string SiteCollectionUrl
    {
        get
        {
            return ((string)(base.GetValue(InitWorkflowAction.SiteCollectionUrlProperty)));
        }
        set
        {
            base.SetValue(InitWorkflowAction.SiteCollectionUrlProperty, value);
        }
    }

    public static DependencyProperty WebUrlProperty = DependencyProperty.Register("WebUrl", typeof(string), typeof(InitWorkflowAction));

    [Description("Web Url")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public string WebUrl
    {
        get
        {
            return ((string)(base.GetValue(InitWorkflowAction.WebUrlProperty)));
        }
        set
        {
            base.SetValue(InitWorkflowAction.WebUrlProperty, value);
        }
    }

    public static DependencyProperty ListNameProperty = DependencyProperty.Register("ListName", typeof(string), typeof(InitWorkflowAction));

    [Description("List Name")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public string ListName
    {
        get
        {
            return ((string)(base.GetValue(InitWorkflowAction.ListNameProperty)));
        }
        set
        {
            base.SetValue(InitWorkflowAction.ListNameProperty, value);
        }
    }

    public static DependencyProperty WorkflowNameProperty = DependencyProperty.Register("WorkflowName", typeof(string), typeof(InitWorkflowAction));

    [Description("Workflow Name")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public string WorkflowName
    {
        get
        {
            return ((string)(base.GetValue(InitWorkflowAction.WorkflowNameProperty)));
        }
        set
        {
            base.SetValue(InitWorkflowAction.WorkflowNameProperty, value);
        }
    }

    public static DependencyProperty ItemIDProperty = DependencyProperty.Register("ItemID", typeof(int), typeof(InitWorkflowAction));

    [Description("Item ID")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public int ItemID
    {
        get
        {
            return ((int)(base.GetValue(InitWorkflowAction.ItemIDProperty)));
        }
        set
        {
            base.SetValue(InitWorkflowAction.ItemIDProperty, value);
        }
    }


    #endregion

    #region Methods

    protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
    {
        //WorkflowHelper workflowHelper = new WorkflowHelper(this.SiteCollectionUrl, this.WebUrl, this.ListName, this.ItemID, this.WorkflowName);
        //workflowHelper.StartWorkflow();

        WorkflowHelper workflowHelper = new WorkflowHelper(this.SiteCollectionUrl, this.WebUrl, this.ListName, this.ItemID, this.WorkflowName);
        Thread thread = new Thread(new ThreadStart(workflowHelper.StartWorkflow));
        thread.Start();

        return ActivityExecutionStatus.Closed;
    }

    #endregion

}

 
With the StartWorkflow method you see the code is straight forward but there some threading. You may be wondering why I have implemented threading? Well I was getting the below error message in the SharePoint logs when the workflow was initiated on the same thread. I did not find any information on why this was occurring but on a hunch I thought the workflow service was competing for the same resources. My solution was to move the call to StartWorkflow into a different thread and the problem was resolved.


w3wp.exe (0x2BC4) 0x0A2C SharePoint Foundation Workflow Infrastructure xmfh Medium Workflow Compile Failed: Invalid token for impersonation - it cannot be duplicated.


w3wp.exe (0x2BC4) 0x0A2C SharePoint Foundation Workflow Infrastructure 72fs Unexpected RunWorkflow: Microsoft.SharePoint.SPException: Invalid token for impersonation - it cannot be duplicated. at Microsoft.SharePoint.Workflow.SPNoCodeXomlCompiler.SubCompiler.DoCompile(WorkflowCompilerParameters parameters, String xomlSource, String assemblyName, CompilationPacket& packet, DirectoryInfo& tempDir) at Microsoft.SharePoint.Workflow.SPNoCodeXomlCompiler.CompileBytes(Byte[] xomlBytes, Byte[] rulesBytes, Boolean doTestCompilation, String assemblyName, SPWeb web, Boolean forceNewAppDomain) at Microsoft.SharePoint.Workflow.SPNoCodeXomlCompiler.LoadXomlAssembly(SPWorkflowAssociation association, SPWeb web) at Microsoft.SharePoint.Workflow.SPWinOeHostServices.LoadDeclarativeAssembly(SPWorkflowAssociation association) at Microsoft.SharePoint.Workflow.SPWinOeHostServi...


w3wp.exe (0x2BC4) 0x0A2C SharePoint Foundation Workflow Infrastructure 72fs Unexpected ...ces.CreateInstance(SPWorkflow workflow) at Microsoft.SharePoint.Workflow.SPWinOeEngine.RunWorkflow(SPWorkflowHostService host, SPWorkflow workflow, Collection`1 events, TimeSpan timeOut) at Microsoft.SharePoint.Workflow.SPWorkflowManager.RunWorkflowElev(SPWorkflow workflow, Collection`1 events, SPWorkflowRunOptionsInternal runOptions)


0x0A2C SharePoint Foundation Workflow Infrastructure 98d8 Unexpected Microsoft.SharePoint.SPException: Invalid token for impersonation - it cannot be duplicated. at Microsoft.SharePoint.Workflow.SPNoCodeXomlCompiler.SubCompiler.DoCompile(WorkflowCompilerParameters parameters, String xomlSource, String assemblyName, CompilationPacket& packet, DirectoryInfo& tempDir) at Microsoft.SharePoint.Workflow.SPNoCodeXomlCompiler.CompileBytes(Byte[] xomlBytes, Byte[] rulesBytes, Boolean doTestCompilation, String assemblyName, SPWeb web, Boolean forceNewAppDomain) at Microsoft.SharePoint.Workflow.SPNoCodeXomlCompiler.LoadXomlAssembly(SPWorkflowAssociation association, SPWeb web) at Microsoft.SharePoint.Workflow.SPWinOeHostServices.LoadDeclarativeAssembly(SPWorkflowAssociation association) at Microsoft.SharePoint.Workflow.SPWinOeHostServices.CreateIns...


w3wp.exe (0x2BC4) 0x0A2C SharePoint Foundation Workflow Infrastructure 98d8 Unexpected ...tance(SPWorkflow workflow) at Microsoft.SharePoint.Workflow.SPWinOeEngine.RunWorkflow(SPWorkflowHostService host, SPWorkflow workflow, Collection`1 events, TimeSpan timeOut) at Microsoft.SharePoint.Workflow.SPWorkflowManager.RunWorkflowElev(SPWorkflow workflow, Collection`1 events, SPWorkflowRunOptionsInternal runOptions)


Check Active Workflows Action


Below is the code that will check to see if there is an action running on a workflow item. The code is straight forward and calls my helper class that I created earlier.


public class CheckActiveWorkflowsAction : Activity
{

    #region Properties

    public static DependencyProperty SiteCollectionUrlProperty = DependencyProperty.Register("SiteCollectionUrl", typeof(string), typeof(CheckActiveWorkflowsAction));

    [Description("Site Collection URL")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public string SiteCollectionUrl
    {
        get
        {
            return ((string)(base.GetValue(CheckActiveWorkflowsAction.SiteCollectionUrlProperty)));
        }
        set
        {
            base.SetValue(CheckActiveWorkflowsAction.SiteCollectionUrlProperty, value);
        }
    }

    public static DependencyProperty WebUrlProperty = DependencyProperty.Register("WebUrl", typeof(string), typeof(CheckActiveWorkflowsAction));

    [Description("Web Url")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public string WebUrl
    {
        get
        {
            return ((string)(base.GetValue(CheckActiveWorkflowsAction.WebUrlProperty)));
        }
        set
        {
            base.SetValue(CheckActiveWorkflowsAction.WebUrlProperty, value);
        }
    }

    public static DependencyProperty ListNameProperty = DependencyProperty.Register("ListName", typeof(string), typeof(CheckActiveWorkflowsAction));

    [Description("List Name")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public string ListName
    {
        get
        {
            return ((string)(base.GetValue(CheckActiveWorkflowsAction.ListNameProperty)));
        }
        set
        {
            base.SetValue(CheckActiveWorkflowsAction.ListNameProperty, value);
        }
    }

    public static DependencyProperty ItemIDProperty = DependencyProperty.Register("ItemID", typeof(int), typeof(CheckActiveWorkflowsAction));

    [Description("Item ID")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public int ItemID
    {
        get
        {
            return ((int)(base.GetValue(CheckActiveWorkflowsAction.ItemIDProperty)));
        }
        set
        {
            base.SetValue(CheckActiveWorkflowsAction.ItemIDProperty, value);
        }
    }

    public static DependencyProperty HasActiveProperty = DependencyProperty.Register("HasActive", typeof(bool), typeof(CheckActiveWorkflowsAction));

    [Description("Has Active")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public bool HasActive
    {
        get
        {
            return ((bool)(base.GetValue(CheckActiveWorkflowsAction.HasActiveProperty)));
        }
        set
        {
            base.SetValue(CheckActiveWorkflowsAction.HasActiveProperty, value);
        }
    }

    public static DependencyProperty WorkflowNamesProperty = DependencyProperty.Register("WorkflowNames", typeof(string), typeof(CheckActiveWorkflowsAction));

    [Description("Workfow Names")]
    [Category("Custom Workflow")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public string WorkflowNames
    {
        get
        {
            return ((string)(base.GetValue(CheckActiveWorkflowsAction.WorkflowNamesProperty)));
        }
        set
        {
            base.SetValue(CheckActiveWorkflowsAction.WorkflowNamesProperty, value);
        }
    }

    #endregion

    #region Methods

    protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
    {
        WorkflowHelper workflowHelper = new WorkflowHelper(this.SiteCollectionUrl, this.WebUrl, this.ListName, this.ItemID);
        this.HasActive = workflowHelper.ItemHasActiveWorkflows(this.WorkflowNames);
            
        return ActivityExecutionStatus.Closed;
    }

    #endregion
}
 

 
Deployment


The deployment of this solution was pretty easy with Visual Studio 2010.


· I created an empty SharePoint 2010 project.


· Added the above code.


· Added a Site Collection level Feature.


· Added a SharePoint mapped folder called “Workflow”


· Create a new file called InitWorkflowAction.actions in the Workflow folder.


The InitWorkflowAction.actions file describes my custom actions and makes them available to design tools such as SharePoint Designer 2010 to create a declarative workflow. Information below is very straight forward. It defines the sentence users will fill in and what type of data will be consumed or returned from the methods I created earlier.

 
<WorkflowInfo>
  <Actions Sequential="then" Parallel="and">
    <Action Name="Start Workflow"
            Assembly="My.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7cb64b163bd9c1db"
            ClassName="My.Activities.InitWorkflowAction"
            AppliesTo="all"
            Category="Custom Workflow">
      <RuleDesigner Sentence="Initiate workflow %4 on item %5 in list %3 in Web %2 in Site Collection %1">
        <FieldBind Field="SiteCollectionUrl"
                   Text="Site Collection Url"
                   Id="1"
                   DesignerType="TextBox" />
        <FieldBind Field="WebUrl" 
                   Text="Web Url"
                   Id="2"
                   DesignerType="TextBox" />
        <FieldBind Field="ListName"
                   Text="List Name"
                   Id="3"
                   DesignerType="TextBox" />
        <FieldBind Field="WorkflowName"
                   Text="Workflow Name"
                   Id="4"
                   DesignerType="TextBox" />
        <FieldBind Field="ItemID"
                   Text="Item ID"
                   Id="5"
                   DesignerType="Integer" />
      </RuleDesigner>
      <Parameters>
        <Parameter Name="SiteCollectionUrl"
                   Type="System.String, mscorlib"
                   Direction="In"
                   DesignerType="TextBox"
                   Description="Site Collection Url"/>
        <Parameter Name="WebUrl"
                   Type="System.String, mscorlib"
                   Direction="In"
                   DesignerType="TextBox"
                   Description="Web Url"/>
        <Parameter Name="ListName"
                   Type="System.String, mscorlib"
                   Direction="In"
                   DesignerType="TextBox"
                   Description="List Name"/>
        <Parameter Name="WorkflowName"
                   Type="System.String, mscorlib"
                   Direction="In"
                   DesignerType="TextBox"
                   Description="Workflow Name"/>
        <Parameter Name="ItemID"
                   Type="System.Int32, mscorlib"
                   Direction="In"
                   DesignerType="Integer"
                   Description="Item ID"/>
      </Parameters>
    </Action>
    <Action Name="Check Active Workflow"
            Assembly="My.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7cb64b163bd9c1db"
            ClassName="My.Activities.CheckActiveWorkflowsAction"
            AppliesTo="all"
            Category="Custom Workflow">
      <RuleDesigner Sentence="Check if workflows %6 on item item %4 in list %3 in Web %2 in Site Collection %1 has a running workflow (Output to %5)">
        <FieldBind Field="SiteCollectionUrl"
                   Text="Site Collection Url"
                   Id="1"
                   DesignerType="TextBox" />
        <FieldBind Field="WebUrl"
                   Text="Web Url"
                   Id="2"
                   DesignerType="TextBox" />
        <FieldBind Field="ListName"
                   Text="List Name"
                   Id="3"
                   DesignerType="TextBox" />
        <FieldBind Field="ItemID"
                   Text="Item ID"
                   Id="4"
                   DesignerType="Integer" />
        <FieldBind Field="HasActive"
                   Text="Has Active"
                   Id="5"
                   DesignerType="ParameterNames" />
        <FieldBind Field="WorkflowNames"
                   Text="Comma delimited list of workflow names"
                   Id="6"
                   DesignerType="TextBox" />
      </RuleDesigner>
      <Parameters>
        <Parameter Name="SiteCollectionUrl"
                   Type="System.String, mscorlib"
                   Direction="In"
                   DesignerType="TextBox"
                   Description="Site Collection Url"/>
        <Parameter Name="WebUrl"
                   Type="System.String, mscorlib"
                   Direction="In"
                   DesignerType="TextBox"
                   Description="Web Url"/>
        <Parameter Name="ListName"
                   Type="System.String, mscorlib"
                   Direction="In"
                   DesignerType="TextBox"
                   Description="List Name"/>
        <Parameter Name="ItemID"
                   Type="System.Int32, mscorlib"
                   Direction="In"
                   DesignerType="Integer"
                   Description="Item ID"/>
        <Parameter Name="HasActive"
                   Type="System.Boolean, mscorlib"
                   Direction="In"
                   DesignerType="ParameterNames"
                   Description="Has Active"/>
        <Parameter Name="WorkflowNames"
                   Type="System.String, mscorlib"
                   Direction="In"
                   DesignerType="TextBox"
                   Description="Comma delimited list of workflow names"/>
      </Parameters>
    </Action>
  </Actions>
</WorkflowInfo>

Using Custom Actions in SharePoint Designer 2010


Now I am going to show you how I used the custom actions in my workflow. Earlier in this series you saw that I had created five different SharePoint Visio 2010 workflow diagrams. I am going to skip documenting the steps on how to bring the Visio diagrams into SharePoint Designer as that is a pretty straight forward process.


Below is a screenshot of the Initiate Process workflow. The first Action is my custom action that checks to see if there is already a workflow running for the custom list item. You will notice that the sentence displayed here corresponds with the sentence in the InitWorkflowAction.actions file. Also note the first parameter is a comma delimited list of all the other workflows (Review,Manager Review,Resubmit Request,Complete Request).


Next I take the value returned from my custom action and check to see if it Yes or No. If it is No, that means there are no running workflows and that I should initiate the Review Workflow. The third sentence in the diagram below captures this. I will initiate the Review workflow on the current item and send a notification email. Otherwise the workflow will not be started and the user will be notified.
 
 Note that for the configuration of this workflow, it is the only one that supports allowing the workflow to be started manually. All of the other workflows I have created have no checkboxes checked for the Start Options section. This reduces the potential for user errors when starting the workflow for the item. The reason why is that we always want users to start with the Initiate Process workflow and let the other workflows be started based on business rules.
 
 Next I have another workflow called Review that corresponds to the Visio diagram in the previous part of this series. Notice again I simply wire into the custom action StartWorkflow to start Manager Review, Complete Review or Resubmit Request.
 
 Below is the Manager Review workflow. As you can see if the Manager approves the Complete Request workflow will be called otherwise the Resubmit Request workflow will be called.
 
 Next is the Resubmit Request workflow. The workflow checks to see if the user cancels the workflow. If they do, the item will be updated and an email will be sent. Otherwise, the item will go back to the Review workflow (for re-review).
 Finally the Complete Request workflow is below. I have not done much with it at this time but more can be added.
 So now once I validate and deploy my workflow I can initiate it on an item. Below is a screenshot of a user initiate the process for an item. Notice only the Initiate Process workflow is available.

Once I have started the workflow I come back to the workflow status screen. Here you see the Review workflow is In Progress. Notice that the Initiate Process workflow is still available. Since the workflow is in progress I want to make sure that I cannot initiate the entire workflow again while it is current running.

Below is a screenshot of the completed workflow.
 Sandbox Solutions


I alluded earlier that I really wanted to make this a sandbox solution. Why? Because I believe that you should always develop for the Sandbox first because it quick to deploy solutions, it is secure, ensures good performance and works well with Office365.


Here are some good resources on how to deploy actions into the SharePoint 2010 Sandbox:

The process is a little different and the code above would change a tiny bit so that it is scoped to a Site Collection only. However fundamentally there is not much difference.


When I deployed the custom actions and created the workflows I would get errors initiating the workflows. I went into the logs and found the following.


SPUCWorkerProcessProxy.exe (0x1E70) 0x1EC0 SharePoint Foundation Workflow Infrastructure 72er Medium System.NullReferenceException: Object reference not set to an instance of an object. at Microsoft.SharePoint.Library.SPRequest.ClearAllVars(String bstrUrl) at Microsoft.SharePoint.SPListItem.PrepareItemForUpdate(SPWeb web, Boolean bMigration, Boolean& bAdd, Boolean& bPublish, Object& objAttachmentNames, Object& objAttachmentContents, Int32& parentFolderId) at Microsoft.SharePoint.SPListItem.UpdateInternal(Boolean bSystem, Boolean bPreserveItemVersion, Guid newGuidOnAdd, Boolean bMigration, Boolean bPublish, Boolean bNoVersion, Boolean bCheckOut, Boolean bCheckin, Boolean suppressAfterEvents, String filename) at Microsoft.SharePoint.SPListItem.Update() at Microsoft.SharePoint.Workflow.SPWinOEWSSService.CommitUpdateListItem(Transaction txn, Object[] transData)


SPUCWorkerProcessProxy.exe (0x1E70) 0x1EC0 SharePoint Foundation Workflow Infrastructure 72fe High Error in commiting pending workflow batch items: System.NullReferenceException: Object reference not set to an instance of an object. at Microsoft.SharePoint.Library.SPRequest.ClearAllVars(String bstrUrl) at Microsoft.SharePoint.SPListItem.PrepareItemForUpdate(SPWeb web, Boolean bMigration, Boolean& bAdd, Boolean& bPublish, Object& objAttachmentNames, Object& objAttachmentContents, Int32& parentFolderId) at Microsoft.SharePoint.SPListItem.UpdateInternal(Boolean bSystem, Boolean bPreserveItemVersion, Guid newGuidOnAdd, Boolean bMigration, Boolean bPublish, Boolean bNoVersion, Boolean bCheckOut, Boolean bCheckin, Boolean suppressAfterEvents, String filename) at Microsoft.SharePoint.SPListItem.Update() at Microsoft.SharePoint.Workflow.SPWinOEWSSService.CommitUpdateListItem...


SPUCWorkerProcessProxy.exe (0x1E70) 0x1EC0 SharePoint Foundation Workflow Infrastructure 72fe High ...(Transaction txn, Object[] transData) at Microsoft.SharePoint.Workflow.SPPendingWorkBatch.Commit(Transaction transaction, ICollection items)


SPUCWorkerProcessProxy.exe (0x1E70) 0x1EC0 SharePoint Foundation Workflow Infrastructure 88xr Unexpected WinWF Internal Error, terminating workflow Id# 8d547a01-bdb8-44ac-b066-586c12367dc0


The information that was provided was not very good. Here are some observations I made.


· Note that the error is in SPUCWorkerProcessProxy.exe and not w3wp.exe. This basically means the entire workflow is running inside the Sandbox service because it has a reference to an action that is deployed there.


· I would only get this error the very first time the workflow run. From then on, it worked every time flawlessly. However if I stopped and restarted the Sandbox service manually, I would get the error again.


· I would also not get the error if Workflow A started a very simple Workflow B. For instance if Workflow B was sending an email, no issues. However if Workflow B had an approval process there would be an error.


After doing some research I came to the conclusion that this error occurs based on a combination of a few things.


1. When you build a workflow in SharePoint Designer 2010 it is a declarative workflow, not a complied workflow. The workflow will be compiled the first time the workflow is initiated. This is why it always takes a long time to start a workflow the very first time.


2. Sandbox solutions have a threshold of 20 seconds before it is determined the process too intensive and will stop the execution.


This combination would push me over the 20 seconds rule the very first time running the workflow. The solution would be to write some sort of script that could manually go through each workflow and initiating them so they would be compiled and ready to go when the user comes to manually start the workflow. I did not find that to be an acceptable solution because the Sandbox cache will be remove items from memory if they are not used often. This is why I did a full trust solution.


Next Part


Hopefully this gave you a pretty good idea of how simple it is to achieve this vision I have. Now in the next part of this series I am going to extend the solution a little bit to support my reporting requirements using Visio Services.





2 comments:

  1. Your blog has always been an inspiration to me. You always share something very interesting and come up with a new approach to surprise all. I do appreciate you for your hard work and knowledge.
    electronic signature for sharepoint

    ReplyDelete
  2. Not an easy way for for the person who just wants to solve particular problem. For those who value their time, I propose to draw an attention to the following list of activities http://www.harepoint.com/Products/HarePointWorkflowExtensions/ListActivities.aspx
    that allows instantly get the result for the vast majority of problems that arise during workflow development in SharePoint Designer.

    ReplyDelete