in

SharePoint Blogs

The Best Place for SharePoint-related Blogs

Andrew Noon's Blog

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

February 2008 - Posts

  • 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!

  • SharePoint 2007 workflow with Visual Studio 2008

    Before getting into this I thought developing workflow for SharePoint 2007 was a complex and daunting task to undertake. Whilst you still won’t find me saying it’s easy I will say that I’ve been pleasantly surprised at how intuitive the process has been. The following pages demonstrate the steps that are required to get a simple workflow going from setup to development and deployment.

    Installation prerequisites

    Visual Studio 2008 greatly simplified the processes of creating, deploying and even debugging workflows but if you need to it is possible to do all of this with Visual Studio 2005 and some additional work. The remainder of this document shows the steps required to create a simple workflow using the 2008 version of the product, so bear in mind that not everything will work as easily if you choose to use an older version such as 2005.

    Visual Studio 2008

    Software

    Link for more information

    Visual Studio 2008

    MSDN Visual Studio Site

    Windows SharePoint Services (WSS)

    WSS 3.0 on Microsoft.com

    Microsoft Office SharePoint Server (MOSS)

    MOSS 2007 on Microsoft.com

    WSS Software Development Kit (optional)

    SDK Download

    MOSS Software Development Kit (optional)

    SDK Download

    Table 1 - Visual Studio 2008 components

    Visual Studio 2005

    Software

    Link for more information

    Visual Studio 2005 Professional or Team Suite

    MSDN Visual Studio Site

    For Team Suite make sure you also download the latest Service Pack (SP1 at time of print)

    Service Pack 1 Download

    .Net 3.0 (FX) Runtime

    .Net 3.0 Download

    Windows SharePoint Services (WSS)

    WSS 3.0 on Microsoft.com

    Visual Studio extensions for workflow

    Download workflow extensions

    Visual Studio extensions for WSS

    Download WSS extensions

    Microsoft Office SharePoint Server (MOSS)

    MOSS 2007 on Microsoft.com

    WSS Software Development Kit (optional)

    SDK Download

    MOSS Software Development Kit (optional)

    SDK Download

    Table 2 - Visual Studio 2005 components

    So what is a WSS workflow?

    Firstly, it is assumed that you have a basic understanding of both workflow principles and development. Specifically though, when we talk about Windows SharePoint Services (WSS) and workflow we are talking about controlling events that can occur when an item in a list or library in a WSS site is changed. Typically this is either to model a business process through a level of automation (Business Process Engineering) and therefore minimising or to manage the lifecycle of content (Information Lifecycle Management).

    Windows Workflow Foundation (WF) is the basic underlying technology that is used to define a workflow however WF alone does not make a workflow happen, it needs a host. WSS forms the host service that runs the workflow in conjunction with the WF run time. Together they will manage tasks, assignment of tasks, persist state over long running transactions (human intervention, scheduled tasks...), scheduling and tracking.

    Sequential Vs State workflows, what is the difference?

    Sequential workflow is nothing like it sounds. It does not mean that there is a single thread of activity with fixed route from start to end. A sequential workflow can have parallel tasks, can branch depending on state, properties, user response...In this case sequential simple means that the workflow has a single start point follows through a number of steps to reach a goal.

    State (machine) workflows basically mean a workflow whereby the core component (a task, a document...) can have a number of states (draft, for review, reviewed, released, approved) and the movement between each of those states is controlled, for example it may be inappropriate to move state between draft and approved without passing the other state first. State workflow has a start and a number of activities that govern state but do not necessarily have to have an end state i.e. Approval can be the best state for content but after approval it’s entirely possible that the content may be reverted to draft for update.

    Basic environment setup

    You should now be able to load Visual Studio and within the workflow section of the new projects library you should have the option to create a number of different workflow types.

    clip_image002

    Figure 1- Setting the project name

    At this point it depends on whether you are working on a MOSS2007 server or not as to what you do next. If you’re on a MOSS2007 server then simply create a new Sequential Workflow project. If not you’ll need to grab the SharePoint DLLs from the server, the easiest way of doing this is to take microsoft.sharepoint.* and microsoft.office * from the servers C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\ISAPI directory and create and copy to the same path on your development machine. You will now have all the references you need to create a workflow project, go ahead and simply create a new Sequential Workflow project.

    You will be asked to specify both the name of the workflow to display to users and also the initial library you want to use during development (for debugging the workflow). Note that in Visual Studio 2005 you do not get this prompt and need to manually set this up.

    clip_image004

    Figure 2 - Naming the workflow

    You will now be asked for the library to attach the development workflow to, the workflow history list (tracks progress) and the task list that you want to use (if assigning tasks).

    clip_image006

    Figure 3 - Set workflow associations

    You will then need to decide when you want your workflow to fire, there are three events that can cause a workflow to start.

    · Manually i.e. the user chooses to start a workflow on the current item

    · On creation, the workflow is started as soon as an item is created (this can be either saving a document to a library or list, uploading a document or simply creating a new item)

    · On change, the workflow will fire every time an item is changed, for document this means that the workflow will start if either the document itself is changed or any of the metadata associated with the document is changed.

    I’m selecting all three because I want a user to be able to choose when to move an item and I want the item to be automatically moved if its metadata indicates that it is ready to move (that can happen on creation or as a result of the metadata changing).

    clip_image008

    Figure 4 - Select workflow triggers

    I created a standard SharePoint Team site to use for developing the workflow and am using the default workflow history list, task list and document library, ‘Shared Documents’ as the workflow source.

    clip_image010

    Figure 5 - Team site

    I have a simple workflow, so what now?

    If you are using Visual Studio 2005 you will notice from the toolbox that although you have created a SharePoint workflow the extensions component does not automatically add the SharePoint items to the toolbox! To add them right click on the toolbox and choose “Add Items” sort the list by namespace and add the Microsoft.sharepoint.workflowactions namespace. If the namespace didn’t appear in the list then you’ll have to navigate to the DLL itself, click browse and usually you’ll find the DLL in C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\ISAPI.

    Now we’re ready to create a workflow, at last!

    The first thing to do when creating a workflow is to simply draw it out on the page in its basic form. Don’t worry about the code yet, we’ll tackle that later. For my example I’m using the following workflow design:

    clip_image012

    Figure 6 - Simple workflow design

    This is a very simplified process that when complete will check the metadata of a document and if it is ready to be moved will move it to the document centre in the top level SharePoint site. In anticipation of this I’ve added metadata to the default document library we specified when we created the workflow project. To do this load up the document library in your browser and from the settings menu choose ‘Create Column’, call the column ‘Approved for move’ and select the ‘Yes/No’ column type.

    Once you’ve created your outline process / workflow map you can start defining in lower level detail how this is going to work. Before we dive into the code it’s important to understand two important aspects of the workflow. Firstly, the workflow need a unique identifier which it will ‘share’ with SharePoint basically it the mechanism the workflow runtime uses to identify the running instance of your workflow. This is called a correlation token and you need one for your workflow and one for each task you create during the workflow, so that when a user completes a task you have a unique identified with which to receive any input from the user into your workflow. If you click on the activation node of the workflow diagram you will see that you have been automatically assigned a correlation token. Secondly, it is important to know where you can find all the information about the source of the workflow (the SharePoint site, library and document or item that started the workflow off in the first place). Workflow Properties is an object that holds all the really important information about your workflow. When you create a workflow it is automatically set to the local variable workflowProperties. The following diagram shows the properties window showing both the correlation token and workflow properties settings.

    clip_image014

    Figure 7 - Workflow properties

    Now that we have the principles out of the way we can move onto getting this fired up and running! Firstly we need to grab the ID that SharePoint has allocated to this instance of the workflow so that we can help SharePoint identify which instance of the workflow is running. SharePoint automatically allocated a workflow identifier in the workflow properties, it is this that we want to extract and store. To do this double click on the workflow activation node (the first node) and you will be taken into the code behind the node. Visual Studio automatically creates the method declaration and two variables for you to use. Your code should look like that shown below.

    clip_image016

    Figure 8 - Getting the workflow ID

    Next we need some logic for our if statement, here you will see that I check the fields (metadata for the column we created earlier and return the value. To save confusion it needs to be said that the terms metadata, fields and columns are used pretty interchangably within the SharePoint world! To add the code for the if statement select the if/else node in the workflow designer and in the properties box set the condition type to ‘code condition’ and type the name of you condition. This will create a method stub for you to enter your code into.

    clip_image018

    Figure 9 - Setting branching logic

    Now you will be placed into your workflow code again and you can add the required logic as such:

    clip_image020

    Figure 10 - Coding branch logic

    So, there nothing too complex there we’re just getting the field back from the SharePoint item (document in this case) and reyrning the value to our workflow.

    Now all we need is the code for the move and history log sections. Both are very simple, starting with the document move code, the following shows the code that’s required. Note there is no move function in the SharePoint object model so a copy then delete is required.

    clip_image022

    Figure 11 - Code document move

    For the history log I will show two methods that can be used, the first and most simple is to add the required output to the properties of the node. Click on the ‘RecordNotReadyToMove’ node (the one without a code block above it) and set the ‘History Description’ property ‘Not ready to be moved’ and the ‘History Outcome’ property to ‘Not ready!’.

    clip_image024

    Figure 12 - Logging progress (simple)

    Next, the more complicated (but flexible) method. Firstly declare two string variables for the history description and history outcome properties. IF you are not familiar with code then you do this after your class definition in such as.

    clip_image026

    Figure 13 - Logging progress (code), step 1

    We now want to alter our document move code to update these variables to indicate the level of success. The following code is a good example of just how useful hat workflowProperties object is!

    clip_image028

    Figure 14 - Logging progress (code), step 2

    Now we have the history variables set we can tell the history node in the workflow diagram to use these variables. To do that select the node and click first on history description, then the ellipses that appear to the right and then choose the _historyDescription variable we created earlier. Do exactly the same for the history outcome

    clip_image030

    Figure 15 - Logging progress (code), step 3

    Ready to test

    If your project will now build then you are bug free and ready to go. If not take a look at the errors that appear and try to fix them, it’s likely to be either a typo or a step you’ve missed as there’s not much that can go wrong at this stage.

    In Visual Studio 2008 you can simply hit F5 or choose ‘Run’ from the debug menu and an the workflow will be deployed and an Internet Explorer window will be loaded with the document library you associated the development of the workflow with displayed. If you now upload a document to the library and make sure the ‘Approved for move’ checkbox is unchecked you will find that the library now has an extra column to tell you how the workflow is doing. If you check the items workflow history you will see that our code has been called and the expected output has been added.

    clip_image032

    Figure 16 - Reviewing workflow progress

    Similarly, if you now edit the properties of the uploaded item and check the ‘Approved for move’ checkbox you will see the item disappear from the library you uploaded it to and it will appear in the Document Centre. Note though that when you do this the document that is copied to the Document Centre will not have any information relating to the workflow attached to it. To see the workflow history for the (now deleted) item you will need to go to the Workflow History list where you will find the expected output. There is no link to the Workflow History list but you can find it at http://[server]/[site]/Lists/Workflow%20History/AllItems.aspx and you should have an item such as:

    clip_image034

    Figure 17 - Hidden progress list

    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

    Figure 18 - Install the feature

    4. Activate the feature to a site collection

    stsadm -o activatefeature –name DocumentMoveAndShortcut -url http://moss2007win2008

    Figure 19 - 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


Need SharePoint Training? Attend a SharePoint Bootcamp!

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