Kicking Off A Sharepoint Workflow from a Button, and [Much] More!

<UPDATE date=”2011-02-14″>: As of jQuery 1.5, single quotes are *required* around z:row or any other similar node selector. This was actually “required” in previous versions of jQuery, but not enforced.

$(xData.responseXML).find("[nodeName='z:row']")

</UPDATE>

Cross-posted from EndUserSharePoint.com

A little while ago, I noticed that a fellow named Rob Doyle was asking some interesting questions about working with workflows in the SPServices Discussions. If you’re not familiar with SPServices, it’s my jQuery library which helps you to relatively easily work with the SharePoint Web Services. It also gives you what I call ‘value-added functions” which can really easily let you dress up your list and library forms to enhance the user experience and improve data quality. (There’s the sales pitch. Now go download that wonderful, free software!)

As we went back and forth on some issue he was having it became clear that he was building something pretty cool, using SPServices as one part of his toolkit. It sounded like it would be a good article to write up and share, so here it is. Rob was kind enough to do the writing, capture all of the screenshots, and clean everything up for general consumption. I just did a little editing, but the story is Rob’s. I hope you enjoy this inventive solution and great use of my SPServices library.

The Business Case

Recently, my team and I were asked to create an enterprise application to assist users in completing a convoluted and infrequently utilized – but absolutely required – business process: expense requests and purchase reports. The process itself can change as a result of the type of request being submitted, and many of the nuances are frequently overlooked. This leads to backtracking and lost productivity at both ends. Sound familiar?

Our goal was to create an electronic form which built in the dynamic business rules of the process, but which did not overwhelm the user. The solution ended up being a blend of InfoPath for the input form, SharePoint for the data storage and workflow processing, and jQuery (and specifically Marc Anderson’s SPServices library) for the UI layer.

clip_image002
Figure 1 – Entry Form for new requests

I designed the system to be a series of dashboards for the user, with only minimal interaction with their record requests. All actions that were allowable on a particular record would be shown in a table along with your standard identifiable details about the record itself.

An example:

clip_image004
Figure 2 – User Dashboard

Here, the individual records can either be simply Viewed or officially Logged to our final system of record. In other views, a record might be able to be deleted, copied, or have its status otherwise changed. All these actions were enabled or disabled according to the business process.

In general, when hitting one of the buttons the form itself appears in Edit or View mode, or a custom SharePoint Designer workflow is kicked off. The View page is a page with a single record data view and with a style sheet made to look similar to the InfoPath form using standard CSS techniques. Importantly, the site Master Page is not attached so that we can display the view page in a popup window.
clip_image006
Figure 3 – ‘View’ record appears in an AJAX iFrame

Kicking off the workflows: Dataview Setup

The workflows themselves are kicked off using the SPServices “StartWorkflow” method. I’ve modified the function a bit to include an argument I named “wfID” to indicate which button was running which workflow. So if wfID = “delT” then the Delete workflow is run; if wfID=”confT” then the Confirm workflow is run. This allowed me to use this same function for all of the workflows in the entire application which updated a record.

On the page, all buttons using this workflow had their onclick event set as follows:

<a href="#" class="button">
  <xsl:attribute name="onclick">
    <xsl:text>javascript:StartWorkflow(&apos;https://yoururlhere.com</xsl:text>
    <xsl:value-of select="@FileRef" disable-output-escaping="yes" />
    <xsl:text>&apos;,&apos;</xsl:text>
    <xsl:value-of select="@ID" disable-output-escaping="yes" />
    <xsl:text>&apos;, &apos;confT&apos;);</xsl:text>
  </xsl:attribute>
  <xsl:attribute name="id">
    <xsl:text>buttonID</xsl:text>
    <xsl:value-of select="@ID" />
  </xsl:attribute>
  <span>Confirm</span>
</a>

Looks like a lot, right? It is a lot of code to add in, but doing it in this manner allowed some huge flexibility with the solution: I was able to kick off any of the workflows on an item just by clicking that button and using literally the exact same JavaScript code. The workflow itself gets passed the ItemURL, its corresponding ID, and an argument which tells the JavaScript function which workflow to run.

