Friday, December 21, 2007

Leave Workflow - The Final Trick

Unfortuantly, I've had to rebuild my machine & I lost my leave workflow example in the process, but I'm sure you have a good idea how to build a state machine workflow.

To update tasks via the API, use SPWorkflowTask.AlterTask and this is where the final trick comes in.

One final tip, there is a bug in WF SharePoint framework (note: I have NOT tried this with Service Pack 1, if SP1 does fix it, please let me know). Essentially, if there is more than one version of the workflow associated with an item, sometimes the workflow will go into an error state with the following message:

"The task is locked by another user and cannot be updated".

So SPWorkflowTask.AlterTask has this problem, how do we fix it? Easy enough, right a static method that looks something like this:


public class MyWorkflowTask
{
    public static bool AlterTask(SPListItem task, Hashtable htData, bool fSynchronous, int attempts, int millisecondsTimeout)
    {
      if ((int)task[SPBuiltInFieldId.WorkflowVersion] != 1)
      {
        SPList parentList = task.ParentList.ParentWeb.Lists[new Guid(task [SPBuiltInFieldId.WorkflowListId].ToString())];
        SPListItem parentItem = parentList.Items.GetItemById((int)task[SPBuiltInFieldId.WorkflowItemId]);
        for (int i = 0; i < attempts; i++)
        {
          SPWorkflow workflow = parentItem.Workflows[new Guid(task[SPBuiltInFieldId.WorkflowInstanceID].ToString())];
          if (!workflow.IsLocked)
          {
            task[SPBuiltInFieldId.WorkflowVersion] = 1;
            task.SystemUpdate();
            break;
          }
          if (i != attempts - 1)
            Thread.Sleep(millisecondsTimeout);
        }
      }
      return SPWorkflowTask.AlterTask(task, htData, fSynchronous);
   }
}

task.SystemUpdate() is the big override. It will override anything, so use this knowledge with care!!!!

And that's that, you should now be able to put togethere a basic state machine workflow without to much in the way of trouble.

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.

Thursday, July 26, 2007

Event Driven WF & WSS

So that technical post is finally here.

I've seen alot of posts recently about sharepoint and sequintial workflows, but precious little concerning sharepoint and event driven state machine workflows. But my current application requires a state machine workflow, so I'll share some of my experiances.

Let's use an example to illustrate. I'm developing a leave request application. After someone submits a request for leave, it must be approved by their line manager and by their current project manager and finally by the HR manager.

Any one of the approvers can refer the leave application back to the orginiator with a request to change the leave application in some way. If the originator resubmits, the workflow process must go throug the approval once again.
At any time before approval, the leave originator may cancel the request for leave.

Sound complicate? It's not. I'm not going to address the front end needs, you can use either ASPX or infopath and there are plenty of posts out there to show you how to hook up a front end to sharepoint. I'm just going to play around with sharepoint lists to provide a front end.


So, how do we approach this? Well, firstly, ensure that you've got WSS, VS 2005, WF Extensions for VS 2005 & WSS Extensions for VS 2005 installed on your dev machine (it just runs smoother that way).
Open up VS 2005. Create a new project of type State Machine Workflow Library as found under the sharepoint tab.
Call your solution LeaveApplication and your state machine library LeaveStateMachineFlow. Click OK.


You should now have a project with Workflow1.cs, feature.xml, workflow.xml & Install.bat. Rename Workflow1 to LeaveWorkflow.cs.





Our workflow has 6 states.

1. Initial State.
2. Applicant Editing.
3. Project Manager Approval.
4. Line Manager Approval.
5. HR Manager Approval.
6. Finalize.

You don't need the finalize state, but let's complete the workflow. We're going to start by developing only the initial state and the applicant editing state. So, let's get to work (no pun intended :) ).
Open up the LeaveWorkflow.cs file. You'll note that an initial state activity has already been added along with an event driven activity. We'll just change the names rather than adding new one. So change the name of the state activity to InitializeLeaveState. Open up the eventDrivenActivity1 by clicking on it. You should see something like this:




