Overview
The following workflow example is something that I developed to manage tasks that need to be completed for the new employee hiring process. My example will assume that you have some knowledge of SharePoint 2007 and have at the very minimum gone through the following blog:
http://weblog.vb-tech.com/nick/archive/2006/09/04/1753.aspx.
Scenario
There is a list of tasks that need to be completed when a new person is hired. The tasks are performed in a specified order by different people. One or more tasks are dependent on one or more other tasks to complete.
Custom Lists
There are two lists required by this example:
-
A list to start the workflow process, in this case I will call it New Employee. New Employee will contain the following columns:
- New Hire Name – Single line of text field that will contain the name of the new employee.
- New Hire Title – Sing line of text containing the new hire’s job title.
- Start Date – A date time field containing the start date for the new employee.
- Hiring Manager – A person or group field containing the name of the hiring manager.
-
Onboarding Task – This is a list that will contain the tasks to be performed by the workflow. In this case the list will contain the following columns:
- Title – Single line of text field that will contain the task description.
- Task Owner Type – A drop down containing "Default" and "Hiring Manager".
- Default Responsibility – A person or group field that contains the person responsible for the task if Task Owner Type is Default.
- Dependencies – A lookup list that will point to itself (New EmployeeTask) and will allow for multiple values.
- Display Order – A number field containing a display order that will not be used in this example.
You will need to populate the New Employee Task list with some items…make sure you have at least one item that does not have any dependencies.
Visual Studio Code
Create a new Visual Studio 2005 project selecting a Sequential Workflow Library. Drop a code activity below the onWorkflowActivated activity. The designer should look like the image below:
For this example I created the following custom classes:
-
NewEmployee – Represents the new employee and holds the tasks to be completed during the workflow. The properties for this class are as follows:
- Name – A string containing the new hires name.
- StartDate – A DateTime field containing the new employee’s start date.
- Tasks – A generic list of NewEmployeeTask
-
OnboardingTask – Represents a task to be completed. The properties for this class are defined as follows:
- ID – Indicates the ID of the corresponding item in New Employee Task
- Title – The title of the task.
- TaskOwner – The default responsibility owner of the task.
- Dependencies – Tasks that must be completed before this task is created. This is an ArrayList of integers containing the IDs of dependent items.
- Dependents – Tasks that cannot be created until this one is complete. This is an ArrayList of integers containing the IDs of dependent items.
- Complete – A Boolean indicating that the task has been completed.
- Processing – A Boolean indicating that the task is processing.
Now that the custom classes are done we can start coding that actual workflow. Start by selecting the code activity in the workflow designer. In the properties window set the ExecuteCode property to processTasks. You will also need to set the appropriate properties on the onWorkflowActivated activity. Those properties are explained in the blog link at the beginning of this blog.
The onWorkflowActivated method will be used to setup the workflow tasks and the new employee information. The method should look like the following:
#region onWorkflowActivated
private void onWorkflowActivated(object sender, ExternalDataEventArgs e)
{
workflowId = workflowProperties.WorkflowId;
_name = workflowProperties.Item["New Hire Name"].ToString();
_title = workflowProperties.Item["New Hire Title"].ToString();
_startDate = Convert.ToDateTime(workflowProperties.Item["Start Date"].ToString());
_manager = workflowProperties.Item["Hiring Manager"].ToString();
_newEmployee = new NewEmployee();
_newEmployee.Name = _name;
_newEmployee.Title = _title;
_newEmployee.StartDate = _startDate;
_newEmployee.Manager = _manager;
SPSite site = new SPSite(workflowProperties.WebUrl);
SPWeb web = site.OpenWeb();
SPList taskList = web.Lists["Onboarding Task"];
OnboardingTask task = null;
foreach (SPListItem item in taskList.Items)
{
task = new OnboardingTask();
task.ID = item.ID;
task.Complete = false;
task.Title = item["Title"].ToString();
if (item["Task Owner Type"].ToString() == "Default")
task.TaskOwner = item["Default Responsibility"].ToString();
else
task.TaskOwner = _manager;
task.DisplayOrder = Convert.ToInt32(item["Display Order"].ToString());
GetDependencies(task, item);
_newEmployee.Tasks.Add(task);
}
SetDependents(_newEmployee.Tasks);
}
#endregion
The code sample above refers to GetDependencies and SetDependents. GetDependencies will set the tasks that need to be completed before the current task is created. GetDependencies was coded like this:
#region GetDependencies
private void GetDependencies(OnboardingTask task, SPListItem item)
{
if (item["Dependencies"].ToString().Length == 0)
return;
SPFieldLookupValueCollection deps = (SPFieldLookupValueCollection)item["Dependencies"];
foreach (SPFieldLookupValue value in deps)
{
task.Dependencies.Add(value.LookupId);
}
}
#endregion
SetDependents will set the tasks that require the current task to be completed before they are created. I used this method to set up the property that drives which tasks are created when one is completed. SetDependents was coded like this:
#region SetDependents
private void SetDependents(List<OnboardingTask> tasks)
{
foreach (OnboardingTask t in tasks)
{
foreach (OnboardingTask t2 in tasks)
{
if (t2.Dependencies.Contains(t.ID))
t.Dependents.Add(t2.ID);
}
}
}
#endregion
The next step in the workflow to execute is the processTasks method that is defined by the ExecuteCode property in the CodeActivity. Process tasks will determine which tasks to create first. The code looks like the following:
#region processTasks
private void processTasks(object sender, EventArgs e)
{
List<OnboardingTask> list = GetNextTasksToExecute();
if (list.Count == 0)
return;
if (list.Count > 1)
ProcessMultipleTasks(list);
else
ProcessSingleTask(list[0]);
}
#endregion
GetNextTasksToExecute will return a list of tasks to be created. The method will check to see if the task is marked as completed if it is not it will make sure it’s dependencies are completed. If the dependencies are marked as completed, then the task will be added to the list of tasks to execute next. If the task is marked as complete, then the process will inspect the dependents. If the dependents are not completed or processing and it’s dependencies are all completed the it will be added to the list. The code is GetNextTasksToExecute, GetTaskByID, DependenciesComplete, and ListContainsTask are shown below:
#region GetNextTasksToExecute
private List<OnboardingTask> GetNextTasksToExecute()
{
List<OnboardingTask> list = new List<OnboardingTask>();
OnboardingTask subTask = null;
foreach (OnboardingTask task in _newEmployee.Tasks)
{
if (task.Complete)
{
foreach (int subTaskID in task.Dependents)
{
subTask = GetTaskByID(subTaskID, _newEmployee.Tasks);
if (!subTask.Complete && DependenciesComplete(subTask) && !subTask.Processing)
{
if (!ListContainsTask(list, subTask.ID))
list.Add(subTask);
}
}
}
else
{
if (!task.Processing && DependenciesComplete(task))
{
if (!ListContainsTask(list, task.ID))
list.Add(task);
}
}
}
return list;
}
#endregion
#region GetTaskByID
private OnboardingTask GetTaskByID(int id, List<OnboardingTask> list)
{
foreach (OnboardingTask task in list)
{
if (task.ID == id)
{
return task;
}
}
return null;
}
#endregion
#region DependenciesComplete
private bool DependenciesComplete(OnboardingTask task)
{
OnboardingTask dependency = null;
foreach (int taskID in task.Dependencies)
{
dependency = GetTaskByID(taskID, _newEmployee.Tasks);
if (!dependency.Complete)
return false;
}
return true;
}
#endregion
#region ListContainsTask
private bool ListContainsTask(List<OnboardingTask> list, int taskID)
{
foreach (OnboardingTask task in list)
{
if (task.ID == taskID)
return true;
}
return false;
}
#endregion
The reason for the method above is to remove the chance of duplication since tasks can have the same dependents.
The next step in the process will process single tasks or multiple tasks depending on the value that is returned by GetNextTasksToExecute. If GetNextTasksToExecute returns a count greater than one then ProcessMultipleTasks methods is called. This method will create all of the tasks and add them to a ParallelActivity. If the count is one, then the ProcessSingleTask is executed. ProcessSingleTask does the same thing as ProcessMultipleTasks except the task is added to a SequenceActivity instead of a ParallelActivity. The code for ProcessMultipleTasks and ProcessSingleTask is shown below:
#region ProcessSingleTask
private void ProcessSingleTask(OnboardingTask task)
{
CreateTask t = null;
OnTaskChanged changed = null;
CompleteTask complete = null;
WorkflowChanges changes = new WorkflowChanges(this);
CompositeActivity ca = changes.TransientWorkflow;
SequenceActivity sa = new SequenceActivity("sequenceActivity" + _counter.ToString());
string activity = string.Empty;
ca.Activities.Add(sa);
try
{
activity = "WORKFLOW_TASKS";
t = GetTask(task, _counter);
_counter++;
sa.Activities.Add(t);
WhileActivity wa = GetWhileActivity();
activity = "WORKFLOW_LISTENERS";
changed = GetOnTaskChanged(t.TaskId);
wa.Activities.Add(changed);
sa.Activities.Add(wa);
activity = "WORKFLOW_COMPLETE_TASK";
complete = GetCompleteTask(t.TaskId);
sa.Activities.Add(complete);
task.Processing = true;
activity = "APPLY_CHANGES";
ApplyWorkflowChanges(changes);
}
catch (System.Workflow.ComponentModel.Compiler.WorkflowValidationFailedException ex)
{
EventLog.WriteEntry(activity, ex.Message);
}
}
#endregion
#region ProcessMultipleTasks
private void ProcessMultipleTasks(List<OnboardingTask> list)
{
CreateTask t = null;
OnTaskChanged changed = null;
CompleteTask complete = null;
WorkflowChanges changes = new WorkflowChanges(this);
CompositeActivity ca = changes.TransientWorkflow;
ParallelActivity parTasks = new ParallelActivity("parallelActivity" + _counter.ToString());
SequenceActivity sa = null;
int i = 0;
string activity = string.Empty;
_counter++;
try
{
foreach (OnboardingTask task in list)
{
sa = new SequenceActivity("sequenceActivity" + _counter.ToString());
_counter++;
activity = "WORKFLOW_TASKS";
if (i == 0)
parTasks.Activities.Clear();
t = GetTask(task, _counter);
_counter++;
sa.Activities.Add(t);
parTasks.Activities.Add(sa);
WhileActivity wa = GetWhileActivity();
activity = "WORKFLOW_LISTENERS";
changed = GetOnTaskChanged(t.TaskId);
wa.Activities.Add(changed);
sa.Activities.Add(wa);
activity = "WORKFLOW_COMPLETE_TASK";
complete = GetCompleteTask(t.TaskId);
sa.Activities.Add(complete);
task.Processing = true;
i++;
}
activity = "APPLY_CHANGES";
ca.Activities.Add(parTasks);
ApplyWorkflowChanges(changes);
}
catch (System.Workflow.ComponentModel.Compiler.WorkflowValidationFailedException ex)
{
EventLog.WriteEntry(activity, ex.Message);
}
}
#endregion
The methods used by the process task methods above are shown below:
#region GetTask
private CreateTask GetTask(OnboardingTask task, int index)
{
CreateTask t = new CreateTask();
t.TaskId = Guid.NewGuid();
t.Name = "CreateTask" + _counter.ToString();
_counter++;
t.CorrelationToken = new CorrelationToken(t.TaskId.ToString());
t.CorrelationToken.OwnerActivityName = this.QualifiedName;
t.TaskProperties = new SPWorkflowTaskProperties();
t.TaskProperties.TaskItemId = task.ID;
t.TaskProperties.Title = task.Title;
t.TaskProperties.Description = task.Title;
t.TaskProperties.AssignedTo = GetUserLogin(task.TaskOwner);
t.TaskProperties.SendEmailNotification = true;
t.TaskProperties.ExtendedProperties["TaskID"] = task.ID;
t.TaskProperties.ExtendedProperties["Completed"] = task.Complete.ToString();
return t;
}
#endregion
The reason for GetWhileActivity is that if the OnTaskChanged is not contained within a WhileActivity, the OnTaskChanged event will only fire once. If you are using a cancel button or more than one status, this is necessary. If this is excluded your workflow will remain in this state and the dependent tasks will never be created.
#region GetWhileActivity
private WhileActivity GetWhileActivity()
{
WhileActivity wa = new WhileActivity("WhileActivity" + _counter.ToString());
_counter++;
CodeCondition cond = new CodeCondition();
cond.Condition += new System.EventHandler<System.Workflow.Activities.ConditionalEventArgs>(this.notFinished);
wa.Condition = cond;
return wa;
}
#endregion
#region GetOnTaskChanged
private OnTaskChanged GetOnTaskChanged(Guid taskID)
{
OnTaskChanged changed = new OnTaskChanged();
changed.Name = "OnChanged" + _counter.ToString();
_counter++;
changed.BeforeProperties = new SPWorkflowTaskProperties();
changed.AfterProperties = new SPWorkflowTaskProperties();
changed.CorrelationToken = new CorrelationToken(taskID.ToString());
changed.CorrelationToken.OwnerActivityName = this.QualifiedName;
changed.TaskId = taskID;
changed.EventName = "OnTaskChanged";
changed.Invoked += new System.EventHandler<System.Workflow.Activities.ExternalDataEventArgs>(this.onTaskChanged);
return changed;
}
#endregion
#region GetCompleteTask
private CompleteTask GetCompleteTask(Guid taskID)
{
CompleteTask complete = new CompleteTask();
complete.Name = "CompleteTask" + _counter.ToString();
_counter++;
complete.TaskId = taskID;
complete.CorrelationToken = new CorrelationToken(taskID.ToString());
complete.CorrelationToken.OwnerActivityName = this.QualifiedName;
complete.MethodName = "CompleteTask";
return complete;
}
#endregion
#region notFinished
private void notFinished(object sender, ConditionalEventArgs e)
{
e.Result = !_completed;
_completed = false;
}
#endregion
I haven’t yet figured out how to branch each task independently, so the next round of tasks will not be created until each task that is put in the ParallelActivity is marked as completed. When I figure that out, I will update this blog.
The code from above can be downloaded from here