The call to the function is in this form:

onclick="javascript:Startworkflow('http://yoururl/itemsite/itemURL.xml’, '1048’, 'workflow1’)"

Breaking it down:

The button itself is actually an anchor tag (<A>) with a class attached to it. This allows the same CSS code to be assigned to all the buttons, so I could copy and paste the entire block of button code, and change two items and be done.

<a href="#" class="button">

Then we use the power of XSL in the data view. Using the <xsl:attribute> tag allowed me to add additional attributes to the A tag and construct the code dynamically. The <xsl:text> tag allows us to add literal text to the line as well:

<xsl:attribute name="onclick">
  <xsl:text>javascript:StartWorkflow(&apos;https://yoururlhere.com</xsl:text>

This results in the following line being added to the dataview:

onclick="javascript:StartWorkflow(“https://yoururlhere.com

Then we add the file’s location, ID, and our workflow argument (along with the requisite commas and apostrophes), so that the workflow knows which item we are processing:

  <xsl:value-of select="@FileRef" disable-output-escaping="yes" />
  <xsl:text>&apos;,&apos;</xsl:text>
  <xsl:value-of select="@ID" disable-output-escaping="yes" />
  <xsl:text>&apos;, &apos;confT&apos;);</xsl:text>
</xsl:attribute>

Note the “confT” argument above – the function will be looking for this so that it knows which workflow to execute. …and that’s it for the JavaScript function call setup.

Then we close out the A tag with a span that includes the text to be shown on the button, and allows us to use this same exact line with every single button. The CSS formatting is beyond the scope of this article, but you can look up the term “sliding doors” and find any number of tutorials.

  <span>Confirm</span>
</a>

Now, when the dataview is displayed on the page, everything is updated with the correct ItemURL and ItemID as needed. So why do all this? Well, when a new button is required in another spot on the page, or on another page, only two items need to be updated: the workflow argument (“confT” above) and the text on the button itself. That’s it! The entire set of code as above can be pasted inside a <td> in the dataview and updated as needed.

Maintainability

Now that we’ve got the setup out of the way, let’s take a look at the JavaScript itself. In a less complex situation, we might hardcode the workflow values on every page, or have a distinct function that runs a specific workflow. There are two problems with this approach, however:

  • Maintainability – it is monstrously difficult to update every single dataview in your site with the required values, and
  • SharePoint is finicky when it comes to using workflows in this manner. The Web Service requires that a specific and current “TemplateID” be passed to the request – sort of the GUID for the workflow itself – and this TemplateID changes every time the workflow is updated.

There was no way I was going to update every dataview in our entire application (there are 12) every time the workflow changed. Our approach to minimize this was to use a lesser-known aspect of the Content Editor Web Part (and later this was incorporated directly into the master page).

We added the CEWP to the bottom of every page which included customized workflow calls and all our reference files, including the jQuery and SPServices libraries. Then the CEWP was pointed at a txt file which resided in a document library (and later in the _layouts folder). This meant that since all the distinct pages required the same supporting code, they could share this one file, and the file itself could be maintained in one location. Any time the workflow TemplateID changed – no problem – a single update and we were done! Hundreds of buttons were automatically updated the next time the page loaded.

clip_image008
Figure 4 – CEWP with codefile.txt reference

The JavaScript call itself

Here’s a sample of the JavaScript call which kicks off the majority of the workflows:

