Issues with OnWorkflowItemChanged

I've recently had a few issues with using OnWorkflowItemChanged.

1) I found that adding OnWorkflowItemChanged seems to cause lock ups and delays in my workflow.



I decided to use an alternative to OnWorkflowItemChanged or get rid of it from my workflow and utilize SharePoint event receivers.

Workflow locks because of OnWorkflowItemChanged:
http://blogs.msdn.com/b/yvan_duhamel/archive/2009/11/25/workflow-locks-because-of-onworkflowitemchanged-event-handler.aspx?CommentPosted=true#commentmessage

Alternative to OnWorkflowItemChanged:
https://www.thorprojects.com/blog/archive/2010/02/22/onworkflowitemchanged-and-workflow-event-delivery-problems.aspx

I wasn't able to get the linked alternative to OnWorkflowItemChanged to work in a custom activity, so I wound up creating an event receiver for my list.

Here's the code excerpts:
Step 1: Create an Event Receiver

A basic list event receiver before adding any code looks like below:

using System;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Navigation;
using Microsoft.SharePoint.Utilities;
using System.Web;
using System.IO;
using System.Data;
using System.Text;
using System.Collections.Generic;
using Microsoft.SharePoint.Workflow;
using System.Collections;

namespace EWWForms
{

    public class EWWListEventReceiver : SPItemEventReceiver
    {

        public EWWListEventReceiver()
        {
 
        }

        public override void ItemAdding(SPItemEventProperties properties)
        {
            base.ItemAdding(properties);
        }
        public override void ItemAdded(SPItemEventProperties properties)
        {
          
            base.ItemAdded(properties);
        }
        public override void ItemUpdating(SPItemEventProperties properties)
        {
            base.ItemUpdating(properties);
        }
        public override void ItemUpdated(SPItemEventProperties properties)
        {
           
            base.ItemUpdated(properties);
        }
        public override void ItemDeleting(SPItemEventProperties properties)
        {
  
            base.ItemDeleting(properties);
        }
    }
}


STEP 2: Update the event code for the events that you want to handle.

In my case, I want to handle the ItemUpdating and ItemUpdated code.

Because I may want to track the changes to a document, I can store the listitem before it was changed in a static storage class in the ItemUpdating method.

I created a static class to store the SPListItem in the ItemUpdating method.

    public static class UpdatedEWW
    {
        private static Dictionary<Guid, SPListItem> _dictionary=null;
        public static  Dictionary<Guid, SPListItem> dictionary
        {
            get
            {
                if (_dictionary == null)
                {
                    _dictionary = new Dictionary<Guid, SPListItem>();
                }
                return _dictionary;
            }
        }
    }


 
Step 3: Add the ItemUpdating Code
        public override void ItemUpdating(SPItemEventProperties properties)
        {
            SPListItem item = properties.ListItem;
            UpdatedEWW.dictionary[item.UniqueId] = item;
            base.ItemUpdating(properties);
        }

Step 4: Add the ItemUpdated Code

