in

SharePoint Blogs

The Best Place for SharePoint-related Blogs

Andrew Noon's Blog

SharePoint, MOSS 2007, Information Architecture, Business Intelligence and other rambling

Collecting data from users in SharePoint workflow's created in Visual Studio 2008

In my previous blog entry I explained how to create a workflow for SharePoint using Visual Studio 2008. It was a simple workflow designed to get you started, but what happens when that’s not enough? Well the next, most natural step is to collect information from users and to utilise that data in your workflow. I've seen a lot of examples that tell you how to collect data using one mechanism or another but none that bring them together or that describe each step in great detail (idiots guide).  Since I'm the sort of person that needs to know EXACTLY how each step works and why here's my guide to collecting data from users in a workflow

If you haven't 'done' workflow before then read my first blog before reading any further:

When can I collect data?

You can collect data at any time during your workflow using tasks, SharePoint provides a custom list to help with this, called a Task List. At any point in your workflow you can opt to collect data from the user by assigning a task to them. Additionally there are specific points in the workflow's lifecycle where you can interrupt workflow execution to collect data from the user, these can be surmised as:

Event name Description Further reading
Association Occurs when a site administrator first adds the workflow to a library (makes an association between the library and workflow) Click here...
Instantiation Occurs when the workflow starts. For example, if your workflow is set to start automatically when a new item is created in a library then it is when the item is created that this will occur. Click here...
Modification Allows a user to modify your workflow during its processing. You can associate a form to a particular section of processing; this form is made available to the user via the workflow status page. Typically this might allow a user to reassign a task to someone else. Click here...
Task This is defined in your workflow. You insert a task creation step in your workflow and wait for the task to be completed. Click here...

Table 1 – Event types

Each of the event shown in Table 1 can be associated with a form so that when the event fires the specified form is loaded and displayed to the user. There is both an easy way and a hard(harder) way of achieving this. Here I will introduce both methods and show the reader how it can be achieved.

In my workflow example I want to move a document from its current location to the document centre library in my top level site. In addition when the workflow is first associated with a library I want to ask the user who needs to approve the document before it is moved and whether a marker (gravestone) should be created in the current site to indicate that the document used to exist in this location but has been moved to the document centre.

Using InfoPath Forms to collect data

If you have Microsoft Office SharePoint Server (MOSS) Enterprise Edition you can use InfoPath forms to collect and process data, including receiving data from and passing data back to the workflow. However if you are not running MOSS Enterprise Edition or are just using Windows SharePoint Services (WSS) then you’ll want to skip this section and go straight to the ASPX forms explanation.

Create the form

InfoPath allows you to create forms in a graphical environment and then save the form to SharePoint where it can be opened up either as a web page or through the InfoPath client application. Let’s go ahead and create a form to show our user when we first associate the workflow with a library. Open InfoPath and choose to ‘design a form template’, then select the blank template option.

clip_image004

Figure 1 - Creating the InfoPath form

Now we have our basic form, let’s apply some simple styling and add some fields. You’ll notice from my screenshot that I’m using a contact selector control that is not a standard part of the InfoPath controls library. You can either leave it out to keep things simple or learn how to do this here http://msdn2.microsoft.com/en-us/library/ms558892.aspx.

clip_image006

Figure 2 - The association form

The finish button will need to submit the form to the host environment and then close the form. This is a simple task, from the button properties add a rule and then add actions to submit the data (when prompted add a submit task to send data ‘to the hosting environment, such as...’). Then add another action to close the form.

Publish the form

Don’t forget to make the form browser compatible - in the compatibility section of form options (from the tools menu) check the check the box to allow browser compatibility forms. Then in the Security and Trust section of Form Options set the forms security level to domain, otherwise the form will not run from within SharePoint. It’s also good practise to digitally sign the form but not essential for this process. We can now go ahead and publish the form, use the publish option in the file menu to publish to a network location. To keep things simple publish the form to the same directory as your workflow project files, doing this will mean that the form is installed automatically without having to edit the features.xml file. On the next screen, deleted the suggested entry in the alternate path textbox, this will give the warning below but click Ok and continue to publish the form.

clip_image008

Figure 3 - Alternate access working (ignore)

Associate the form with the workflow

Now that the form is published we can add it to our workflow. To do this open up the workflow you want it associating with and open the workflow.xml. SharePoint has a page that is used to host InfoPath forms for each of the workflow events we discussed earlier, except the custom task (we will discuss the reason for that later).

Event name XML tag

SharePoint host page

Association AssociationUrl

CstWrkflIP.aspx

Instantiation InstantiationUrl IniWrkflIP.aspx
Modification ModificationUrl ModWrkflIP

Table 2 - Form host pages

We also need to tell the host page which form we want to be displayed, so we’ll need the unique identifier that was applied to our form when we published it. If you open the form back up in design mode and look at the file properties you’ll see the form ID. Copy it to the clip board and sue it to make your workflow.xml looks similar to the following:

clip_image010

Figure 4 - workflow.xml

Integrate the form data

So, if we deployed and ran out workflow we would now find that the form is displayed! BUT before we do that we really want to be able to respond to the data that’s been supplied from within our workflow. There’s a bit of magic here firstly in the host page (and InfoPath form) that will automatically handle sending the data through to our workflow. The other piece of magic needs some work, when we get hold of the data it is nothing more than XML but I find it much easier to work with an object that parsing through XML. We can automatically create a class to squirt the XML into using our forms data source. Back in InfoPath, in design view, select file and save source files. Save them to a new directory in the directory that holds your form. From the files (or one of them) that are produced we can use a nifty .NET utility called XSD to automatically create a class that represents all the data in our form. You’ll need the SDK for .NET to get hold of XSD and can download it from here http://www.microsoft.com/downloads/details.aspx?familyid=C2B1E300-F358-4523-B479-F53D234CDCCF&displaylang=en. Running the following command will write a new class to the same directory as your XSN file:

xsd myschema.xsd /classes
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 5 - Creating a class from XSN

Now add this class to your workflow project and add code similar to that below to extract the initiation data from into your workflow:

XmlSerializer serialiser = new XmlSerializer(typeof(associationFields));
XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(workflowProperties.InitiationData));
XmlSerializer serialiser = new XmlSerializer(typeof(myFields));
associationFields initData = (associationFields)serialiser.Deserialize(reader);
_approver = initData.contact[0];
_addGraveStone = (bool)initData.gravestone;
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 5b - Creating a class from XSN

Deploy the workflow

So, that’s it – your workflow works and you’re ready to deploy. This is the easy bit! In essence the deployment of your workflow has already happened, if you’re using Visual Studio 2008 the workflow will have been automatically deployed to the site collection you specified as the development site when you first created the workflow project. However should you need to deploy the workflow to a different server or site collection then these are the steps that you’ll need.

The following steps will deploy your workflow to a site collection.

1. Copy the DLL your workflow creates from the build directory (\bin\Debug) to the Global Assembly Cache (GAC)

2. Create a directory in the features directory [C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\FEATURES] and drop both the feature.xml and workflow.xml files into the directory.

3. Install the feature on your farm, using the following command line statements

stsadm -o installfeature -name DocumentMoveAndShortcut

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 6 - Install the feature

4. Activate the feature to a site collection

stsadm -o activatefeature –name DocumentMoveAndShortcut -url http://moss2007win2008
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 7 - Activate the feature

NOTE: You can usually find the STSADM command in the directory C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\BIN

Test

When you associate your workflow with a document library you should now see your association form, looking something like that below and your data should be sent into your workflow!

clip_image012

Figure 8 - InfoPath association form

Using ASPX Forms to collect data

For this example we’ll extend our workflow to assign a task to a user and collect data from that user when they edit the task. In my example I’m going to use that task as a form of crude approval. I’m going to ask a user (the user I captured in the association form) to approve the item. Once the item is approved I will move the document and if the association form requested a gravestone then I’ll add an item to a list on the site to indicate that the document has been moved.

Create the form

Open your work flow in Visual Studio 2008 and add a new web site project to your solution, you project should be created in the _layouts directory as indicated in the following screenshot.

clip_image014

Figure 9 - Creating the ASPX form

It’s probably a good idea to delete the default.aspx file that is created since we don’t need it. Create a new Web Form (Website -> Add New Item) I’ll name mine ‘DocumentApprovalTask.aspx. Since our page needs to be a ‘SharePoint page’ you can delete everything in the page with the exception of the first line, the page declaration. We can now put the bare bones to our page, I won’t go into excessive detail about this but at a high level we need to tell the page to use SharePoints’ application master page, add some reference to SharePoint and put content placeholders in.

You will not be able to design this page in design view as the masterpage directive will only work when the page is running within the context of the SharePoint web application. We will rely on your HTML / ASP skills to put this page together but if you wanted to you could create a new page use that to design the page and then paste the HTML code into the task page. Your HTML code should look like:

<%@ Page Language="C#" AutoEventWireup="true" MasterPageFile="~/_layouts/application.master" CodeFile="DocumentApprovalTask.aspx.cs" Inherits="DocumentApprovalTask" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 
<asp:Content ID="PageTitle" runat="server" contentplaceholderid="PlaceHolderPageTitle" > 
Approval Task
</asp:Content>
<asp:Content ID="PageTitleInTitleArea" runat="server" contentplaceholderid="PlaceHolderPageTitleInTitleArea" > 
Document Approval Task
</asp:Content>
<asp:Content runat="server" contentplaceholderid="PlaceHolderMain" > 
<div>
<h1>Task Notification</h1><br /><br />
<table>
<tr>
<td colspan="2">
<b>You have been assigned a task to approve the following document. Approving the document will result in the move of the document from its current location to the document centre.</b><br /><br />
</td>
<td>Document Name</td>
<td>
<asp:Label ID="lblDocName" runat="server"></asp:Label>
</td>
</tr>
<tr>
<td>Document URL</td>
<td>
<asp:Label ID="lblDocURL" runat="server"></asp:Label>
</td>
</tr>
<tr>
<td>Do you approve this action?</td>
<td>
<asp:CheckBox ID="chkApproved" runat="server" Checked="true" />
</td>
</tr>
<tr>
<td colspan="2" align="right">
<br /><br />
<asp:Button ID="btnSubmit" OnClick="btnSubmit_Click" runat="server" Text="Complete" />
</td>
</tr>
</table>
<SharePoint:FormDigest ID="taskPost" runat="server"></SharePoint:FormDigest>
</div>
</asp:Content>
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 10 - The bare ASPX page

You’ll notice from this that I have controls that will accept data from our workflow (document name and URL) and a control to accept data from the user (approval). Both of these are easy enough, if you use the following code to insert into your code behind that’s the ASPX form complete.

using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.Workflow;
using Microsoft.SharePoint.Utilities;
public partial class DocumentApprovalTask : System.Web.UI.Page
{
    SPWeb _web;
    SPList _taskList;
    SPListItem _taskItem;
    SPWorkflow _workflow;
    Guid _workflowID;
    protected void Page_Load(object sender, EventArgs e)
    {
        //get task data
        this._web = SPControl.GetContextWeb(Context);
        this._taskList = this._web.Lists[new Guid(Request.Params["List"])];
        this._taskItem = 
        this._taskList.GetItemById(System.Convert.ToInt16(Request.Params["ID"]));
        //get workflow data
        this._workflowID = new 
        Guid(Convert.ToString(this._taskItem["WorkflowInstanceID"]));
        this._workflow = new SPWorkflow(this._taskItem, this._workflowID);
        lblTitle.Text = this._taskItem["Title"].ToString();
        lblDocName.Text = this._taskItem["Document Name"].ToString();
        lblDocURL.Text = this._taskItem["Document URL"].ToString();
        lblAssignee.Text = this._taskItem["Assigned To"].ToString();
    }
    protected void btnSubmit_Click(object sender, EventArgs e)
    {
        Hashtable hash = new Hashtable();
        hash["MoveApproved"] = chkApproved.Checked;
        hash["TaskStatus"] = "Complete";
        hash["PercentComplete"] = "1";
        SPWorkflowTask.AlterTask(this._taskItem, hash, true);
        SPUtility.Redirect(this._TaskListAttachedTo.DefaultViewUrl, 
        SPRedirectFlags.UseSource, HttpContext.Current);
    }
}
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 11 - Form code behind file

Create a content type

All tasks in SharePoint have to be defined as a content type. Again, I won’t go into the detail of what a content type is and how it works but if you want to do some research you can start here – http://msdn2.microsoft.com/en-us/library/ms472236.aspx. A content type has at least 3 files, a features file that tells SharePoint what files to expect in the content type feature, a content type file that describes, well – the content type. Finally, a columns or fields file that describes each of the columns in our content type. Let’s work backwards and start with the columns file. For each filed you want in your content type (either for displaying to the user or collecting from the user you will need to add a <field> entry and each field needs a unique ID (use the Create GUID option in Visual Studio’s Tools menu). There are some fields that will be automatically available because we will later tell SharePoint that our content type inherits the default behaviour of a task content type (these are fields such as PercentComplete, Title, Approved, Assigned To...). Table 12 shows my columns file, I’ve called it MoveApproval Columns.xml. For more information on the fields attribute follow this link http://msdn2.microsoft.com/en-us/library/ms437580.aspx

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Field ID="{53A10CDA-DA2C-4077-B2E8-174B3DD1E5E5}"
         Name="MoveApproved"
         DisplayName="Approve the move"
         Description="Allow the document to be moved to the document centre"
         Group="Move Approval"
         Type="Text"
         Required="TRUE"
         >
  </Field>
  <Field ID="{C9E51AA2-EB60-4cb7-A83D-B72306B2B72C}"
         Name="Document Name"
         DisplayName="Document Name"
         Description="The name of the document associated with this task"
         Group="Move Approval"
         Type="Text"
         Required="FALSE"
         >
  </Field>
  <Field ID="{2961F19C-50B1-423d-ABD9-807B70BE70F1}"
         Name="Document URL"
         DisplayName="Document URL"
         Description="The URL to the document associated with this task"
         Group="Move Approval"
         Type="Text"
         Required="FALSE"
         >
  </Field>
</Elements>
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 12 - Columns XML file