function StartWorkflow(ItemURL, ItemID, wfID) {
  if (wfID == "wrT") {
    tempID = "{7acb9ced-bd58-4ea4-993e-787a0773f1db}"; wfname = "withdraw?"; alert("you are running workflow" +tempID)
  } else if (wfID == "wrE") {
    tempID = "{a6b9e3d5-fc89-428b-b861-0af09b56a4cc}";  wfname = "withdraw this expense report?";
  } else if (wfID == "logT") {
    tempID = "{6d28a7fb-cffb-489b-966a-27efd6fc1082}";  wfname = "mark this request as logged?";
  } else if (wfID == "logE") {
    tempID = "{cf69a2bc-e80b-4ee0-a3bd-b3577156996d}"; wfname = "mark this expense report as logged?";
  } else if (wfID == "delT") {
    tempID = "{8a914bcb-b54b-4ef3-803a-47a7e1aef30f}";  wfname = "delete this request?";
  } else if (wfID == "delE") {
    tempID = "{022276b6-3032-4354-b52f-6afdd2898e66}";  wfname = "delete this expense report?";
  } else if (wfID == "confT") {
    tempID = "{02449161-b2f6-4f90-acad-174dd42c4617}";  wfname = "confirm this request as being withdrawn?";
  } else if (wfID == "confE") {
    tempID = "{dfc7ed43-0c7e-4c50-a4d8-18df749743d5}";  wfname = "confirm this expense report as being withdrawn?";
  } else {
    alert("Sorry, you've enountered an error! Please contact the helpdesk immediately");
  }

  document.body.style.cursor = "wait";
  var r = confirm("Are you sure you wish to " + wfname);

  if (r==true) {
  $().SPServices({
    operation: "StartWorkflow",
    item: ItemURL,
    debug: true,
    templateId: tempID,
    workflowParameters: "<root />",
    completefunc: function(xData, Status) {
      var out =  $().SPServices.SPDebugXMLHttpResult({node:  xData.responseXML});
    }
  });
  $("#rowID" + ItemID).find("td").fadeOut(1000, function(){
    $(this).parent().remove();});
    document.body.style.cursor = "default";
  } else {
    alert("Cancelled!");
    document.body.style.cursor = "default";
  }
}

The code itself is very simple. The only modifications made have been to allow the additional “wfID” argument to be passed, and the corresponding templateID is inserted depending on the button pushed. That’s it! All the records are updated, emails are sent, and the record itself is removed from the screen without a time-consuming page refresh.

This line finds the table row which has been assigned in the code using the ItemID from the list, fades it out slowly, and then removes it completely from the table.

$("#rowID" + ItemID).find("td").fadeOut(1000, function(){
  $(this).parent().remove();
});

Because of the way the system is designed, that row will not appear again in this page, because its status has now changed as a result of the workflow successfully completing.

The Copy Function

One of the core requirements of the system was that users be able to copy previous requests in order to get a head start on filling everything out. This presented a number of challenges in our design, not the least of which is the common “I don’t know the ID of an item until after it’s been created” issue.

One of my design prerequisites was that the user never be interrupted from their personal process flow. This meant that on all dashboards, the system would present a ‘View’ or ‘Edit’ mode on a record on top of the dashboard itself. This allowed the user to finish reviewing or filling out their form, and go right back to the dashboard without interrupting their main flow, and without causing a single page refresh.

The copy function is kicked off using the exact same principles and xsl code in the dataview as all the other workflows, although the function name is different. Our call is instead:

onclick="javascript:CopyWorkflow('ItemURL’, 'ItemID’);"

The function itself does the following things:

  1. Alerts the user with a message box that this process takes a moment or two
  2. Places a jQuery overlay with a “processing…” icon over the screen so that the user can’t click on anything else during this process
  3. Runs a workflow which copies a blank version of the InfoPath form from a system library
  4. The workflow then updates that new destination form with all the values from the copied source
  5. Runs a modified version of Marc Anderson’s invaluable “GetLastItemID” function that pulls the last ID of the InfoPath file which matches the current user to a custom field called “DocOwner”
  6. Opens the new form in a new window on the same page
  7. Closes the overlay

Seems sort of a roundabout way of doing things, but is what you need to do in order to copy record values from an InfoPath form. Simply copying the document unfortunately doesn’t work. In the script, the code is pushed the same two arguments as the standard StartWorkflow function, and does not require the wfID argument, since we are always running the Copy workflow function.

