SharePoint surveys don’t fire a workflow. You can create one, but it will never fire.
And the reason is fairly understandable. When should it fire? When is the survey item created? When is it changed? To understand how difficult it is to answer those questions, I need to take a moment to explain how surveys work.
Surveys can take two forms, branched or unbranched. An unbranched survey looks a lot like any other list and functions almost exactly like it (except for workflows).
But, a branched survey is a completely different proposition.
When you have a branched survey, the questions are presented a few at a time and depend on the answers from previous questions. This is great, especially when you have a requirement that looks like this:
Do you like ice cream? If yes, which flavor?
In other words, if the answer is No, I’m not going to ask you which flavor is your favorite.
SharePoint accomplishes this by starting with a NewForm.aspx, just like other lists, but it only shows the first branch of the survey. When the user clicks Next, the answer to the first branch is saved and an ID is assigned to the survey. The next page is displayed using EditForm.aspx and passing in two parameters to the page: the ID of the survey, and the first question of the next branch. This happens each time the user clicks Next, until the last branch, when a Finish button is displayed rather than Next.
At any point, if the user clicks Cancel on an EditForm.aspx page, she is prompted with a question about whether the entire survey should be deleted (recycled, actually) or not. So, built into the creation of the survey item is also the possibility that it will be destroyed before all questions are answered, but after some data has already been saved.
Typically, with other lists, a create
workflow
is fired when NewForm.aspx is
saved, that is, when the item ID is created. The problem with doing that
with a branched survey is that the workflow may need required fields
that come later in the survey and so are not available when NewForm.aspx is saved.
Likewise, with other lists, a change
workflow
is fired when something changes and the EditForm.aspx is saved. Again, with a survey,
EditForm.aspx is used in the
creation of the item, so a save
from it doesn’t
necessarily mean something has changed; it may be the initial creation
of the data for that column.
It’s often necessary or desirable to spawn a new task for a user to complete, which can be handled separately from the creation of the initial item. The challenge comes in when you want to easily link from the creator item to the created item and vice versa. In Understanding SharePoint Journal’s Bonus to Issue 4, I explained a method to use the Collect Date from a user workflow action to create that two-way link between a List item and a Task item using a workflow. In our case, we need the ability to ask certain questions based on the answers to other (branching), which a Task can’t do, so a Survey is a better option.
In both cases, it’s pretty useful to include a lookup column in both items (initiating item and task, or initiating item and survey response) that point to each other. With the former, you can fire a workflow on creation of the task to write its ID to the initiating item. With the latter, you can’t.
I was recently asked to create an intake list for work-related injuries and illnesses, which we are calling the Injury Report list. The simple setup allows employees to report when an injury or illness occurs at work and allows HR to see those reports for its legal documentation.
But, because the company is a healthcare provider, blood-borne pathogen exposure (BPE) is one of the possible types of injury/illness that can occur. And if it does occur, there’s an entirely separate set of additional questions that need to be documented. Many of these are similar to our ice cream question above: if yes, then answer these additional clarifying questions; if no, move on to the next question.
These additional questions were perfectly suited for a survey. But, the answers have to be tied to the original injury intake report.
So, how can we get these two to talk to each other?
Using Marc Anderson’s jQuery Library for SharePoint Web Services on Codeplex, we can get the information we need, when we need it, and write out where it needs to go.
The basic steps are:
If it’s a BPE, email the user a survey response link that includes the Injury Report’s ID in the querystring.
Add jQuery to the survey’s NewForm.aspx to fill the lookup on the survey with the ID of the report from the query string.
Add jQuery to the survey’s EditForm.aspx to update the Injury Report with the survey ID.
Since our Injury Report is a list that can execute a
workflow, it’s easy to send an email message to the user that includes
a link to the BPE survey NewForm.aspx. We also want to pass the ID
of the Injury Report to the BPE survey, and we can do that through the
query string. All we have to do is add "?ReportID=
" and the Current Item’s ID field
to the anchor tag’s HREF. If this isn’t clear, see my separate post
called “Building
a Hyperlink in an Email in SPD”.
We’ll use Javascript and jQuery to capture the ReportID
and update our report lookup column
in the BPE survey with it. Of course, that means you’ll need to add a
lookup column as the first question (or at least in the first branch)
of the survey so that it can link back to the Injury Report on
creation. This will create a drop-down list as the first question.
We’re going to use this column to connect the two, but we’re going to
hide it. So, make it required and no one will be able to submit a
survey without supplying a ReportID
once we add a little jQuery to it.
Point your browser to your survey’s NewForm.aspx (you don’t need the
ReportID
in the query string
yet).
Right-click on the page and choose View Source.
While viewing the source, press Ctrl+F and find the label text for your lookup field. It should look something similar to this:
<TR> <TD valign="top" width="90%" class="ms-formlabel"> Injury Report </TD> </TR> <TR> <TD valign="top" width="90%" class="ms-formbodysurvey"> <!-- FieldName="Injury Report" FieldInternalName="ReportID" FieldType="SPFieldLookup" --> <span dir="none"><select name="ctl00$m$g_a31f7024_8309_422f_9bba_84a452812e43$ ctl00$ctl01$ctl00$ctl00$ctl00$ctl04$ctl00$Lookup" id="ctl00_m_g_a31f7024_8309_422f_9bba_84a452812e43_ctl00_ctl01_ ctl00_ctl00_ctl00_ctl04_ctl00_Lookup" title="Injury Report"> <option selected="selected" value="0">(None)</option> <option value="10">Migraine</option> <option value="9">Needle stick</option> <option value="15">Sprained ankle</option> <option value="14">Threw back out</option> </select><br/></span> </TD> </TR>
Notice that the select control has a title that matches your label (Injury Report). That’s what we’re going to be searching on with jQuery.
Add ?PageView=Shared&ToolPaneView=2
to
the URL in your browser to put it in Edit mode.
Add a hidden CEWP to the bottom of the page.
Paste the following into the webpage dialog:
<script src="/scripts/jquery.min.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function() { $("input[value='Save']").hide(); // hides the Save buttons $("table.ms-formtoolbar tr:first").hide(); // hides the top toolbar var rptItem = $("select[title='Injury Report']"); // sets a variable to the select control //with our ID in it rptItem.hide(); //hides the select control fillDefaultValues("Injury Report","ReportID"); // fills the select control with the data //from the querystring var rptText = $("select[title='Injury Report'] option:selected").text(); // sets //a variable to the text of the selected item rptItem.after(rptText); // prints the text of that variable after the select control //(which is now hidden) }); function fillDefaultValues(fieldName,value) { // Notice that you call fillDefaultValues, passing it: a) the Display Name of the //column, and b) the querystring key JSRequest.EnsureSetup(); var qs = JSRequest.QueryString[value], x = $('select[title="'+fieldName+'"]'); if (typeof(qs)=="undefined") { x.val(0) }else { x.val(qs) }; x.change(function () { x.val(qs); }).change(); } </script>
fillDefaultValues
captures the query string value and puts it in the control
matching the title supplied.
So far, what we’ve done is pass the Injury Report’s ID to the BPE survey response via an email link. Steps 1 and 2 are complete.
Here’s where we really put Marc’s library to work. We’re going to use his web services library to update the item in the Injury Report list by writing the ID of this BPE survey response to the BPE survey lookup field (make sure you’ve created that column) in the Injury Report. We’ll do this on the EditForm.aspx, since the ID of the survey response isn’t available on NewForm.aspx (it hasn’t been created yet).
Follow the steps above for adding a CEWP to the EditForm.aspx and getting the basic jQuery
shell in place. From there, we’ll start putting the pieces in place to
make the final connection between these two items. Include the
snippets for hiding the select control and displaying the text, as
well as hiding the Save button and top toolbar (when we come back to
the record in Edit mode, we’ll want that same functionality to be in
place). fillDefaultValues
is not
needed on the EditForm.aspx,
however, because the connection will have already been made.
Looking through the library, we want to use the UpdateListItems
operations of the Lists web
service.
$().SPServices({ operation: "UpdateListItems", async: false, // This ensures that processing waits for the response listName: "Injury Report", // Use the Display Name of your Source List updates: "<Batch OnError='Continue'>" + "<Method ID='1' Cmd='Update'>" + "<Field Name='SurveyID'>" + SurveyID + "</Field>" + // The first SurveyID is the StaticName of the lookup column in the Injury Report; // The second SurveyID is a variable we'll define in a minute "<Field Name='ID'>" + ReportID + "</Field>" + // ID is the column name of the ID of Injury Report // ReportID is another variable we'll define shortly "</Method>" + "</Batch>", completefunc: function(xData, Status) { // We could put any post-processing or error handling here } }); });
So, we need to declare a couple of variables to make the above work.
SurveyID
Since we’re on EditForm.aspx, the easiest place to get this is from the query string.
The following code snippets will do that for us:
We need to declare this variable (bold line below) before
the SPServices call to UpdateListItems
, so we’ll put it right
after we display the Report Title:
. . . rptItem.after(rptText); var SurveyID = getQuerystring('ID'); // ID is found in the page URL (e.g. ../EditForm.aspx?ID=1) }); . . . //The code snippet for getQuerystring should go last before the <script> tag closes. . . . }); function getQuerystring(key, default_) { if (default_==null) default_=""; key = key.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); var regex = new RegExp("[\\?&]"+key+"=([^&#]*)"); var qs = regex.exec(window.location.href); if(qs == null) return default_; else return qs[1]; } </script>
ReportID
The ReportID
is not
accessible from this page unless we’re on the first branch of
the survey. When the response is recorded, the first time we’re
on EditForm.aspx is on the
second branch, so we need to get the ReportID
some other way.
SPServices to the rescue again, in the form of the
GetListItem
operation. This
helpful service allows us to get the XML for any list item by
using common SQL syntax. First, we’ll declare the variable,
making it null. This gives it a scope outside the jQuery call to
SPServices. For easy maintenance, we’ll just declare it right
after the SurveyID
declaration:
. . . rptItem.after(rptText); var SurveyID = getQuerystring('ID'); var ReportID = null; }); . . .
Now, we’ll call GetListItem
before we call UpdateListItems
, so that ReportID
has data by the time we call
UpdateListItems
. Paste the
following jQuery snippet (with edits) right before the UpdateListItems
section:
$().SPServices({ operation: "GetListItems", async: false, listName: "BPE Survey", //Again, use the Display Name here CAMLQuery: "<Query><Where><Eq>" + "<FieldRef Name='ID' />" + "<Value Type='Integer'>" + SurveyID + "</Value>" + // This is the variable we declared earlier "</Eq></Where></Query>", completefunc: function(xData, Status) { $(xData.responseXML).find("[nodeName= z:row]").each(function() { ReportId = $(this).attr("ows_ReportID"); // Notice ows_ReportID; ReportID is StaticName of the Lookup column // in the Survey list; // Add the "ows_" to the front of that and you will have the XML // attribute you need. }); } });
That’s it. Here are the complete snippets we created (with notes left in to help you make the changes necessary for your implementation).
<script src="/scripts/jquery.min.js" type="text/javascript"></script> <script src="/scripts/jquery.SPServices.min.js" type="text/javascript"></script> // Download Marc's code here http://spservices.codeplex.com; // Save it in a SharePoint directory on your site and modify the previous line accordingly <script type="text/javascript"> $(document).ready(function() { $("input[value='Save']").hide(); $("table.ms-formtoolbar tr:first").hide(); var rptItem = $("select[title='Injury Report']"); // Use title shown in source in step 3 rptItem.hide(); fillDefaultValues("Injury Report","ReportID"); // See function below for syntax //on this one var rptText = $("select[title='Injury Report'] option:selected").text(); // Use title shown in source in step 3 rptItem.after(rptText); }); function fillDefaultValues(fieldName,value) { // Notice that you call fillDefaultValues, passing it: a) the Display Name of the column, // and b) the querystring key (i.e. NewForm.aspx?ReportID=1) JSRequest.EnsureSetup(); var qs = JSRequest.QueryString[value]; var x = $('select[title="'+fieldName+'"]'); if (typeof(qs)=="undefined") {x.val(0)} else {x.val(qs)}; x.change(function () { x.val(qs); }) .change(); } </script>
<script src="/scripts/jquery.min.js" type="text/javascript"></script> <script src="/scripts/jquery.SPServices.min.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function() { $("input[value='Save']").hide(); $("table.ms-formtoolbar tr:first").hide(); var rptItem = $('select[title="Injury Report"]'); // Use title shown in source in step 3 rptItem.hide(); var rptText = $('select[title="Injury Report"] option:selected').text()); // Use title shown in source in step 3 rptItem.after(rptText); var SurveyID = getQuerystring('ID'); var ReportID = null; $().SPServices({ operation: "GetListItems", async: false, listName: "BPE Survey", // Use Display Name of your survey list CAMLQuery: "<Query><Where><Eq>" + "<FieldRef Name='ID' />" + "<Value Type='Integer'>" + SurveyID + "</Value>" + "</Eq></Where></Query>", completefunc: function(xData, Status) { ReportID = $(xData.responseXML).find("[nodeName='z:row']").attr ("ows_ReportID"); // Prepend 'ows_' to the StaticName of the Lookup column in your survey list } }); $().SPServices({ operation: "UpdateListItems", async: false, listName: "Injury Report", // Use Display Name of your source list updates: "<Batch OnError='Continue'>" + "<Method ID='1' Cmd='Update'>" + "<Field Name='SurveyID'>" + SurveyID + "</Field>" + // Use StaticName of the Lookup column in your source list "<Field Name='ID'>" + ReportID + "</Field>" + "</Method>" + "</Batch>", completefunc: function(xData, Status) { } }); }); function getQuerystring(key, default_) { if (default_==null) default_=""; key = key.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); var regex = new RegExp("[\\?&]"+key+"=([^&#]*)"); var qs = regex.exec(window.location.href); if(qs == null) return default_; else return qs[1]; } </script>
I may make it sound really easy, but figuring it all out wasn’t that straightforward. If you’d like to follow the dialog and thought process that actually led to this solution, it’s all archived at http://spservices.codeplex.com/Thread/View.aspx?ThreadId=76735.