Notice the red exclamation marks. Those are errors that need be fixed before build will succeed.

If you click on the upper most exclamation mark, you'll see an error saying that the state is invalid. This is because we have changed the state name. Click on the error and the properites tab will take you to the correct entry to fix the error. Set the InitialStateName of LeaveWorkflow to InitializeLeaveState.

The exclamation mark has two errors. the correlation token has no valid owner activity and the workflow properties have no valid name. This is because we changed the workflow name. Set the correlation token owner activity to LeaveWorkflow and the workflow properites name to LeaveWorkflow.
The exclamation marks should dissapear and you should be able to build the solution.


Our next step is creating the six states. This is really simple. In your toolbox, there should be a state activity. Drag it onto the design canvas. Rename the state activities to ApplicantEditing, PMApproval, LineManagerApproval, HRApproval and LeaveFinalize. Right click on the finalize activity and mark it as the completed state. You should have something like this:


We'll ignore the other states for now and focus on creating the leave application. So, open up the eventDrivenActivity1 again. On your toolbox, check if you have the CreateTask activity. If you don't, right click on the toolbox, select Choose Items and select the items from the microsoft.sharepoint.WorkflowActions namespace.

Ok, now that the admin is done, drag the CreateTask activity from the toolbox to below your onWorkflowActivated1 event (this event is required by all sharepoint workflows). Rename CreateTask1 to createApplicantTask. Set the correlation token to ApplicantTaskToken and the owner activity as the workflow (so that you can have the task spread over mulitple states). Set the task ID to be a new field named applicantTaskID (Activity=LeaveWorkflow, Path=applicantTaskID) and the task properties to be a new field named createApplicantTask_TaskProperties1 (Activity=LeaveWorkflow, Path=createApplicantTask_TaskProperties1). So far so good. Now we come to the code. In your LeaveWorkflow.cs file, adding the following methods:

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";
}
private void onWorkflowActivated1_Invoked(object sender, ExternalDataEventArgs e)
{
}

Link these methods to the handlers of task creation (creatApplicantTask) and the workflow activated (onWorkflowActivated1) respectivly. This will create a task for the leave applicant to complete his leave application after creation. Let's see it in action shall we? First we need to edit the workflow.xml and feature.xml files so that the feature for the workflow is correctly installed. By the same token, you must edit the Install.bat file. Your files should look something like this: (remember, your assembly needs a strong name) (Don't forget snippets for the workflow.xml and the feature.xml, they should already be installed):

workflow.xml:





Name="LeaveWorkflow"
Description="This workflow handles leave approval"
Id="07235F44-DCF1-4a41-BBEC-77907939678D"
CodeBesideClass="LeaveStateMachineFlow.LeaveWorkflow"
CodeBesideAssembly="LeaveStateMachineFlow, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1851b6189ce8aae4">



feature.xml:




Title="LeaveStateMachineFlow"
Description="This feature is a workflow that approves leave"
Version="12.0.0.0"
Scope="Site"
xmlns="http://schemas.microsoft.com/sharepoint/">






Install.bat:

:: Before running this file, sign the assembly in Project properties
::
:: To customize this file, find and replace
:: a) "MyFeature" with your own feature names
:: b) "feature.xml" with the name of your feature.xml file
:: c) "workflow.xml" with the name of your workflow.xml file
:: d) "http://localhost" with the name of the site you wish to publish to
echo Copying the feature...
rd /s /q "%CommonProgramFiles%\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\LeaveStateMachineFlow"
mkdir "%CommonProgramFiles%\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\LeaveStateMachineFlow"
copy /Y feature.xml "%CommonProgramFiles%\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\LeaveStateMachineFlow\"
copy /Y workflow.xml "%CommonProgramFiles%\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\LeaveStateMachineFlow\"
xcopy /s /Y *.aspx "%programfiles%\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS"
echo Adding assemblies to the GAC...
"%programfiles%\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe" -uf LeaveStateMachineFlow
"%programfiles%\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe" -if bin\Debug\LeaveStateMachineFlow.dll
:: Note: 64-bit alternative to lines above; uncomment these to install on a 64-bit machine
::"%programfiles% (x86)\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe" -uf LeaveStateMachineFlow
::"%programfiles% (x86)\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe" -if bin\Debug\LeaveStateMachineFlow.dll