<script type="text/javascript">
function CopyWorkflow(ItemURL, ItemID)
{
  var Itempath = "https://yoursystemURL.com" + ItemURL;
  alert("Please wait - this function may take a few moments");
  var overlay2 = jQuery('<div id="overlay2"><img src="loader.gif" /><p>Your new form will appear in a new window momentarily</p></div>');
  overlay2.appendTo(document.body);
  $().SPServices({
    operation: "StartWorkflow",
    item: Itempath,
    debug: true,
    templateId: "{4c9edb2c-b6f4-4c91-97c6-641b3219ec1e}",
    workflowParameters: "<root />",
    completefunc: function(xData, Status) {
      var lastdoc = $().SPServices.SPGetLastDocId({ listName: "PreTravel" });
      var out =  $().SPServices.SPDebugXMLHttpResult({node: xData.responseXML});
      alert("Copy Complete");
      alert("lastID is: " + lastdoc);
      var newurl = "Yoursite.com/sitepath/_layouts/FormServer.aspx?XmlLocation=/sitecoll/library/REQ" + lastdoc + ".xml&DefaultItemOpen=1&OpenIn=Browser";
      newwindow = window.open(newurl, 'Edit', 'width=900, height=600, status=no, directories=no, location=no, menubar=no, toolbar=no, scrollbars=1');
      if(window.focus) {newwindow.focus()}
      //$("#overlay2").remove();
      $("#fancy_overlay").fadeOut();
    }
  });
}

GetLastDocID

Here’s a final item, where I’ve modified the GetLastItemID function to pull a particular document ID. The only real modification is that we are matching on the “DocOwner” field, not the “Created” or “Modified By” field. If I could go back to this one, I would have modified it to accept the field name as a parameter.

$.fn.SPServices.SPGetLastDocId = function(options) {

    var opt = $.extend({}, {
      webURL: "",        // URL of the target Web.  If not specified, the current Web is used.
      listName: "",
      userAccount: "",
      CAMLQuery: ""
    }, options);

    var userId;
    var lastId = "none.xml";
    $().SPServices({
      operation: "GetUserInfo",
      async: false,
      userLoginName: (opt.userAccount != "") ? opt.userAccount : $().SPServices.SPGetCurrentUser(),
      completefunc: function (xData, Status) {
        $(xData.responseXML).find("User").each(function() {
          userId = $(this).attr("ID");
        });
      }
    });

    // Get the list items for the user, sorted by Created, descending. If the CAMLQuery option has been specified, And it with
    // the existing Where clause
    var camlQuery = "<Query><Where>";
    if(opt.CAMLQuery.length > 0) camlQuery += "<And>";
    camlQuery += "<Eq><FieldRef Name='DocOwner' LookupId='TRUE'/><Value Type='Integer'>" + userId + "</Value></Eq>";
    if(opt.CAMLQuery.length > 0) camlQuery += opt.CAMLQuery + "</And>";
    camlQuery += "</Where><OrderBy><FieldRef Name='Last_x0020_Modified' Ascending='FALSE'/></OrderBy></Query>";

    $().SPServices({
      operation: "GetListItems",
      async: false,
      webURL: opt.webURL,
      listName: opt.listName,
      CAMLQuery: camlQuery,
      CAMLViewFields: "<ViewFields><FieldRef Name='ID'/></ViewFields>",
      CAMLRowLimit: 1,
      CAMLQueryOptions: "<QueryOptions><ViewAttributes Scope='Recursive' /></QueryOptions>",
      completefunc: function(xData, Status) {
        $(xData.responseXML).find("[nodeName=z:row]").each(function() {
          lastId = $(this).attr("ows_ID");
        });
      }
    });
    return lastId;
  };

</script>

So that’s it – a focus on maintainability and running workflows using SPServices. But here’s the best thing: this entire application is not only easy to maintain, it is now entirely portable to be deployed to other sites in our environment. We are already working on a modified version to automate another process.

If you have questions post them here, or Rob Doyle can be reached through his LinkedIn profile.

