Monday, November 18, 2013

Creating a custom sequential workflow similar with SharePoint OOTB Approval workflow (1) Association / Initiation Form


Creating a custom sequential workflow similar with SharePoint OOTB Approval workflow (1) Association / Initiation Form
Creating a custom sequential workflow similar with SharePoint OOTB Approval workflow (2) Custom Activity, While Activity and Replicator Activity
Creating a custom sequential workflow similar with SharePoint OOTB Approval workflow (3) custom Content Type and custom Task Edit Form
Creating a custom sequential workflow similar with SharePoint OOTB approval workflow (4) End on first rejection
Creating a custom sequential workflow similar with SharePoint OOTB approval workflow (5) cancel task(s) when item changed
Creating a custom sequential workflow similar with SharePoint OOTB approval workflow (6) reassign task and request change

a.     To dynamically enter multiple stages for multiple approvers in parallel mode or serial mode in Association/Initiation form by user, we have following association form (initiation form is the same):


DateTime related functions (such as  Due Date, Duration Per Task, …) are not included.
The “Approvers” control is a GridView, the “Assign To” control is a SharePoint PeopleEditor, the “Order” control is a DropDownList, and the Request control is a MultiLine TextBox.
This page is kind of a regular Asp.Net page, so I will not show all the detailed code here.

b.    To pass stages and approvers information from Association/Initiation form to approval workflow, we need to update the form’s GetAssociationData()/GetInitiationData() method by using  SerializeData.SerializeFormToXML method.
The approvers(Assign To and order) is a dynamic array that grows and reduces as needed. Following is the code snippet:

     public class Stages
    {
        public String AssignedTo = default(string);
        public String Order = default(string);
    }

    [Serializable]
    public class MyCustomData : IDisposable
    {
        private bool disposed = false;
        private string _request = default(string);

        public string Request
        {
            get { return _request; }
            set { _request = value; }
        }

        private List<Stages> _stages = new List<Stages>();

        public List<Stages> Stages
        {
            get { return _stages; }
            set { _stages = value; }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    // Dispose managed resources.
                    //component.Dispose();
                }
                disposed = true;
            }
        }
    }

    public class SerializeData
    {
        //serailize data to XML
        public static string SerializeFormToXML(MyCustomData data)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(MyCustomData));
            using (StringWriter writer = new StringWriter())
            {
                serializer.Serialize(writer, data);
                return writer.ToString();
            }
        }
    }

Update the association form’s GetAssociationData() as following( for initiation form, it is GetInitiationData()):

private string GetAssociationData()
{
            // TODO: Return a string that contains the association data that will be passed to the workflow. Typically, this is in XML format.
            //return string.Empty;

            MyCustomData assocForm = new MyCustomData();
            assocForm.Request = txtRequest.Text;
            Stages myStage = null;
            DataTable dtCurrentTable = (DataTable)ViewState["CurrentTable"];
            for (int i = 0; i < Gridview1.Rows.Count; i++)
            {
                PeopleEditor myPE = (PeopleEditor)Gridview1.Rows[i].FindControl("taskAssignedToPicker");
                DropDownList myOrder = (DropDownList)Gridview1.Rows[i].FindControl("Order");
                if (myPE.ResolvedEntities.Count > 0)
                {
                    myStage = new Stages();
                    for (int j = 0; j < myPE.ResolvedEntities.Count; j++)
                    {
                        PickerEntity selectedEntity = (PickerEntity)myPE.ResolvedEntities[j];
                        myStage.AssignedTo = myStage.AssignedTo + selectedEntity.Key + "," + selectedEntity.EntityType + ",";
                    }
                    myStage.Order = myOrder.SelectedValue;
                    assocForm.Stages.Add(myStage);
                }
            }
            return SerializeData.SerializeFormToXML(assocForm);
        }

We can see selectedEntity.EntityType is appended to field AssignedTo, this is to identify if the selectedEntity.Key is a single user or a group. Later on, we will see the code for each group entered, assign a task to every member of that group(topic d).
Following code snippet shows how the workflow gets the Association/Initiation data in workflow:

    public sealed partial class ApprovalWorkflow : SequentialWorkflowActivity
    {
        public ApprovalWorkflow()
        {
            InitializeComponent();
        }

       public Guid workflowId = default(System.Guid);
       public SPWorkflowActivationProperties workflowProperties = new SPWorkflowActivationProperties();
        public IList stages = default(System.Collections.IList);

        private void onWorkflowActivated1_Invoked(object sender, ExternalDataEventArgs e)
       {
            XmlSerializer serializer = new XmlSerializer(typeof(MyCustomData));
            XmlTextReader reader = new XmlTextReader(new StringReader(workflowProperties.InitiationData));
            stages = new ArrayList();
            using (MyCustomData initData = serializer.Deserialize(reader) as MyCustomData)
            {
                foreach (Stages myStage in initData.Stages)
                {
                    stages.Add(myStage.AssignedTo + myStage.Order);
                }
                RequestMessage = initData.Request;
            }
        }

c.     Every time when the association/Initiation form is loaded, check The association data, if it is not null, populate existing association data to the form:

SPWorkflowAssociation association = this.workflowList.WorkflowAssociations[new Guid(this.associationGuid)];
            if (association!= null)
            {
                XmlSerializer serializer = new XmlSerializer(typeof(MyCustomData));
                XmlTextReader reader = new XmlTextReader(new StringReader(association.AssociationData));
                ArrayList stages = new ArrayList();
                string RequestMessage = "";
                using (MyCustomData assocData = serializer.Deserialize(reader) as MyCustomData)
                {
                    foreach (Stages myStage in assocData.Stages)
                    {
                        //Populate association data to this form.
                    }
                        RequestMessage = assocData.Request;
                }
            }

d.    For each group entered, assign a task to every member of that group. Following is the method to pick up every single user from groups, the parameter _AssignTo is an ArrayList, the items are login name followed by user type:

        public static ArrayList UserList(ArrayList _AssignTo, SPWeb myWeb)
        {
            ArrayList UserList = new ArrayList();
            if (_AssignTo.Count > 1)
            {
                for (int j = 0; j < _AssignTo.Count; j = j + 2)
                {
                    if (_AssignTo[j + 1].ToString() == "User")
                    {
                        SPUser singlevalue = myWeb.AllUsers[_AssignTo[j].ToString()];
                        if (!UserList.Contains(singlevalue.LoginName))
                        {
                            UserList.Add(singlevalue.LoginName);
                        }
                    }
                    else
                    {
                        SPGroup group = myWeb.Groups[_AssignTo[j].ToString()];
                        foreach (SPUser user in group.Users)
                        {
                            // add all the group users to the list
                            if (!UserList.Contains(user.LoginName))
                            {
                                UserList.Add(user.LoginName);
                            }
                        }
                    }
                }
            }
            return UserList;
        }

After all of above steps, we have the stages data in workflow’s stages IList, every item is a string of users login name separated by comma, the last letter of this string is a letter “P” or “S” also separated by comma indicating this stage is in Parallel mode or Serial mode.

No comments:

Post a Comment