Next let’s move on to our content type file, firstly we need to give the content type a unique ID, rather than a simple GUID this is actually made up of both a GUID and a prefix that indicated the parent content type , followed by ‘00’ and then a GUID. You will see from our example (below) that we are inheriting from ‘0x010801’ which is the ID for the workflow task content type (http://msdn2.microsoft.com/en-us/library/ms452896.aspx). We then simple list the fields that are contained in our content type, you can copy paste and delete unnecessary information (display name, description...) from the columns file. Finally we specify the forms we want to display when a user creates a new, edits an existing or simply displays an item of this content type. For our simple example we will show the same form for all options – the form we created earlier.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <ContentType ID="0x010801003B32C00B8FDB4e9aB99CD9C0D46B9CDA"
      Name="Move Document Approval Content Type"
      Group="Move Document"
      Description="Task to approve the movement of a document into the document centre"
      Version="0"
      Hidden="FALSE">
    <FieldRefs>
      <FieldRef ID="{53A10CDA-DA2C-4077-B2E8-174B3DD1E5E5}" Name=" MoveApproved "/>
      <FieldRef ID="{C9E51AA2-EB60-4cb7-A83D-B72306B2B72C}" Name="Document Name"/>
      <FieldRef ID="{2961F19C-50B1-423d-ABD9-807B70BE70F1}" Name="Document URL"/>
    </FieldRefs>
    <XmlDocuments>
      <XmlDocument NamespaceURI="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms/url">
        <FormUrls xmlns="http://schemas.microsoft.com/sharepoint/v3/contenttype/forms/url">
          <New>_layouts/DocumentMoveApproval/DocumentApprovalTask.aspx</New>
          <Display>_layouts/DocumentMoveApproval/DocumentApprovalTask.aspx</Display>
          <Edit>_layouts/DocumentMoveApproval/DocumentApprovalTask.aspx</Edit>
        </FormUrls>
      </XmlDocument>
    </XmlDocuments>
  </ContentType>
</Elements>
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 13 - Content type XML file

Finally, the feature file, again it’s pretty simple. This file specifies a unique ID (a normal GUID this time) for the feature, the location of the other files that are needed (columns and content type files). Also, the property ‘GloballyAvailable’ that whilst for our little example isn’t really needed specifies that the feature will be available everywhere in the portal.

<?xml version="1.0" encoding="utf-8"?>
<Feature  Id="13EA2AAA-29A1-4ed2-AA6F-0B17DB862BA1"
          Title="File Move and Shortcut Feature"
          Description="Provides a task item content type to request approval before moving a document to the document centre"
          Version="12.0.0.0"
          Scope="Site"
          xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementManifest Location="MoveApproval Columns.xml" />
    <ElementManifest Location="MoveApproval Content Type.xml" />
  </ElementManifests>
  <Properties>
    <Property Key="GloballyAvailable" Value="true" />
  </Properties>
</Feature>
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 14 - Feature XML file

These files now need copying over to a directory within the features directory [C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\FEATURES] and installing. I called my directory ‘MoveApprovalTaskContentType’ so my install script is as follows.

   1:  C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\BIN>stsadm -o installfeature -name MoveApprovalTaskContentType
   2:  C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\BIN>stsadm -o activatefeature -name MoveApprovalTaskContentType -url http://localhost
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 15 - Install the content type

Now is a good time to point out that in actual fact, for our example the fields we specify in the content type are actually not so important. When we pass data back to our workflow from the ASPX page we are actually just creating a hash table object and the truth is that we can put any data we like in there and it will be passed to the workflow regardless of the content type specification. However there are myriad reasons why it’s a good idea to keep the content type and parameter passing between ASPX page and workflow in synch, not least that the user may want to use your content type as the basis (parent) for another task content type. You can now check that the content type has been deployed by looking at the site setting of your site and selecting Site Content Type Gallery, from here you will see your content type and can check the field definitions.

clip_image016

Figure 16 - Deployed content type

The final piece in the puzzle is to marry the content type up with the workflow. This is relatively straightforward. Firstly we need to adapt our workflow to create the task. Since this task uses a custom content type, we’ll need to add a ‘CreateTaskWithContentType’ activity to our workflow. Drag this from the toolbar into the relevant place in the workflow and:

1. Paste the content type GUID you used in the content type XML file into the ContentTypeId property

2. Type a unique name into the correlation token property and set the owneractivityname to the preceeding activity

3. Click the ellipses on the TaskId property and click Bind to new member, select Field and either accept the default name or name it as you wish

4. Click the ellipses next to the TaskProperties property and bind this to a new field

5. Switch to the event view in the properties window (the clip_image018 icon), and double click the MethodInvoking property, use the code that follows this description to create a new GUID for the task.

Figure 17 shows the final state of the properties window for my create task step.

clip_image020

Figure 17 - Create task properties

Now that we’ve created a task we need to wait for the user to update and complete the task before we continue our workflow processing.

1. Add a while loop below the activity you just created

2. Within the while block add an ‘OnTaskChanged’ activity, name it taskChanged.

3. Click on the while loop activity and set the property ‘Condition’ to a code condition, expand the condition property and click the bind icon (clip_image022), bind to a new property member.

4. Click on the taskChanged activity and set the correlation token to the same as you created for the create task.

5. Click the ellipses next to the AfterProperties property and bind to a new field

6. Click the ellipses next to TaskId and bind to the existing field you created in thecreate task activity

7. Switch to the event view in the properties window (the clip_image018[1] icon), and double click the MethodInvoking property, use the code that follows this description to check for completion and process the data.

Figure 18 shows the final state of the properties window for my task changed step.

clip_image024

Figure 18 - Task changed properties

That’s it for the workflow, you can cross check your code against the code I’ve used in figure 19 and you should then be ready to roll.

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Linq;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using Microsoft.SharePoint.WorkflowActions;
using Microsoft.Office.Workflow.Utility;
using System.Collections.Specialized;
using System.Xml.Serialization;
using System.Xml;
 
namespace DocumentMoveAndShortcut
{
    /// <summary>
    /// This class along with associated forms and content type definitions
    /// demonstrates the creation of a simple workflow and passing of data 
    /// from infopath forms and ASPX forms
    /// </summary>
    public sealed partial class MoveAndMark : SequentialWorkflowActivity
    {
        /// <summary>
        /// Holds the description of the history log entry
        /// </summary>
        public string _historyDesc;
        /// <summary>
        /// Holds the outcome (success, failure...) of the history log entry
        /// </summary>
        public string _historyOutcome;
        /// <summary>
        /// The URL to the document centre library i.e. http://localhost/Docs/Documents/
        /// </summary>
        public string _documentCentre = "http://moss2007win2008/Docs/Documents/";
        /// <summary>
        /// The name of the list to hold gravestone entries for moved files
        /// </summary>
        public string _listName = "Promoted Files";
        /// <summary>
        /// A Person object representing the specified approved.  Specified on association
        /// </summary>
        public Person _approver  = null;
        /// <summary>
        /// Flag to depict whether gravestones are left behind for moved files
        /// </summary>
        public bool _addGraveStone = true;
        /// <summary>
        /// Flag to indicate task completion
        /// </summary>
        private bool _taskComplete;
        /// <summary>
        /// Flag to indicate that the file move has been approved
        /// </summary>
        private bool _moveApproved;
        /// <summary>
        /// GUID of the workflow created task
        /// </summary>
        public Guid moveApprovalTask_TaskId1 = default(System.Guid);
        /// <summary>
        /// GUID of this instance of the workflow
        /// </summary>
        public Guid workflowId = default(System.Guid);
        /// <summary>
        /// A handle to the workflow properties
        /// </summary>
        public SPWorkflowActivationProperties workflowProperties = new SPWorkflowActivationProperties();
        /// <summary>
        /// The properties of the workflow created task
        /// </summary>
        public SPWorkflowTaskProperties moveApprovalTask_TaskProperties1 = new Microsoft.SharePoint.Workflow.SPWorkflowTaskProperties();
        /// <summary>
        /// The properties of the workflow created task after the task has been changed
        /// </summary>
        public SPWorkflowTaskProperties taskChanged_AfterProperties1 = new Microsoft.SharePoint.Workflow.SPWorkflowTaskProperties();
        /// <summary>
        /// Default constructor
        /// </summary>
        public MoveAndMark()
        {
            InitializeComponent();
        }
        /// <summary>
        /// Called when the workflow is activated
        /// </summary>
        /// <param name="sender">Event originator</param>
        /// <param name="e">Arguments</param>
        private void WorkflowActivation_Invoked(object sender, ExternalDataEventArgs e)
        {
            workflowId = workflowProperties.WorkflowId;
        }
        /// <summary>
        /// Retrieves the workflow properties needed for processing
        /// </summary>
        private void getInstantiationData()
        {
            try
            {
                if (workflowProperties.InitiationData!=null)
                {
                    XmlSerializer serialiser = new XmlSerializer(typeof(associationFields));
                    XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(workflowProperties.InitiationData));
                    associationFields initData = (associationFields)serialiser.Deserialize(reader);
                    _approver = initData.contact[0];
                    if (initData.gravestoneSpecified)
                        _addGraveStone = (bool)initData.gravestone;
                }
            }
            catch (Exception) { }
        }
        /// <summary>
        /// Returns a boolean in the ConditionalEventArgs argument to specify whether the document has been marked as ready to be moved
        /// </summary>
        /// <param name="sender">The originating object</param>
        /// <param name="e">input / output paramenters</param>
        private void IsDocumentReadyForMove(object sender, ConditionalEventArgs e)
        {
            e.Result = false;
            try
            {
                if (workflowProperties.Item != null &&
                    workflowProperties.Item["Approved for move"] != null)
                {
                    e.Result = (bool)workflowProperties.Item["Approved for move"];
                }
            }
            catch (Exception) { }
        }
        /// <summary>
        /// Called when the document is ready and has been approved for moving.  Document will be moved without leaving a gravestone marker.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MoveDocument_ExecuteCode(object sender, EventArgs e)
        {
            MoveAndLeaveMarker(false);
        }
        /// <summary>
        /// Called when the document is ready and has been approved for moving.  Document will be moved and a gravestone marker left behind.
        /// </summary>
        /// <param name="sender">The originating object</param>
        /// <param name="e">input / output paramenters</param>
        private void MoveDocumentAndMark_ExecuteCode(object sender, EventArgs e)
        {
            MoveAndLeaveMarker(true);
        }
        /// <summary>
        /// This will move the file and if appropriate leave behind a gravestone to mark its original location
        /// </summary>
        /// <param name="mark">Flag to indicate whether a gravestone should be left behind after move</param>
        private void MoveAndLeaveMarker(bool mark)
        {
            try
            {
                string filename = _documentCentre + workflowProperties.Item.File.Name;
                workflowProperties.Item.CopyTo(filename);
                _historyDesc = "Moved document " + workflowProperties.Item.File.Name + " from the library " +
                    workflowProperties.List.Title + " in the site " + workflowProperties.Web.Title + " (" +
                    workflowProperties.Web.Url + ") to the document centre";
                _historyOutcome = "Success";
 
                if (mark)
                {
                    //leave a grave stone to show that the file has been moved
                    SPList list = workflowProperties.Web.Lists[_listName];
                    if (list == null)
                    {
                        //add the list if it doesn't exist
                        workflowProperties.Web.Lists.Add(_listName, "", SPListTemplateType.GenericList);
                        workflowProperties.Web.Update();
                        list = workflowProperties.Web.Lists[_listName];
                        list.Fields.Add("Comments", SPFieldType.Text, false);
                        list.Fields.Add("Link", SPFieldType.URL, false);
                        StringCollection sCol = new StringCollection();
                        sCol.Add("Title");
                        sCol.Add("Comments");
                        sCol.Add("Link");
                        list.Views.Add(_listName + "Default View", sCol, "", 100, true, true);
                        list.Update();
                    }
                    //add and item to the list
                    SPListItem item = list.Items.Add();
                    item["Title"] = workflowProperties.Item.File.Name;
                    item["Comments"] = _historyDesc;
                    item["Link"] = filename;
                    item.Update();
                }
                //delete the old file to complete the move
                workflowProperties.Item.Delete();
            }
            catch (Exception ex)
            {
                if (workflowProperties.Item != null)
                    _historyDesc = "Failed to move document " + workflowProperties.Item.File.Name + ". Error: " + ex.Message;
                else
                    _historyDesc = "Failed to move document. Error: " + ex.Message;
 
                _historyOutcome = "Failure";
            }
        }
        /// <summary>
        /// Gets data from the instantiation form and sets the gravestone flag
        /// </summary>
        /// <param name="sender">The originating object</param>
        /// <param name="e">input / output paramenters</param>
        private void markMove(object sender, ConditionalEventArgs e)
        {
            getInstantiationData();
            e.Result = _addGraveStone;
        }
        /// <summary>
        /// Sets up the task list and creates a task for approving the document move
        /// </summary>
        /// <param name="sender">The originating object</param>
        /// <param name="e">input / output paramenters</param>
        private void moveApprovalTask_MethodInvoking(object sender, EventArgs e)
        {
            this.moveApprovalTask_TaskId1 = Guid.NewGuid();
            if (this.workflowProperties.TaskList.ContentTypesEnabled != true)
                workflowProperties.TaskList.ContentTypesEnabled = true;
            SPContentTypeId ctMoveID = new SPContentTypeId(moveApprovalTask.ContentTypeId);
            SPContentType ctMove = workflowProperties.Site.RootWeb.ContentTypes[ctMoveID];
            bool contentTypeExists = false;
            foreach (SPContentType contentType in workflowProperties.TaskList.ContentTypes)
                if (contentType.Name == ctMove.Name)
                {
                    contentTypeExists = true;
                    break;
                }
            if (contentTypeExists != true)
                workflowProperties.TaskList.ContentTypes.Add(ctMove);
            getInstantiationData();
            moveApprovalTask_TaskProperties1.Title = "Request approval for document move (promotion)";
            moveApprovalTask_TaskProperties1.DueDate = DateTime.Now.AddDays(1);
            if (this._approver != null)
                moveApprovalTask_TaskProperties1.AssignedTo = this._approver.AccountId.ToString();
            else
                moveApprovalTask_TaskProperties1.AssignedTo = workflowProperties.OriginatorUser.LoginName;
            moveApprovalTask_TaskProperties1.PercentComplete = 0F;
            moveApprovalTask_TaskProperties1.ExtendedProperties["Document Name"] = workflowProperties.Item.File.Name;
            moveApprovalTask_TaskProperties1.ExtendedProperties["Document URL"] = workflowProperties.Web.Url + "/" + workflowProperties.Item.File.Url;
            moveApprovalTask_TaskProperties1.ExtendedProperties["Approved"] = true;
        }
        /// <summary>
        /// Called when the task item has been changed, checks for completion and approval status
        /// </summary>
        /// <param name="sender">The originating object</param>
        /// <param name="e">input / output paramenters</param>
        private void taskChanged_Invoked(object sender, ExternalDataEventArgs e)
        {
            if (taskChanged_AfterProperties1.PercentComplete == 1F)
            {
                this._taskComplete = true;
                this._moveApproved = taskChanged_AfterProperties1.ExtendedProperties["MoveApproved"].ToString()=="True";
            }
        }
        /// <summary>
        /// Checks and returns via the ConditionalEventArgs parameter whether the task has been completed
        /// </summary>
        /// <param name="sender">The originating object</param>
        /// <param name="e">input / output paramenters</param>
        private void isTaskComplete(object sender, ConditionalEventArgs e)
        {
            e.Result = !this._taskComplete;
        }
        /// <summary>
        /// Checks and returns via the ConditionalEventArgs parameter whether the move has been approved
        /// </summary>
        /// <param name="sender">The originating object</param>
        /// <param name="e">input / output paramenters</param>
        private void isMoveApproved(object sender, ConditionalEventArgs e)
        {
            e.Result = this._moveApproved;
        }
    }
}
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Figure 19 - Workflow code