public override void ItemUpdated(SPItemEventProperties properties)
        {
            try
            {
                StringBuilder sbOutput = new StringBuilder();
                SPListItem item = properties.ListItem;
                SPWeb web = item.Web;

                List<Guid> taskListIDs = new List<Guid>();
                //Get the task lists in the web
                sbOutput.Append("<table border='1'>");
                foreach (SPList list in web.Lists)
                {
                    if (list.BaseTemplate.ToString() == "Tasks")
                    {
                        sbOutput.Append("<tr><td>" + HttpUtility.HtmlEncode(list.Title) + "</td><td>" + list.BaseTemplate.ToString() + "</td>");
                        sbOutput.Append("<td>");
                        foreach (SPContentType ct in list.ContentTypes)
                        {
                            sbOutput.Append("[" + ct.Name + "]" + "<br />");
                            if (ct.Name == "Approval Form Content Type v2.0")
                            {
                                taskListIDs.Add(list.ID);
                                break;
                            }
                        }
                        sbOutput.Append("</td>");
                    }

                    sbOutput.Append("</tr>");
                }
                sbOutput.Append("</table>");

                SPList ewwlist = item.ParentList;
                   
                //Query for tasks that are not completed, yet.
                    sbOutput.Append("<div style='border-top: solid 1px silver;'>" + item.Title + "</div>");
                    foreach (Guid taskListID in taskListIDs)
                    {
                        SPList taskList = web.Lists[taskListID];
                        sbOutput.Append("Checking [" + HttpUtility.HtmlEncode(taskList.Title) + "] task list for pending tasks: ");
                        SPQuery query = new SPQuery();
                        string strQuery = "<Where><Neq><FieldRef Name='Status' /><Value Type='Text'>Completed</Value></Neq></Where>";
                        query.Query = strQuery;
                        SPListItemCollection taskItems = taskList.GetItems(query);
                        sbOutput.Append("Total tasks in task list: " + taskItems.Count.ToString() + "<br />");
                        //For each task, see if the task links to the item
                        foreach (SPListItem taskItem in taskItems)
                        {
                            if (taskItem.Fields.ContainsField("Link"))
                            {
                                string[] passedLink = taskItem["Link"].ToString().Split(',');

                                string Link = passedLink[0].Trim();
                                sbOutput.Append("Checking Link: " + Link + "<br />");
                                try
                                {
                                    SPList linkedItemList = null;
                                    try
                                    {
                                        //NOTE: If your workflow is referencing a document rather than a list item, you could use web.GetFile(url) and web.GetListItem(url) to get the SPFile and SPListItem.

                                        linkedItemList = web.GetListFromUrl(Link);
                                    }
                                    catch (Exception exDocVList)
                                    {
                                        sbOutput.Append("Error Getting List from Link.  This may be a document link rather than a list item link.<br />");
                                        continue;
                                    }
                                    if (linkedItemList != null && linkedItemList.ID == ewwlist.ID)
                                    {
                                        //You would probably use web.GetListItem(Link) here.  I had to make my own function because my SPList uses its own forms.
                                        SPListItem linkedItem = EWWTravelHelper.GetSPListItemFromLink(web, Link);
                                        if (linkedItem.ID == item.ID)
                                        {
                                           
                                                    //Here you would put your logic for whether the task on the item is complete.  In my case, the logic checks to see if they updated certain fields on the SPListItem. If it is complete, then you would alter the task.

                                          }       
        
                         }
         
            }
            catch (Exception ex) {
           
               
            }
           
            base.ItemUpdated(properties);
        }


Step 5: Add your logic to alter the task to set it to complete if it should be complete.  You can use the SPWorkflowTask.AlterTask to do this.

 //Mark the task as completed
                                                        Hashtable taskHash = new Hashtable();                               // Task data is sent via a Hash Table
                                                        string strUser = "";
                                                        if (web.CurrentUser != null)
                                                        {
                                                            strUser = " by " + web.CurrentUser.Name + ".";
                                                        }
                                                        string strApprovalNotes = "Actual hours may have been updated by " + strUser + ".";

                                                        taskHash[Fields.ApprovalNotesFieldName.ToString()] = strApprovalNotes;
                                                        taskHash[Fields.ApprovalStatusFieldName.ToString()] = true;
                                                        web.AllowUnsafeUpdates = true;
                                                        if (SPWorkflowTask.AlterTask(taskItem, taskHash, false))
                                                        {
                                                            sbOutput.Append("Successfully marked the task as completed.<br />");
                                                        }
                                                        else
                                                        {
                                                            sbOutput.Append("Failed to update the task status.<br />");
                                                        }// Send the data to the task list, by altering the tasks value
                                                        web.AllowUnsafeUpdates = false;


Step 6: Build your solution and add the event receiver to your custom list feature's element file.
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
 <ListTemplate Image="/_layouts/images/itgen.png" Description="EWW List"
 DisplayName="EWW Requirement List" Sequence="410" SecurityBits="11"
 OnQuickLaunch="TRUE"
 BaseType="0"
 Type="10010"
 Name="EWWRequirementList"/>
 <Receivers ListTemplateId="10010" ListTemplateOwner="E14D0325-CC33-41BE-9A5C-2ACA3BD2765D">
  <Receiver>
   <Name>EWWListItemUpdated</Name>
   <Type>ItemUpdated</Type>
   <SequenceNumber>10000</SequenceNumber>
   <Assembly>YourNameSpace, Version=2.0.0.0, Culture=neutral, PublicKeyToken=87b00efff535493c</Assembly>
   <Class>YourNameSpace.EWWListEventReceiver</Class>
  </Receiver>
  <Receiver>
   <Name>EWWListItemUpdating</Name>
   <Type>ItemUpdating</Type>
   <SequenceNumber>10000</SequenceNumber>
   <Assembly>YourNameSpace, Version=2.0.0.0, Culture=neutral, PublicKeyToken=87b00efff535493c</Assembly>
   <Class>YourNameSpace.EWWListEventReceiver</Class>
  </Receiver>
 </Receivers>
</Elements>

Step 7: You can alternatively write custom code to manage your event receivers:
  protected string AddEventReceiverToList(SPWeb web, string strListName, string strEventReceiverName,
        SPEventReceiverType EventReceiverType, string AssemblyName, string ClassName, Boolean bCreateIfNotExists)
    {
        //Verify that the Updating Event Receiver exists, since this a new event receiver that wasn't in the original SigEvent solution package
        Boolean bReceiverFound = false;
        string strRet = "";
        try
        {
            SPList list = web.Lists[strListName];
            foreach (SPEventReceiverDefinition sperd in list.EventReceivers)
            {
                if (sperd.Class == ClassName && sperd.Type == EventReceiverType)
                {
                    bReceiverFound = true;
                    break;
                }
            }

            if (!bReceiverFound && bCreateIfNotExists)
            {
                SPEventReceiverDefinition er = list.EventReceivers.Add();
                er.Name = strEventReceiverName;
                er.Type = EventReceiverType;
                er.SequenceNumber = 10000;
                er.Assembly = AssemblyName;
                er.Class = ClassName;

                er.Update();
            }
            else
            {
                strRet = strEventReceiverName + " ALREADY attached to  " + EventReceiverType.ToString() + " event on list " + list.Title + "<br />";
                return strRet;
            }
            strRet = strEventReceiverName + " attached to  " + EventReceiverType.ToString() + " event on list " + list.Title + "<br />";
        }
        catch (Exception ex)
        {
            strRet = "Failed to attach "+ strEventReceiverName + " to  " + EventReceiverType.ToString() + " event on list " + strListName + ": " + ex.Message + "<br />";
        }
        return strRet;
    }


 protected string RemoveEventReceiverFromList(SPWeb web, string strListName, string strEventReceiverName,
       SPEventReceiverType EventReceiverType, string AssemblyName, string ClassName)
    {
        //Verify that the Updating Event Receiver exists, since this a new event receiver that wasn't in the original SigEvent solution package
        Boolean bReceiverFound = false;
        string strRet = "";
        try
        {
            SPList list = web.Lists[strListName];
            Guid erID=Guid.Empty;
            foreach (SPEventReceiverDefinition sperd in list.EventReceivers)
            {
                if (sperd.Class == ClassName && sperd.Type == EventReceiverType)
                {
                    bReceiverFound = true;
                    erID = sperd.Id;
                    break;
                }
            }

            if (bReceiverFound && erID != Guid.Empty)
            {
                list.EventReceivers[erID].Delete();
            }
            else
            {
                strRet = strEventReceiverName + ", " + EventReceiverType.ToString() + " was NOT attached to the list " + list.Title + "<br />";
                return strRet;
            }

            strRet = strEventReceiverName + ", " + EventReceiverType.ToString() + " was removed from list " + list.Title + "<br />";
        }
        catch (Exception ex)
        {
            strRet = "Failed to attach " + strEventReceiverName + " to  " + EventReceiverType.ToString() + " event on list " + strListName + ": " + ex.Message + "<br />";
        }
        return strRet;
    }

Using the same concept for looking up tasks related to a SPListItem, here is a function to display any pending tasks related to the list item:
        //Given a list item, display links to tasks in the same web site associated with the item
        public static string DisplayPendingTasks(SPListItem item)
        {StringBuilder sbOutput = new StringBuilder();
        int iCountPendingTasks = 0;
        StringBuilder sbDebugOutput = new StringBuilder();
           try
            {
                               
                SPWeb web = item.Web;
                List<Guid> taskListIDs = new List<Guid>();
                //Get the task lists in the web
                sbDebugOutput.Append("<table border='1'>");
                foreach (SPList list in web.Lists)
                {
                    if (list.BaseTemplate.ToString() == "Tasks")
                    {
                        sbDebugOutput.Append("<tr><td>" + HttpUtility.HtmlEncode(list.Title) + "</td><td>" + list.BaseTemplate.ToString() + "</td>");
                        sbDebugOutput.Append("<td>");
                        foreach (SPContentType ct in list.ContentTypes)
                        {
                            sbDebugOutput.Append("[" + ct.Name + "]" + "<br />");
                            if (ct.Name == "USACISAP Approval Form Content Type v2.0")
                            {
                                taskListIDs.Add(list.ID);
                                break;
                            }
                        }
                        sbDebugOutput.Append("</td>");
                    }

                    sbDebugOutput.Append("</tr>");
                }
                sbDebugOutput.Append("</table>");

                SPList ewwlist = item.ParentList;
              
                //Query for tasks that are not completed, yet.
                sbDebugOutput.Append("<div style='border-top: solid 1px silver;'>" + item.Title + "</div>");
                foreach (Guid taskListID in taskListIDs)
                {
                    SPList taskList = web.Lists[taskListID];
                    sbDebugOutput.Append("Checking [" + HttpUtility.HtmlEncode(taskList.Title) + "] task list for pending tasks: ");
                    SPQuery query = new SPQuery();
                    string strQuery = "<Where><Neq><FieldRef Name='Status' /><Value Type='Text'>Completed</Value></Neq></Where>";
                    query.Query = strQuery;
                    SPListItemCollection taskItems = taskList.GetItems(query);
                    sbDebugOutput.Append("Total tasks in task list: " + taskItems.Count.ToString() + "<br />");
                    sbOutput.Append("<div class='PendingTasksDiv'><strong>Pending Tasks for this request:</strong><br />");
                    //For each task, see if the task links to the item
                    foreach (SPListItem taskItem in taskItems)
                    {
                        if (taskItem.Fields.ContainsField("Link"))
                        {
                            string[] passedLink = taskItem["Link"].ToString().Split(',');

                            string Link = passedLink[0].Trim();
                            sbDebugOutput.Append("Checking Link: " + Link + "<br />");
                            try
                            {
                                SPList linkedItemList = null;
                                try
                                {
                                    SPFile file = web.GetFile(Link);
                                    linkedItemList = web.GetListFromUrl(Link);
                                }
                                catch (Exception exDocVList)
                                {
                                    sbDebugOutput.Append("Error Getting List from Link.  This may be a document link rather than a list item link.<br />");
                                    continue;
                                }
                                if (linkedItemList != null && linkedItemList.ID == ewwlist.ID)
                                {
                                    SPListItem linkedItem = EWWTravelHelper.GetSPListItemFromLink(web, Link);
                                    if (linkedItem.ID == item.ID)
                                    {
                                        sbDebugOutput.Append("Found a pending task for this list item.<br />");
                                        //Update the task, if it should be updated
                                       
                                        string taskUrl=CommonFunctions.GetDisplayItemUrl(taskItem);
                                        iCountPendingTasks++;
                                        sbOutput.Append("<a href=\"" + taskUrl + "\">" + HttpUtility.HtmlEncode(taskItem.Title) + "</a><br />");
                                 
                                    }
                                }
                            }
                            catch (Exception ex)
                            {
                                sbOutput.Append("Error occurred: " + ex.ToString() + "<br />");
                            }
                        } //if task has a link field
                    } //for each task
                    sbOutput.Append("</div>");
                } //foreach task list

            }
            catch (Exception ex)
            {

            }
           if (iCountPendingTasks > 0)
           {
               return sbOutput.ToString();
           }
           else
           {
               return "";
           }
        }

Here is the code for GetSPListItemFromLink:
NOTE: this works for listitems vs document file links. You could probably just use web.GetListItem(url) and SPFile file = web.GetFile(url) if the link is to a document library file.

        /// <summary>
        /// Given an SPWeb and Url, this function will evaluate the link and return the SPListItem
        /// </summary>
        /// <param name="web"></param>
        /// <param name="strLink"></param>
        /// <returns></returns>
        public static SPListItem GetSPListItemFromLink(SPWeb Web, string strLink)
        {
           
            string documentViewLink = strLink.Trim();
            SPList itemlist = Web.GetListFromUrl(documentViewLink);
            string DocLinkTextInfo = "";
            int idStartIndex = -1;
            int itemID = -1;
            if (documentViewLink.IndexOf("?ID=") >= 0)
            {
                idStartIndex = documentViewLink.IndexOf("?ID=") + 4;
                DocLinkTextInfo += "Found start index " + idStartIndex.ToString() + ".";
            }
            else if (documentViewLink.IndexOf("&ID=") >= 0)
            {
                idStartIndex = documentViewLink.IndexOf("&ID=") + 4;
            }
            if (idStartIndex >= 0)
            {
                int iEndIndex = -1;
                if (documentViewLink.Substring(idStartIndex).IndexOf("&") >= 0)
                {
                    iEndIndex = documentViewLink.Substring(idStartIndex).IndexOf("&");
                    itemID = int.Parse(documentViewLink.Substring(idStartIndex, iEndIndex));
                }
                else
                {
                    itemID = int.Parse(documentViewLink.Substring(idStartIndex));
                }
                SPListItem item = itemlist.GetItemById(itemID);
                return item;
            }
            else
            {
                try
                {
                    SPListItem item = Web.GetListItem(strLink);
                    return item;
                }
                catch (Exception ex)
                {
                    return null;
                }
            }
            return null;

        }

Comments

Popular posts from this blog

How To use ASPNET_SetReg to store encrypted data in the registry and then decrypt the data for use in your app

Nostalgia for SNL's Il Returno De Hercules

PowerShell Script to Clean the Windows Installer Directory