Wednesday, September 1, 2010

Send reminder emails from SharePoint Workflow for overdue tasks

The Problem

A requirement of the workflow I have been working on is to send reminder emails when a task is approaching the due date or overdue. The workflow in question has numerous replicator activities with an execution type of Parallel. The typical, at least all the examples I could find, would be to use a ListenerActivity with one leg waiting on an OnTaskChanged event and the other leg contains a delay activity followed by the necessary activities to send out an email. My problem was, when the delay activity completes and that particular leg kicks off, it is the OWSTimer running those events. I don’t have SPContext and I cannot tell which instance of my replicator sequence is running. I don’t have a reference back to my actual workflow task and as a result, I have no way of knowing if a task is completed or overdue etc. I was really struggling trying to figure out how I was going to accomplish this requirement. Whatever I did, I was going to have to repeat it for several different replicators in my workflow.

The Solution

My solution was to basically develop a “multi-threaded” workflow. To do this I used a ConditionedActivityGroup workflow object. Below is the structure of one of the Sequences in my CAG.

I’m going to explain what each of the activities in this leg of the CAG are doing but I have also included the code for each of them below.

The CAG itself has a Declarative Rule Condition set for its until condition that is checking a private bool called _workflowCompleted. This bool is initiated to false and only set to true when the main workflow sequence is complete.

The first sequence that is not showing is my main workflow. This is where all my tasks are created and completed etc. This activity in the CAG has no when condition and is therefore only executed once. SharePoint is responsible for hydrating and dehydrating this workflow as SharePoint events occur.

The second sequence that is shown below is the sequence that checks for overdue tasks and sends emails as necessary. The when condition for this activity is also checking !_workflowCompleted using a Declarative Rule Condition. Adding the when condition causes this leg of the CAG to run continuously until the _workflowCompleted bool is set to true.

The DelayActivity is initialized with a time span defined in a settings file but could be hard coded. We are going to check for overdue tasks daily.

The GetWorkflowTasks activity gets current tasks for the workflow and assigns the collection of tasks to a local variable.

The while loop iterates the tasks collection.

The IfElse block checks to see if the task is not completed and has a (due date – some adjustment) later than today’s date.

If the task is overdue and not completed we send an email to the assignee to remind them they have a task due. The correlation toke for the SendEmail activity was set to the workflow correlation token.

The increment index simply increments a local variable we use to pull the correct value from the task collection. When the local index variable exceeds the task collection count we exit the loop.

Finally, the clearWorkflowCol CodeActivity sets the local variable that contains the workflow tasks collection to null because SPWorkflowTaskCollection is not a serializable object.

Code for all of the activities above

private SPWorkflowTaskCollection workflowTasks = null;
private int _currentIndex = 0;

private void EmailDelay_InitializeTimeoutDuration(object sender, EventArgs e)
{
    EmailDelay.TimeoutDuration = Settings.Default.EmailThreadDelay;
}

private void getWorkflowTasks_ExecuteCode(object sender, EventArgs e)
{
    workflowTasks = workflowProperties.Workflow.Tasks;
    _currentIndex = 0;

}

private void iterateEmails_Cond(object sender, ConditionalEventArgs e)
{
    if (_currentIndex < workflowTasks.Count)
        e.Result = true;
}

private void sendReminderEmail_MethodInvoking(object sender, EventArgs e)
{
    ((SendEmail)sender).Body = GetEmail(
                                workflowTasks[_currentIndex][SPBuiltInFieldId.Title].ToString(),
                                workflowTasks[_currentIndex]["Due Date"].ToString());
    ((SendEmail)sender).To = workflowProperties.Web.AllUsers.GetByID(Convert.ToInt32(workflowTasks[_currentIndex]["Assigned To"].ToString().Split(";".ToCharArray())[0])).Email;
    ((SendEmail)sender).Subject = string.Format("Reminder - {0}", workflowTasks[_currentIndex][SPBuiltInFieldId.Title].ToString());
  
}

private void ifEmailOverdue_Cond(object sender, ConditionalEventArgs e)
{
    if (workflowTasks[_currentIndex]["Due Date"] != null
        && DateTime.Today >= Convert.ToDateTime(workflowTasks[_currentIndex]["Due Date"].ToString()) - Settings.Default.ReminderEmailStart
        && workflowTasks[_currentIndex][SPBuiltInFieldId.TaskStatus].ToString().ToLower() != "completed")
        e.Result = true;
}

private void incrementIndex_ExecuteCode(object sender, EventArgs e)
{
    _currentIndex++;
}

private void clearWorkflowColl_ExecuteCode(object sender, EventArgs e)
{
    workflowTasks = null;
}

Update InfoPath field value from SharePoint Workflow

The Problem

While developing a SharePoint workflow for an InfoPath form library I ran in to a couple of issues with writing values back to the submitted InfoPath form.  The form data that is stored in the form library is pure xml so it seemed reasonable to me that it was simply a matter of finding the node and updating the value.  Turns out, if you don’t take a couple of very specific steps while doing this you will complete hose up the InfoPath form schema and never be able to open the InfoPath form in Web enabled mode again.  The odd thing is you if you do mess up the schema you can still open it with InfoPath filler but we wanted to keep everything web based for this particular effort. 

The Solution

I wrote a few methods to update the InfoPath form correctly.  The primary method that we call from our code is UpdateInfoPathField. 

UpdateInfoPathField Method

public void UpdateInfoPathField(string nodeXPath, string nodeValue)
{

    SPFile file = workflowProperties.Item.File;
    MemoryStream inputStream = new MemoryStream(file.OpenBinary());
    XmlDocument ipDoc = new XmlDocument();
    ipDoc.Load(inputStream);

    // This was the first trick.  When you are reading the InfoPath form into an XMLDocument, it is very important to specify PreserveWhiteSpace = true
    ipDoc.PreserveWhitespace = true;

    inputStream.Close();
    inputStream.Dispose();
    XPathNavigator ipNav = ipDoc.CreateNavigator();
    
    // The BuildNamespaceManager dynamically builds a namespacemanager to use in XPathNavigator select statements.  
    XmlNamespaceManager nsManager = BuildNamespaceManager(ipNav);

    XPathNavigator nodeNav = ipNav.SelectSingleNode(nodeXPath, nsManager);
    
    // This is the second trick.  In my case the field I was populating is initially null when the form is created.  You have to remove the nil attribute or you will receive terrible errors.
    DeleteNil(nodeNav);

    nodeNav.SetValue(nodeValue);

    MemoryStream outputStream = new MemoryStream();
    ipDoc.Save(outputStream);
    file.SaveBinary(outputStream.ToArray());
    outputStream.Close();
    outputStream.Dispose();
}

BuildnamespaceManager Method

private XmlNamespaceManager BuildNamespaceManager(XPathNavigator ipFormNav)
{
    XmlNamespaceManager nsManager = new XmlNamespaceManager(new NameTable());
    ipFormNav.MoveToFollowing(XPathNodeType.Element);
    foreach (KeyValuePair<string, string> ns in ipFormNav.GetNamespacesInScope(XmlNamespaceScope.All))
    {
        if (ns.Key == string.Empty)
            nsManager.AddNamespace("def", ns.Value);
        else
            nsManager.AddNamespace(ns.Key, ns.Value);
    }
    return nsManager;
}

DeleteNil Method

public void DeleteNil(XPathNavigator node)
{
    if (node.MoveToAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance"))
        node.DeleteSelf();
}

SharePoint Workflow – Parallel Replicator

The Problem

I recently was tasked with creating a SharePoint workflow for an internal document approval process. One of the major aspects of this workflow was that any number of tasks could be created depending on information filled out in an InfoPath form. In other words, the InfoPath form contains a repeating table where the user can enter N number of rows and I have to be able to create a task for each item in the submitted form. The logical choice for accomplishing this task was to use a Workflow Replicator activity. I fleshed out a workflow structure as follows:

clip_image001

When the replicator was initialized I would populate the InitialChildData property with my list of items that I needed to create and then in the OnChildInitialized method of the replicator I would set the properties on the CreateTask activity to create the appropriate task. I should also note that my CreateTask, TaskChanged and CompleteTask activity all have the same CorrelationToken with the correlation toke owner set to the sequence. This ensures that the replicator can create unique instances of the token. Everything was working as expected when the replicator ExecutionType was set to Sequence. When I changed the ExecutionType to parallel, the workflow would create N number of tasks but they would all be identical. It never failed that every task created would be the task that should be created for the last index of my InitialChildData collection. I am fairly new to SharePoint workflow development so I don’t know if my solution is the best practice or not but this is how I solved it.

The Solution

My solution was to write a custom SequenceActivity. This is nothing more than a class file that inherits SequenceActivity. My custom sequence activity did nothing more than register a DependencyProperty that I could set in the OnChildInitilized method of the replicator. My CreateTask MethodInvoking method the reads the information from the sequence property to populate it’s fields.

Custom SequenceActivity Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Workflow.Activities;
using System.Workflow.ComponentModel;
using System.ComponentModel;

namespace SemCrude_MOC_Workflow
{
    public partial class ReplicatorSequence : SequenceActivity
    {
        public ReplicatorSequence()
        {
            InitializeProperties();
        }

        public static DependencyProperty ChildDataProperty = System.Workflow.ComponentModel.DependencyProperty.Register("ChildData", typeof(Object), typeof(ReplicatorSequence));

        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public Object ChildData
        {
            get
            {
                return ((Object)(base.GetValue(ReplicatorSequence.ChildDataProperty)));
            }
            set
            {
                base.SetValue(ReplicatorSequence.ChildDataProperty, value);
            }
        }

    }

}

When you create a custom activity using the above code, you will see a new activity in your toolbar that you can drag into your workflow just like any of the built in activities.  When you click on your activity and look in the properties tab of Visual Studio, you will see your custom property.  I did not assign a value for this property using the properties window but instead would set it programmatically as follows.

Replicator OnChildInitialized Method

private void PreModMgrApprovalReplicator_ChildInitialized(object sender, ReplicatorChildEventArgs e)
{
    ((ReplicatorSequence)e.Activity).ChildData = e.InstanceData;
}

In parallel mode, the replicator current index is always the last item in the InitialChildData Property. As a result you have to use the InstanceData from the ReplicatorChildEventArgs to get the current item being processed. This method sets the ChildData property of the Sequence, which is the activity of the replicator, to the current data. This method is executed for each item in the InitialChildData collection simultaneously, creating a new instance of the sequence for each item.

CreateTask MethodInvoking Method

private void CreatePreModMgrApprovalTask_MethodInvoking(object sender, EventArgs e)
{
    TaskItem item = (TaskItem)((ReplicatorSequence)((Activity)sender).Parent).ChildData;
    CreatePreModMgrApprovalTask.TaskId = Guid.NewGuid();
    CreatePreModMgrApprovalTask.TaskProperties.Title = item.TaskTitle;
    CreatePreModMgrApprovalTask.TaskProperties.AssignedTo = item.TaskUser;
    CreatePreModMgrApprovalTask.TaskProperties.SendEmailNotification = true;
}

As you can see, I have a custom class called TaskItem that I am using to pass the relevant information to the sequence to create a task. My CreateTask MethodInvoking method retrieves that value from my CustomSequence ChildData property and assigns the appropriate fields on my CreateTask activity.

Using this method, the appropriate number of unique tasks were created and when they were all completed the replicator activity is completed and the workflow moves on.

I hope this post saves someone the frustration I was experience attempting to get the parallel replicator working.