For completeness the following figure shows the final state of my workflow.

clip_image002

Figure 20 - Final workflow design

To test the workflow, run your workflow project, this will deploy it into the SharePoint library and open up an explorer window, edit a document and mark it ready for move. This should spark up your workflow and create a task, edit the task and complete it. You should see that your workflow has run through. If there are errors you can take a look at the workflow status screen by clicking the link in the document library and if you need to debug any further you can open the latest log file from the logs directory and search for ‘workflow’. With a small amount of configuration in the SharePoint GUI this is what my home page looks like with the documents and a list of moved (promoted) documents displayed together:

clip_image004[4]

Figure 21 - Final result

PHEW - ALL DONE!

Comments

 

Links (2/12/2008) « Steve Pietrek’s SharePoint Stuff said:

Pingback from  Links (2/12/2008) &laquo; Steve Pietrek&#8217;s SharePoint Stuff

February 12, 2008 7:33 PM
 

Introduction to Web Development; Getting started with ASP.NET at World Flat said:

Pingback from  Introduction to Web Development; Getting started with ASP.NET    at World Flat

March 19, 2008 1:18 AM
 

Brian said:

Hi

Managed to follow this through and deploy etc with no problems but as will all the examples I have found on this I am getting an error when running on this line...

XmlTextReader reader = new XmlTextReader(new System.IO.StringReader((workflowProperties.InitiationData)));

Problem is workflowProperties.InitiationData is null.

Any ideas ?

Cheers

March 20, 2008 6:29 AM
 

Andrew Noon said:

Are you seeing the form on initiation?

If so then it has to be one of the bindings:

2. Type a unique name into the correlation token property and set the owneractivityname to the preceeding activity

3. Click the ellipses on the TaskId property and click Bind to new member, select Field and either accept the default name or name it as you wish

4. Click the ellipses next to the TaskProperties property and bind this to a new field

If these are all OK then I'm afraid I'd have to get a copy from you to find the error.

March 20, 2008 6:43 AM
 

Kurt Opel said:

Is there a way to use a workflow where the Assoc Form is the Form Template for the Form Library AND the same Form is an Administrator Uploaded Form Template?

Thanks!

April 9, 2008 2:58 AM
 

Richard Jacobs said:

Hi,

I am also getting the same problem, workflow.InitiationData is null and my Instantiation form is not being loaded/hit.

I have modified the workflow.xml file to use the URN of my Instantiation form and added:

"InstantiationUrl="_layouts/IniWrkflIP.aspx">"

to my workflow element in the xml file.

Is there any reason why my Instantiation form is not being hit, have also noticed my assocation form is not being hit either when I run my new workflow from VS2008, is this expected?

many thanks

May 8, 2008 4:55 AM
 

Javier said:

Hi, I need to develop a initiation form in infopath 2007 for a workflow asociated to a document library... Is there a way to get information of the document associated to the workflow? For example i need to get the content type of the document that fires the worfklow from the infopath initiation form becuase i will change the view based on the content type... Any ideas?

Thanks

June 25, 2008 10:17 AM
 

Roopesh25 said:

How do you add a method in your woflow methods to assign tasks to multiple approvers. LIke for example you have 5 approvers and each of them gets a task created and each of them goes through an approval process.

June 25, 2008 2:10 PM

Leave a Comment

(required )  
(optional )
(required )  
Add

About Andrew Noon

Technical Consultant Strengths  Microsoft Office SharePoint Server 2007  SharePoint Portal Server 2003  Windows SharePoint Services  Enterprise Content Management  Business Process and Workflow Skills  Microsoft .NET C#  SQL Server  ASP.NET  K2  Information Architecture Certifications  Microsoft Certified Professional (MCP)  FileNet Certified Professional  Sun Java Certified Professional

Need SharePoint Training? Attend a SharePoint Bootcamp!

Posts (c) their respective authors. Everything else (c) 2007 SharePoint Experts