Search This Blog

Saturday, June 15, 2013

Display workflow history in custom InfoPath task forms

If you have worked on custom InfoPath task forms with code behind, here is a ready made enhancement which users loved and hope your users will too..

What it does:
Retrieves the workflow history items associated with a workflow instance and shows them in a Grid in InfoPath form.

How is it useful:
For a given custom workflow in SharePoint - the InfoPath task form and associated workflow history are 2 different pages.  If you already have a custom task form in IP,  then using this method we can display workflow history so that every approver can see what has happened so far in the workflow (with latest on the top so that they can quickly see the task history prior to their approval), and this could help in making informed decision on task approval. Additionally the dates are shown similar to SharePoint 2013 format i.e. for example:
Today at 3:45 PM
Yesterday at 12:10 PM
Day before yesterday at 12:15 PM

Sample output:


Skills needed to implement this:
SharePoint workflows, InfoPath code behind knowledge

Can it be used on ASPX task forms?
Yes, it can be. Please change the yellow highlighted lines below which are specific to InfoPath

Our User Feedback:
They love it.

How to configure (please refer the image below):
a) Select main data source in InfoPath
b) Create a group element as shown below with name HistParent
c) Under HistParent group create an element with name "hs".  Data type is text and check on "repeating"
d) Remaining i.e. pos, un, oc, cm, dt are all text type and are "attributes" (not element)




e) Copy and paste the function below in code behind of your InfoPath task form.
f)  Call the method from form load passing in the WorkflowInstanceId and HistoryList Id
     How to pass in the above values:
      f1) Assuming you are using itemMetadata.xml for the task form - create 2 new fields for
            workflowInstanceId and historyListId
      f2) from the custom code workflow pass in the values like
   taskProperties.ExtendedProperties["workflowInstanceId"] = workflowproperties.WorkflowId.ToString("B") ; // Important to put B here since we need { } to be included in string
     
  taskProperties.ExtendedProperties["historyListId"] = workflowProperties.HistoryList.ID.ToString() ;


Source Code (included as is - no warranties)

        /// <summary>
        /// Function to display Workflowhistory for a given workflow instance in reverse chronological order
  /// </summary>
        /// <param name="workflowId">Contains the workflow instance guid with { } in string format</param>
        /// <param name="historyListId">GUID for history list without { } </param>
        public void GetWorkflowHistory(string workflowId, string historyListId, string siteURL, string webRelativeURL)
        {
            SPList historyList = null;
            // Open site collection
            using (SPSite site = new SPSite(siteURL))
            {
                // Open web
                using (SPWeb web = site.OpenWeb(webRelativeURL))
                {
                    try
                    {
                        // Check for existence of history list
                        historyList = web.Lists[new Guid(historyListId)];
                    }
                    catch // History list has been deleted or renamed
                    {
                        throw new ApplicationException("History List not found");
                    }
                    SPQuery qry = new SPQuery();
                    qry.ViewFieldsOnly = true; // Optimize performance
                    qry.ViewFields = "<FieldRef Name='User'/><FieldRef Name='Occurred'/><FieldRef Name='Outcome'/><FieldRef Name='Description'/>"; // Get specific fields only -  user name, date, task outcome and, task comments
                    // WorkflowInstance field contains the GUID of the workflow instance in history
                    qry.Query = string.Format("<Where><Eq><FieldRef Name=\"WorkflowInstance\"/><Value Type=\"Text\">{0}</Value></Eq></Where><OrderBy><FieldRef  Name=\"ID\" Ascending='False' /></OrderBy>", workflowId);
                    qry.DatesInUtc = false;
                    SPListItemCollection coll = historyList.GetItems(qry);
                    SPListItem item = null;
                    StringBuilder sb = new StringBuilder();
                    string userName, outcome, comments, sDT;
                    DateTime dt;
                    SPUserCollection uColl = web.AllUsers;
                    SPFieldUserValue uVal;
                    SPFieldUser uField = historyList.Fields.GetFieldByInternalName("User") as SPFieldUser; 
                    int count = coll.Count;
                    for (int ctr = 0; ctr < count; ctr++)
                    {
                        item = coll[ctr];
                        uVal = uField.GetFieldValue(item["User"] as string) as SPFieldUserValue; // get SPFieldUserValue object using User from history list
                        if (uVal != null)
                        {
                            userName = uVal.User.Name;
                        }
                        else
                        {
                            userName = "Not known";
                        }
                        outcome = item["Outcome"] as string;
                        dt = DateTime.Parse(Convert.ToString(item["Occurred"])); // Convert date occurred
                        /*The following block displays the date in SP 2013 kind of format i.e.
                         * when current date == Today then displays "Today at 3:45PM" (assuming time is 3:45 in occurred field).
                         * Similary for Yesteray and Day before yesterday. 
                         * When none of the if are satisfied - display the Long Date string format
                         */
                        if (dt.Date == DateTime.Today)
                        {
                            sDT = "Today at " + dt.ToShortTimeString();
                        }
                        else if (dt.AddDays(1).Date == DateTime.Today)
                        {
                            sDT = "Yesterday at " + dt.ToShortTimeString();
                        }
                        else if (dt.AddDays(2).Date == DateTime.Today)
                        {
                            sDT = "Day before yesterday at " + dt.ToShortTimeString();
                        }
                        else
                        {
                            sDT = dt.ToLongDateString();
                        }
                        comments = item["Description"] as string;
                        comments = (comments == null ? string.Empty : comments);
                        /*  The assumption here is that we have fields in our InfoPath main data source with these names.
                         * Abbreviations are:
                         * pos -- position (in reverse) 
                         * hs -- History
                         * un -- UserName (display name)
                         * oc -- Task outcome
                         * cm -- Comments by approving user or system
                         * dt -- date of history item - in reverse chronological order
                         */
                        sb.AppendFormat("<my:hs my:pos='{0}' my:un='{1}' my:oc='{2}' my:cm='{3}' my:dt='{4}' />", count - ctr, userName, outcome, comments, sDT);
                    }
                    // If there are any history items
                    if (sb.Length > 1)
                    {
                        XPathNavigator nav = CreateNavigator();
                        // Set the generated XML to InfoPath main datasource "Group" element i.e. History Parent
                        nav.SelectSingleNode("/my:myFields/my:HistParent", NamespaceManager).InnerXml = sb.ToString();
                    }
                }
            }
        }


Finally please let me know your feedback: