Tuesday, September 11, 2007

Leave workflow - Logging & Submitting

Finally, I have time again! With a successful live implementation of WF under my belt, let's return to the leave workflow example.

Last time, we had managed to create the complete leave application task, but now, we actually want to submit the task to the approver and log what is happening in the workflow.
First, let's add a sharepoint user group called Leave Linemanagers.
Second, let's add some columns to the Leave Applications list:
  • Line Manager - a people selection column restricted to the leave linemanager group.
  • Start Date - a date that defaults to today
  • End Date - a date that defaults to today

Make sure all the columns are required!

Now, open up the leave workflow solution. To the eventDrivenActivity1 in the InitializeLeaveState add a LogToHistoryListActivity called logLeaveCreated. Bind the HistoryDescription field to a new field calld HistoryDescription, the HistoryOutcome field to a new field calld HistoryOutcome and the UserId to a new field called HistoryUserID

Then go to the code behind and modify the createApplicantTask_MethodInvoking event so that it looks something like this:

private void setHistory(string description, string outcome, int logginUserID)
{
HistoryDescription = description;
HistoryOutcome = outcome;
HistoryUserID = logginUserID;
}
private void createApplicantTask_MethodInvoking(object sender, EventArgs e)
{
applicantTaskID = Guid.NewGuid();
createApplicantTask_TaskProperties1.AssignedTo = this.workflowProperties.Originator;
createApplicantTask_TaskProperties1.Description = "Complete your leave application";
createApplicantTask_TaskProperties1.Title = "Leave Application";
setHistory(string.Format("Leave application created by {0} on {1}",
this.workflowProperties.OriginatorUser.Name, DateTime.Today),"Created",
this.workflowProperties.OriginatorUser.ID);
}

That takes care of logging. Next, we want to move the ApplicantEditing state. Add a Sharepoint SetState activity and set the state field to be ApplicantEditing. We are done with the InitializeLeaveState!

There should be a line drawn from InitializeLeaveState to ApplicantEditing on the main canvas of the workflow. This line is the result of adding the SetState activity. Now, the applicant can edit his task and eventually submit it. So, first add and event driven activity to the ApplicantEditing state. Call it InitiatorUpdating. To the InitiatorUpdating activity add a on task changed activity which will listen for any changes made to the task. So our state will re-activate after a change occurs in some task. Which task? Well, we want to the listen for the initiator task changing, so configure the ontaskchanged activity so that the correlation token is the same as the initial create task activity and the TaskId is bound to the applicantTaskID. Bind the AfterProperties field to a new field called onApplicantTaskEdited_AfterProperties. Now the activity will listen for any task changes to our initial task.

What to do when we catch the changed event? Goto the code behind and add a variable
public bool isApplicantDone = false;
Next add a static string
private static string CompletedTaskStatus = "Completed";
Now we need our event listener method.

private void onApplicantTaskEdited_Invoked(object sender, ExternalDataEventArgs e)
{
isApplicantDone = (onApplicantTaskEdited_AfterProperties.ExtendedProperties[this.workflowProperties.TaskList.Fields["Status"].Id].ToString() == CompletedTaskStatus);
}
So, if the status of the task is set to completed, we isApplicantDone will be set to true.

Go back to the canvas, open up InitiatorUpdating again.
Set onApplicantTaskEdited MethodInvoking property to onApplicantTaskEdited_Invoked.
Add an IfElse activity just beneath onApplicantTaskEdited. Rename the first branch to ifApplicantIsDone and the second branch to ifApplicantNotDone.

Set the Condition of ifApplicantIsDone to a Declarative Rules Condition and add a declarative rule condition called ApplicantDone such that: this.isApplicantDone == True

The second branch is the default branch, so it doesn't need a condition, much like the else of a normal if statement. If the applicant is not done, we want to remain in our current state, so to the ifApplicantNotDone branch, add a SetState activity and set the TargetStateName to ApplicantEditing.

The first branch of the IfElse activity, ifApplicantIsDone, is much more interesting. If our leave applicant is ready to submit his leave application, we must create a task for his line manager to approve the leave, log the submission and move to the LineManagerApproval state. But what if the task for the line manager has already been created? As in: Applicant submits task - Line manager declines, Applicant resubmits. We don't want to recreate the task! So in that case, we must update the already created task.

So in ifApplicantIsDone, add an IfElse activity. Rename the first branch to ifApproverTaskCreated and the second branch to ifApproverTaskNotCreated. Set ifApproverTaskCreated condition to Code Condition and create the following method for the condition:


private void isLineManagerTaskCreated(object sender, ConditionalEventArgs e)
{
if (lineMangerTaskID != default(System.Guid))
e.Result = true;
else
e.Result = false;
}

So, if the task has been created, we want to update the task. Add an UpdateTask activity called updateLineManagerTask to the ifApproverTaskCreated branch. Add a CreateTask activity called createLineManagerTask to the ifApproverTaskNotCreated branch.
First set up createLineManagerTask as follows:
Correlation Token: Create a new correlation token called LineManagerTaskToken
TaskId: New field called lineManagerTaskID
TaskProperties: New Field called createLineManagerTask_TaskProperties
Set up updateLineManagerTask to use the same fields. Now, go to the code behind and add the following methods:

private void createLineMangerTask_MethodInvoking(object sender, EventArgs e)
{
lineMangerTaskID = Guid.NewGuid();
setLineManagerTask();
}
private void updateLineManagerTask_MethodInvoking(object sender, EventArgs e)
{
setLineManagerTask();
}
private void setLineManagerTask()
{
hasLineManagerApproved = false;
hasLineManagerApproved = false;
createLineMangerTask_TaskProperties.Title = string.Format("Leave Approval - {0}",
this.workflowProperties.Item.Title);
createLineMangerTask_TaskProperties.AssignedTo = getUser(this.workflowProperties.Item["Line Manager"].ToString()).LoginName;
createLineMangerTask_TaskProperties.Description = string.Format("A leave request by {0} has been created.
Leave start date: {1}
Leave end date: {2}
Please action the task as soon as possible.",
this.workflowProperties.OriginatorUser.Name, this.workflowProperties.Item["Start Date"].ToString(), this.workflowProperties.Item["End Date"].ToString());
setHistory(string.Format("Leave application submitted for line manager approval on {0}",
DateTime.Today),
"Line Manager Approval",
this.workflowProperties.OriginatorUser.ID);
}



private SPUser getUser(string fieldValue)
{
if (fieldValue.Contains(";"))
{
int userID = -1;
int.TryParse(fieldValue.Substring(0, fieldValue.IndexOf(";")), out userID);
if (userID != -1)
{
return this.workflowProperties.Web.AllUsers.GetByID(userID);
}
}
return null;
}

Set updateLineMangaerTask MethodInvoking to updateLineManagerTask_MethodInvoking and createLineManagerTask MethodInvoking to createLineMangerTask_MethodInvoking. That should take care of our task.
Next add a LogToHistoryListActivity, bind it to the history description, outcome and user id fields perviously created.
Finally add a SetState activity to take us to the LineManagerApproval state.

One final step remains before looking at approval and declining. Exception handling. Each state must have it's own exception handler. Right click on InitiatorUpdating event driven activity and select view fault handlers.
Add a new faultHandlerActivity called applicantEditingFaultHandler.
In this example, I'm just going to log the exception message & stack trace to the history list, but you could do whatever you like to handle the errors in some other way if you need to.
First set appplicantEditingFaultHandler's FaultType to System.Exception, so we'll catch all exceptions. Next add to LogToHistoryListActivities, one called logFault and one called logStackTrace.
Configure logFault's description to log as follows:
Activity=applicantEditingFaultHandler, Path=Fault.Message (click on the ellipses, open up applicantEditingFaultHandler, find the Fault variable, select message).
Set History Outcome to ApplicantFault.
Do the same for logStackTrace, except, select the Fault.StackTrace field.

Compile the project, unintall the previous assembly from the GAC, install the new one to the GAC, issreset (or recycle the sharepoint application pool) test:
Create a new entry to the leave applications list.
Open up the workflowstatus for the entry.
Edit the task that has been created.
Set the status to Completed and save the change.
A new task should be created assigned to the line manager you selected for your entry.
Your log list should have two entries:

9/10/2007 3:45 PM
Comment
JEDIMASTER\Administrator
Leave application created by JEDIMASTER\Administrator on 2007/09/10 12:00:00 AM
Created


9/10/2007 3:45 PM
Comment
JEDIMASTER\Administrator
Leave application submitted for line manager approval on 2007/09/10 12:00:00 AM
Line Manager Approval

If you need to debug, attach to the w3wp.exe process, make sure your Attach to field in the Attach to Process dialogue window is set to Workflow code.