echo Activating the feature...
pushd %programfiles%\common files\microsoft shared\web server extensions\12\bin
::Note: Uncomment these lines if you've modified your deployment xml files or IP forms
::stsadm -o deactivatefeature -filename LeaveStateMachineFlow\feature.xml -url http://localhost
::stsadm -o uninstallfeature -filename LeaveStateMachineFlow\feature.xml
stsadm -o installfeature -filename LeaveStateMachineFlow\feature.xml -force
stsadm -o activatefeature -filename LeaveStateMachineFlow\feature.xml -url http://localhost

echo Doing an iisreset...
popd
iisreset

Build the project and run the install.bat file.

Now open up your sharepoint site (the same one you installed the feature to). Create a list with a title, from date and to date (we'll add more stuff later). Go to the list settings, go to workflows, add the leave workflow and set it to start when a new item is added. Add a new item to the list and voila a task is added to the initiator's task list. (It's a good idea to create a new task list for the workflow, just to keep things neat).

To see your tasks and so forth, click on the down arrow of the item, click on workflows, click on your workflow and the task is displayed.

Next post: How to move from one state to the next (or how to go to the approvers and back).

Friday, June 22, 2007

Cape Town First Week

Well, the first week in Cape Town has come and gone and I haven't drowned under meetings, so all is well.

Yeah, I know, technical post. Not going to happen today, still snowed under for all that most of my anaylis is done. Now comes writing the functional spec. Blah.

On the other hand, the client is great :). And it's a 15 minute drive to work...ah the luxury of the Cape...15 minutes at 7:45 I might add. I'm actually working 8 to 5, who'd've thunk it?

Flying back to Guateng today, back on monday in time for a 9:15 meeting, pretty much rush off the plane and into the meeting. Boy next week is but PACKED.

And somewhere I still have to finish my module for ICON (Unwilling Heroes). Playtest is next weekend!!!!!

Friday, June 8, 2007

TTT touchdown!!!!!!!!!!

And it's official, I'm not a business!!! Hehe.

My TTT stuff was good for me, got honour in death play set and Kharma in Death playset (after some trading with JP).

Hoping to get my third assign blame this saturday. Hmmm, assign blame...kill a courtier and it's YOUR fault, hehehehehe

On the work front, matters have gotten better. We're finally semi integrated and just about ready for End to End testing. Just as well, because my cape town jaunt starts 18 June. Yep, I'll be commuting 3000 km each week *snickers*

Kushiel's Justice release date is almost upon us...I can hardly wait.

I'll post a workflow post soon, almost done with Programming Windows Workflow Foundation by K. Scott Allen, which is a great book if anyone is interested.

Friday, June 1, 2007

So still waiting for my stupid TTT order

Soooo, still waiting for TNT to get their finger out of their *** and actually clear my order. My TTT cards have been sitting in Customs for the past week. Nice eh?

In the mean time, My Market have been jerking me around and as a due consequence, my current project has just missed UAT deadline. Charming.

I've only had 4 hours of sleep because yesterday was a 16 hour work day (yeah, that does include travel 1 hour each way) so I'm feeling grumpier than usual.

On the upside, tomorrow is roleplaying, I've got an L5R game that's looking to be all kinds of kick ass and Sunday we're having a Drums of War Suicide tourney, YAY!

I'm finally Level 9 in Kingdom of Loathing, trying to get to the Mysterious Island to get an abridged dictionary to get a bridge to cross to the Orc territory for my level 9 council quest.

Well, that's all I have time for now, maybe more later.