Situation
Wouldn’t it be nice to start a specific SharePoint list workflow on each item in a list? Unfortunately, there’s no simple way to do this out of the box in SharePoint or Nintex, even though it’s just a few lines of code. You could always grab all of the list items and then loop through each one and start a workflow, but this is complicated and cumbersome.
Solution
Using Nintex Workflow 2010, it’s incredibly easy to create a simple to use and configure workflow action that can start any workflow associated with a list. I’m not going to go into detail on how to create a Nintex action, their help document does a pretty good job, but I will say you need to have the Nintex Workflow SDK installed in order to create an action. Once you have your action created, there are two interesting items left to complete: your action’s Execute method and the custom dialog. Everything else from the default action process remains the same.
Custom Execute Method
The default Execute action is rather bland, but has a ton of potential. The Execute method is where the magic happens, and you can do anything the user could do. That’s something to keep in mind, because the same security still applies, unless your workflow is run from the farm account, but you shouldn’t count on that.
Anyway, you want to grab the List ID that the workflow author provided along with the Workflow Name and any startup data that the workflow will need. These are all strings, because that’s what you get back from all of the Nintex configuration controls. Since my version of the Workflow Data allows the user to provide anything in the XML, you need to resolve the context data before using it. Once you have the List ID, Workflow Name, and Workflow Data, you’re ready to go. Just grab the list, get the corresponding WorkflowAssociation, and loop through each item starting the WorkflowAssociation on the item.
One thing I want to point out is you can start the WorkflowAssociation synchronously or asynchronously. I have the workflow starting synchronously so if one item fails, my action fails. The choice is up to you and asynchronous will be faster generally.
Without further ado, do the code:
1: /// <summary>
2: /// Main execution thread. Pulls back the workflow to run and starts it synchronously on each item in the list successively
3: /// </summary>
4: /// <param name="executionContext">Execution context this activity is running under</param>
5: /// <returns>The overall execution status of the activity</returns>
6: protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
7: {
8: //Check that we're allowed to run
9: ActivityActivationReference.IsAllowed(this, this.__Context.Web);
10:
11: //Grab the context and log start progress
12: NWWorkflowContext context = NWWorkflowContext.GetContext(this.__Context, new Guid(this.__ListId), this.__ListItem.Id, this.WorkflowInstanceId, this);
13: base.LogProgressStart(context);
14:
15: //Resolve the input names
16: Guid resolvedListName = new Guid(this.ListName);
17: string resolvedWorkflowName = this.WorkflowName;
18: string resolvedWorkflowData = context.AddContextDataToString(this.WorkflowData);
19:
20: //Get the list
21: SPList list = this.__Context.Web.Lists[resolvedListName];
22:
23: //Get the workflow
24: SPWorkflowManager manager = this.__Context.Site.WorkflowManager;
25: SPWorkflowAssociation association = list.WorkflowAssociations.GetAssociationByName(resolvedWorkflowName, CultureInfo.InvariantCulture);
26:
27: /* Loop using a counter because:
28: * 1) Starting a workflow can take a long time to complete and the items may change between start and finish
29: * 2) Starting a workflow on a list item actually changes the list item and will cause a foreach loop on the list to fail
30: */
31: int count = list.ItemCount;
32: for(int i = 0; i < count; i++)
33: {
34: SPListItem item = list.Items[i];
35:
36: //Start the workflow synchronously on the list item with the given data
37: manager.StartWorkflow(item, association, resolvedWorkflowData);
38: }
39:
40: base.LogProgressEnd(context, executionContext);
41: return base.Execute(executionContext);
42: }
Everything that happens inside the for loop can by changed for your purposes to suit any sort of looping you want. You don’t have to start a workflow, you could use this construct to change every item in a list.
Custom Dialog
The custom dialog is the popup that Nintex has SharePoint display when you’re configuring an action. This is an ASP.NET page and you can write code-behind or client-side script for it. I have gone the client-side script route. Accessing the Workflows available for a List from the SharePoint JavaScript Client Object Model was covered in a previous blog (SharePoint 2010: Getting Workflows for a List from the JavaScript Client Object Model), but the code is included for reference.
Here, we’re going to provide the user with a list of the Lists in the current site. Based on the List selected, we’ll then show a list of applicable WorkflowAssociations to choose from. On the backend, we’ll be storing the List’s ID and the WorkflowAssociation’s Name, because each is unique. The last thing we need the user to provide is the Workflow Data. It’s possible to pull the required workflow fields from the workflow, but I haven’t for the sake of brevity and showing what the data actually looks like. Note that no validation occurs in the custom dialog.
To the code:
1: <%@ Page Language="C#" DynamicMasterPageFile="~masterurl/default.master" AutoEventWireup="true"
2: CodeBehind="StartListWorkflowDialog.aspx.cs" EnableEventValidation="false" Inherits="PB.SharePoint.StartListWorkflowDialog, $SharePoint.Project.AssemblyFullName$" %>
3:
4: <%@ Register TagPrefix="Nintex" Namespace="Nintex.Workflow.ServerControls" Assembly="Nintex.Workflow.ServerControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=913f6bae0ca5ae12" %>
5: <%@ Register TagPrefix="Nintex" TagName="ConfigurationPropertySection" Src="~/_layouts/NintexWorkflow/ConfigurationPropertySection.ascx" %>
6: <%@ Register TagPrefix="Nintex" TagName="ConfigurationProperty" Src="~/_layouts/NintexWorkflow/ConfigurationProperty.ascx" %>
7: <%@ Register TagPrefix="Nintex" TagName="DialogLoad" Src="~/_layouts/NintexWorkflow/DialogLoad.ascx" %>
8: <%@ Register TagPrefix="Nintex" TagName="DialogBody" Src="~/_layouts/NintexWorkflow/DialogBody.ascx" %>
9: <%@ Register TagPrefix="Nintex" TagName="SingleLineInput" Src="~/_layouts/NintexWorkflow/SingleLineInput.ascx" %>
10: <%@ Register TagPrefix="Nintex" TagName="PlainTextWebControl" Src="~/_layouts/NintexWorkflow/PlainTextWebControl.ascx" %>
11: <asp:Content ID="ContentHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead"
12: runat="server">
13: <Nintex:DialogLoad runat="server" />
14: <script type="text/javascript" src="/_layouts/NintexWorkflow/CustomActions/StartListWorkflow/Scripts/jquery-1.6.2.min.js"></script>
1:
2: <script type="text/javascript">
3: function TPARetrieveConfig() {
4: document.getElementById('<%=listNameSelector.ClientID %>').value = configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='ListName']/PrimitiveValue/@Value").text;
5: loadWorkflows();
6: setPlainTextEditorText('<%=workflowDataControl.ClientID %>', configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='WorkflowData']/PrimitiveValue/@Value").text);
7: }
8:
9: function TPAWriteConfig() {
10: var listNameControl = document.getElementById('<%=listNameSelector.ClientID %>');
11: if (listNameControl.value.length > 0) {
12: configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='ListName']/PrimitiveValue/@Value").text = listNameControl.value;
13: }
14:
15: var workflowNameControl = document.getElementById('<%=workflowNameSelector.ClientID %>');
16: if (workflowNameControl.value.length > 0) {
17: configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='WorkflowName']/PrimitiveValue/@Value").text = workflowNameControl.value;
18: }
19:
20: configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='WorkflowData']/PrimitiveValue/@Value").text = getStringFromPlainTextEditor('<%=workflowDataControl.ClientID%>');
21:
22: return true;
23: }
24:
25: function loadWorkflows() {
26: var targetListGuid = document.getElementById('<%=listNameSelector.ClientID %>').value;
27: if (targetListGuid) {
28: this.guid = new SP.Guid(targetListGuid);
29: var context = SP.ClientContext.get_current();
30: var lists = context.get_web().get_lists();
31: var list = lists.getById(this.guid);
32: this.workflows = list.get_workflowAssociations();
33: context.load(this.workflows);
34: context.executeQueryAsync(Function.createDelegate(this, onQuerySucceeded), Function.createDelegate(this, onQueryFailed));
35: }
36:
37: function onQuerySucceeded(sender, args) {
38: $('#<%=workflowNameSelector.ClientID %>').html('');
39: var enumerator = this.workflows.getEnumerator();
40: while (enumerator.moveNext()) {
41: var workflow = enumerator.get_current();
42: if (workflow.get_enabled() && workflow.get_allowManual()) {
43: var workflowName = workflow.get_name();
44: $('#<%=workflowNameSelector.ClientID %>').append('<option value="' + workflowName + '">' + workflowName + '</option>');
45: }
46: }
47:
48: if (document.getElementById('<%=listNameSelector.ClientID %>').value === configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='ListName']/PrimitiveValue/@Value").text) {
49: document.getElementById('<%=workflowNameSelector.ClientID %>').value = configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='WorkflowName']/PrimitiveValue/@Value").text;
50: }
51: }
52:
53: function onQueryFailed(sender, args) {
54: alert('Unable to retrieve workflows: ' + args.get_message());
55: }
56: }
57:
58: $(document).ready(function () {
59: $('#<%=listNameSelector.ClientID %>').change(function () {
60: loadWorkflows();
61: });
62: });
63:
64: onLoadFunctions[onLoadFunctions.length] = function () {
65: dialogSectionsArray["<%= MainControls1.ClientID %>"] = true;
66: };
67:
</script>
15: </asp:Content>
16: <asp:Content ID="ContentBody" ContentPlaceHolderID="PlaceHolderMain" runat="Server">
17: <Nintex:DialogBody runat="server" ID="DialogBody" />
18: <Nintex:ConfigurationPropertySection runat="server" ID="MainControls1">
19: <TemplateRowsArea>
20: <Nintex:ConfigurationProperty runat="server" FieldTitle="List Name"
21: RequiredField="true">
22: <TemplateControlArea>
23: <Nintex:ListSelector ID="listNameSelector" runat="server">
24: </Nintex:ListSelector>
25: </TemplateControlArea>
26: </Nintex:ConfigurationProperty>
27: <Nintex:ConfigurationProperty runat="server" FieldTitle="Workflow Name"
28: RequiredField="true">
29: <TemplateControlArea>
30: <Nintex:InputDropDownList ID="workflowNameSelector" runat="server">
31: </Nintex:InputDropDownList>
32: </TemplateControlArea>
33: </Nintex:ConfigurationProperty>
34: <Nintex:ConfigurationProperty runat="server" FieldTitle="Workflow Data"
35: RequiredField="true">
36: <TemplateControlArea>
37: <Nintex:PlainTextWebControl ID="workflowDataControl" runat="server" Width="100%" />
38: </TemplateControlArea>
39: </Nintex:ConfigurationProperty>
40: </TemplateRowsArea>
41: </Nintex:ConfigurationPropertySection>
42: </asp:Content>
There you have it: a Nintex Workflow action that will start a selected workflow on every list item for a given list. The code isn’t all that hard, once you know what you’re doing in Nintex. It’s all about passing elements back and forth successfully and getting the right data to the right place. Now that some of the effort is demystified, what will you make next?