53 Comments

  1. Hi Marc,

    Excellent post on triggerring the workflows with abutton click.

    I am trying to use this approach to send an email on button click.
    But I was getting an error: Exception of type ‘Microsoft.SharePoint.SoapServer.SoapServerException’ was thrown.
    Are there any specific reasons why this error comes up.

    I would also like to pass some parameters to the workflow, like the to address for the Send mail activity I want to pass it to this method and use that in the workflow.
    Could you please help me with the syntax on how can I pass the workflow parameters and use them in the SPD workflow.

    Many Thanks.

    Reply
  2. Have been looking for quite some time for a method to kick off a workflow while viewing a list item that will refresh the data that is being displayed, is there a way to do this?

    Reply
  3. I am stuck at the “@FileRef” as its not returning anything, so it seems my workflow in not getting executed. Can you through som elight on this.. what exactly ” this doing.. as mentioned its for file location, however i m not getting any value for this.

    Reply
    • Raman:

      @FileRef is the path to the document in a Document Library or to the item in a list. If you’re not using a Document Library, then you probably will need to modify the script.

      M.

      Reply
  4. Hi Marc,

    Excellent post and really helpful.

    I am trying to implement on the same line as stated by you in above post but facing one problem. I am not able to get the value of @FileRef parameter. @Fileref is not returning any value because of which the required item is not getting deleted.
    Can you please shed some light as to what went wrong or do i need to do anything else to get that value?

    Thanks
    Shilpa

    Reply
    • Shilpa:

      It’s hard to say without understanding the details of your environment and your code. As with most of my posts, this is an example which worked in a particular situation, but will undoubtedly require adaptation to work for you.

      M.

      Reply
      • Hi Marc,

        Thanks for the quick reply. For some reason i cant post the code here.
        Just to give you brief about my code,
        I am using this code on one list’s display form wherein i have created a input button and on click on that button i am calling the same function which you used above for calling single workflow. I have provided the template id for the workflow directly in jquery function.
        When i tried to find the value of @Fileref using >xsl:select>, i found that it is coming as blank.

        If i am not wrong , @Fileref is the existing variable in the aspx page correct?

        Can you be able to say something based on above information?

        Thanks
        Shilpa

        Reply
      • Hi Marc,

        Thanks for the quick reply. For some reason i cant post the code here.

        But just to give you some idea about code,
        I am using the above code in one of the display form of the list. I am using this function on click of one input button wherein i am calling single workflow. The template id for the workflow i am providing in jquery function itself. For list item url i am giving http://servername/sitename and for @Fileref. I am also providing requiired item id.

        As per my understanding @fileref is existing variable in the form.

        Would you be able to help based on above information?

        Thanks
        Shilpa

        Reply
        • Shilpa:

          @FileRef is available in a Data View Web Part, and is a column value from the list. It’s not a JavaScript variable. If you want to use this method, you have to use a DVWP.

          M.

          Reply
  5. Hi Marc,

    Thanks for the reply.

    I am using Data Form Web part in Sharepoint Designer 2010. You are correct that its column value from the list. I am able to see @FileRef(URL Path) in datafields of the data form web part.

    Its value is coming blank.

    Thanks
    Shilpa

    Reply
    • Shilpa:

      I’m not sure what’s going on. If you can see the value for @FileRef in the Data Source Details pane, but it’s not showing up in the DVWP, then you probably have a spelling error?

      M.

      Reply
  6. Hi I am using SP 2007 and trying to work on a custom list form..I want to delete the current list item using a form action button on this form and send a notification to an email group saying that the item has been deleted.How can i achieve this using javascript?

    Reply
    • madhurima:

      Take a look at my SPServices library. You could also do this by convering the list form to a DVWP and adding a form action button for the delete.

      The emailing is the tricky part. You’ll have to come up with a way to either trigger a workflow (SPServices can help with this) before deleting the item and wait for it to complete or devise a way to send an email via an SMTP server with script.

      M.

      Reply
  7. Hi Marc

    thx for ur response…I have created a manual workflow to delete the list item and send an email..I can assign this to the form action button..but the challenge is that it leads me to the actual URL of the aspx where the user has the option to “start” and “cancel” the workflow..i do not want the user to be directed to another aspx for this but take the same action if he had clicked on “start”.

    Also,we do not use visual studio here…so i can only manipulate it thru SP designer..any suggestions,,

    Reply
  8. I am unable to pass the correct parameters..I am also tracking the error thru :
    completefunc: function(xData, Status) {
    alert(xData.responseText);

    getting a soap error:
    Value doesnt fall within the expected range…

    Reply

Have a thought or opinion?