Friday, December 17, 2010

How to get a list of running workflows in a document library

Getting a list of workflows that are running on a list library or document library is pretty straightforward. Essentially, all we need to do is get a handle on the site, get a handle on the list, loop through the list's items, then for each item, loop through that item's workflows collection, then for each workflow, look at its InternalState and if its set to "Running", its, uhh, running. You could remove the IF statement from the code below and display all workflows for the current item to see whats Running, Completed, Locked, or Canceled. Once you have a handle on the workflow, you could then do whatever else you might need to do with it. What if you wanted to be emailed every time a workflow got locked? In my case, I have a document library with items that may have workflows running on them. As these workflows have due dates, I need to look at all running workflows in the list, and if today is past their due date, I want to stop these workflows. I will then put this code in a SharePoint timer job that will execute daily. In fact, I will post an article on this in the coming days or weeks after I actually do it! This is using VS2008/MOSS2007 and the code is in VB, but it would be extremely easy to rewrite in C#. I would imagine this would work in SharePoint 2010, but I haven't tested yet. If anyone wants to copy/paste it in 2010 and test it for the rest of us, we'd love a confirmation!

For simplicity's sake, we will create a console application that will access our SharePoint site and loop through the document library's workflows to find the running ones.

1. in Visual Studio, click File -> New Project -> Windows -> Console Application

2. it defaults to ConsoleApplication1, leave it and click OK

3. in Solution Explorer, right-click ConsoleApplication1, click Add Reference

4. browse to C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\ISAPI\ and select Microsoft.SharePoint.dll

5. above Module Module1, add:
Import Microsoft.SharePoint

6. inside Sub Main(), add:
Dim site As New SPSite("http://my_sp_site_here")   'get site
Dim list As SPList = site.RootWeb.Lists("my_document_library") 'get document library

For Each item As SPListItem In list.Items 'for each item in library
For Each wrkflw As Workflow.SPWorkflow In item.Workflows 'for current item's list of workflows
If wrkflw.InternalState.ToString = "Running" Then 'if current workflow is running
Console.WriteLine(wrkflw.InstanceId.ToString) 'display it's instance id
End If
Next
Next

Console.WriteLine()
Console.Write("Press ENTER to continue")
Console.ReadLine()

Wednesday, June 9, 2010

Update 12 Hive Through A Feature

As we got our SharePoint environment up and running, we needed to have the ability to make changes to our 12-Hive. We also wanted to have the flexibility to undo the changes and restore the 12-Hive to the way it was originally.

This post discusses the scenario where you need to make changes to files that already exist in the 12-Hive. I call this solution HiveUpdates. If you have a need to add new files to the 12-Hive that do not already exist, such as new images or layouts, check out my other article Add New Files To 12-Hive Through A Solution. I call that solution HiveAdditions. We decided it would be best for us to separate these two types of Hive changes into two separate solutions.

This solution, HiveUpdates, allows us to modify existing Hive files. This solution contains a feature that, when activated, makes a backup copy of the original file, then overwrites the original with the new file. When the feature is deactivated, it copies the backup file back to the original filename.

The method I used is based on the article 12 Hive System-File Changes: One Feature to rule them all!. The above post has a routine that is called when the feature is activated or when its deactivated. The code that fires simply "swaps" newly changed 12-hive files with their corresponding original 12-hive files. This happens both when the feature is activated or deactivated. While the "swap" code seems like a good idea on the surface (the same code that deploys the changes will swap files back to undeploy them), I found in practice this seemed risky and even prone to error if something went wrong on the feature activate or feature deactivate. I sometimes got the files out of sync and would lose the original 12-Hive versions.

Therefore, I modified the code to first make a backup copy of a 12-Hive file I want to update, then overwrite the original 12-hive file with my changed version (this is an activate). On a deactivate, it overwrites my changed version with the original's backup file. The original's backup file always stays there as a backup, which makes me feel very safe. And I like feeling safe. I like my job and want to keep it.

1. In Visual Studio 2008, File -> New -> Project

2. Under Visual C#, pick SharePoint, then Empty

3. For Name, type: HiveUpdates

4. For Location, type: D:\development

5. Click OK

6. Select Full Trust (Deploy to GAC) and click OK

7. In WSP View (View -> Other Windows -> WSP View), click "Create new feature"

8. Change Feature Scope to Farm

9. Check Add feature receiver

10. click OK

11. In Solution Explorer, rename Feature1 to HiveUpdates

12. Rename Feature1Receiver.cs to HiveUpdatesReceiver.cs

13. Double-click HiveUpdatesReceiver.cs to open it

14. Rename class from FeatureReceiver1 to HiveUpdatesReceiver

15. Below that, you'll see the constructor "public FeatureReceiver1". Rename constructor from FeatureReceiver1 to HiveUpdatesReceiver.

16. In WSP View, hit refresh and notice FeatureReceiver1 is now HiveUpdatesReceiver

17. Rename Feature1 folder to HiveUpdates

Make the following code modifications/additions to HiveUpdatesReceiver.cs:

18. At the top of the code, add the following using statements:
using Microsoft.SharePoint.Administration;
using System.IO;

19. Above the HiveUpdatesReceiver constructor, add the following:
private DirectoryInfo localDirectory = new DirectoryInfo(@"C:\");

20. In the FeatureActivated method, add:
DeployToHive(properties);

21. In the FeatureDeactivating method, add:
RemoveFromHive(properties);

22. Below the FeatureUninstalling method, add the following 4 methods (DeployToHive, RemoveFromHive, GetHiveFile, GetLocalDirectory):
private void DeployToHive(SPFeatureReceiverProperties properties)
{
//loop through each server in the farm
foreach (SPServer server in properties.Definition.Farm.Servers)
{
//if the current server is a WebFrontEnd server
if (server.Role == SPServerRole.WebFrontEnd || server.Role == SPServerRole.Application
|| server.Role == SPServerRole.SingleServer)
{
string localFilePath = GetLocalDirectory(properties.Definition.RootDirectory,
server.Name);
localDirectory = new DirectoryInfo(localFilePath);

foreach (FileInfo fileinfo in localDirectory.GetFiles("*",
SearchOption.AllDirectories))
{
//if current file in feature exists in 12 hive on current server
if (File.Exists(GetHiveFile(fileinfo.FullName, server.Name))
&& (!fileinfo.FullName.Contains(".hivebackup")))
// backup file from 12 hive to feature
File.Copy(GetHiveFile(fileinfo.FullName, server.Name),
fileinfo.FullName + ".hivebackup", true);

//if current file in feature exists
if (File.Exists(fileinfo.FullName) &&
(!fileinfo.FullName.Contains(".hivebackup")))
// copy local file to the 12 hive
File.Copy(fileinfo.FullName, GetHiveFile(fileinfo.FullName, server.Name),
true);
}
}
}
}



private void RemoveFromHive(SPFeatureReceiverProperties properties)
{
//loop through each server in the farm
foreach (SPServer server in properties.Definition.Farm.Servers)
{
//if the current server is a WebFrontEnd server
if (server.Role == SPServerRole.WebFrontEnd
|| server.Role == SPServerRole.Application
|| server.Role == SPServerRole.SingleServer)
{
string localFilePath = GetLocalDirectory(properties.Definition.RootDirectory,
server.Name);
localDirectory = new DirectoryInfo(localFilePath);

foreach (FileInfo fileinfo in localDirectory.GetFiles("*",
SearchOption.AllDirectories))
{
//if current backup file in feature exists in 12 hive
if (File.Exists(fileinfo.FullName + ".hivebackup"))
// copy hivebackup file back to the 12 hive
File.Copy(fileinfo.FullName + ".hivebackup",
GetHiveFile(fileinfo.FullName, server.Name), true);
}
}
}
}



string GetHiveFile(string fileName, string serverName)
{
if (fileName.StartsWith(localDirectory.FullName))
{ // file is local
string p = @"\\" + serverName
+ @"\C$\Program Files\Common Files\Microsoft Shared\web server extensions\12"
+ fileName.Remove(0, localDirectory.FullName.Length);
return p;
}
throw new Exception("Filepath doesn't point to correct local hive!");
}



string GetLocalDirectory(string filePath, string serverName)
{
return @"\\" + serverName + @"\" + @filePath.Replace(":", "$") + @"\12";

//write a class that writes exceptions to the event log
throw new Exception("Filepath doesn't point to correct local hive!");
}

Now we need to create the appropriate 12-Hive file structure for the feature in Solution Explorer. Any 12-Hive files we want to update will have their newer versions placed in the file structure we are about to create.

23. In Solution Explorer, right-click project HiveUpdates, then click Add -> New Item... -> SharePoint -> Template.

24. You will notice a folder called Templates was created in the Solution Explorer. Delete the default file TemplateFile1.txt.

25. Under Templates, create a folder called FEATURES.

26. Under FEATURES, create a folder called HiveUpdates.

So far, we have created 12/TEMPLATE/FEATURES/HiveUpdates. This is the location in the 12-Hive where that feature's feature.xml gets deployed to. But now, we must create another 12-Hive structure under this folder. This is where our newer files will be deployed to when the solution is initially deployed. When the feature is activated, this is where the above code will store backups of the original files from the 12-Hive, as well as the source from which to overwrite the 12-Hive files with.

27. Under HiveUpdates, create a folder called 12.

28. Under 12, create a folder called TEMPLATE.

29. Under TEMPLATE, create a folder called LAYOUTS.

30. Copy application.master in here. Either make some small change to it or at the very least change the date on the file so its different from what it is in the 12-Hive. Be sure to right-click application.master and click Include In Project if it is not already included.

31. In WSP View, double-click feature.xml.

32. Change the value of Title from Feature1 to HiveUpdates.

33. Verify Scope is set to Farm.

33. After the title or scope, add: ActivateOnDefault="false"

34. Verify your manifest was created correctly. In WSP View, Under HiveUpdates, you should see manifest.xml. Double-click manifest.xml and verify your file path for application.master is FEATURES\HiveUpdates\12\TEMPLATE\LAYOUTS.

35. click Build -> Rebuild Solution

36. click Build -> Package Solution

The solution package that you just created in the above step, HiveUpdates.wsp, was created in your project's bin\debug folder.

To deploy the solution:

37. create folder d:\deploy

38. copy HiveUpdates.wsp from project's bin\debug folder to d:\deploy

39. In d:\deploy, create two batch files, add_hiveupdates.bat and remove_hiveupdates.bat.

add_hiveupdates.bat:
c:

cd\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\BIN

stsadm -o addsolution -filename d:\deploy\HiveUpdates\HiveUpdates.wsp

stsadm -o deploysolution -name HiveUpdates.wsp -immediate -allowGacDeployment

pause

stsadm -o activatefeature -name HiveUpdates

d:

remove_hiveupdates.bat:
c:

cd\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\BIN

stsadm -o deactivatefeature -name HiveUpdates

stsadm -o retractsolution -name HiveUpdates.wsp -immediate

pause

stsadm -o deletesolution -name HiveUpdates.wsp

d:

40. in d:\deploy, type add_hiveupdates.wsp and hit enter

41. verify solution was deployed in Central Administration -> Operations -> Solution Management

42. verify files were added to 12-Hive!

Saturday, June 5, 2010

Add New Files To 12-Hive Through A SharePoint Solution

As we got our SharePoint environment up and running, we needed to have the ability to make changes to our 12-Hive. We also wanted to have the flexibility to undo the changes and restore the 12-Hive to the way it was originally.

This post discusses the scenario where you need to add new files to the 12-Hive that do not already exist, such as new images or layouts. I call this solution HiveAdditions. If you have a need to update files that already exist in the 12-Hive, check out my other article Update 12 Hive Through A Feature. I call that solution HiveUpdates. We decided it would be best for us to separate these two types of Hive changes into two separate solutions.

This solution, HiveAdditions, simply allows us to add files into the 12-Hive upon a deployment. When we retract this solution, it simply removes all those files. We liked this approach because it allows us to have this solution as the one place for all new 12-Hive files. If we needed to add new files, we retract this solution, add the new files to the solution, then deploy it again.


1. In Visual Studio 2008, File -> New -> Project

2. Under Visual C#, pick SharePoint, then Empty

3. For Name, type: HiveAdditions

4. For Location, type: D:\development

5. Click OK

6. Select Full Trust (Deploy to GAC) and click OK

7. In Solution Explorer, right-click project HiveAdditions, then click Add -> New Items... -> SharePoint -> Root File

8. Leave Name alone and click Add

9. Under RootFiles (which now appears in Solution Explorer), delete RootFile1

Under RootFiles, we must mirror the 12-Hive locations where we want to add the new files.

Let's add image files under TEMPLATE/IMAGES.

10. Right-click RootFiles, click Add -> New Folder and name it TEMPLATE.

11. Right-click TEMPLATE, click Add -> New Folder and name it IMAGES.

12. Drag and drop your images into IMAGES folder. Be sure to right-click newly added files and click Include In Project if they are not already included.

13. Verify your manifest was created correctly. Click View -> Other Windows -> WSP View. Under HiveAdditions, you should see manifest.xml, then under that, RootFiles -> TEMPLATE -> IMAGES followed by the files you added. Double-click manifest.xml and verify your file paths are under TEMPLATE\IMAGES\file1.jpg, etc.

14. click Build -> Rebuild Solution

15. click Build -> Package Solution

The solution package that you just created in the above step, HiveAdditions.wsp, was created in your project's bin\debug folder.

To deploy the solution:

16. create folder d:\deploy

17. copy HiveAdditions.wsp from project's bin\debug folder to d:\deploy

18. In d:\deploy, create two batch files, add_hiveadditions.bat and remove_hiveadditions.bat.

add_hiveadditions.bat:
c:

cd\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\BIN

stsadm -o addsolution -filename d:\deploy\HiveAdditions\HiveAdditions.wsp

stsadm -o deploysolution -name HiveAdditions.wsp -immediate -allowGacDeployment

d:
remove_hiveadditions.bat:
c:

cd\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\BIN

stsadm -o retractsolution -name HiveAdditions.wsp -immediate

pause

stsadm -o deletesolution -name HiveAdditions.wsp

d:
19. in d:\deploy, type add_hiveadditions.wsp and hit enter

20. verify solution was deployed in Central Administration -> Operations -> Solution Management

21. verify files were added to 12-Hive!

Tuesday, August 25, 2009

How to deploy SharePoint list templates

Ok, so after I had packaged up three web parts into their own features in a solution (wsp) file, I had one more issue to address: those three web parts were dependent on lists created from two list templates. So I needed to also package up those list templates as features in the solution file.

After finding countless pages that explain how to do it the hard way (and there are many hard ways), I finally realized one of my favorite sites showed me the easiest, best way, by far. Jeremy Thake of SharePointDevWiki created a video showing how its done. Jeremy is the man downunder! Excuse me, the bloke downunder.

In addition, I also needed to package up two list templates into the solution package, not just one as is done in the video. As with most stuff SharePoint, once you know how to do something, its extremely easy to do.

At a high level, here is what we will do:
  1. Create some lists in your SharePoint site.

  2. Install SPSource from CodePlex and use it to reverse engineer the lists you created on your SharePoint site back into list templates, storing the necessary files that make up the list templates into a Visual Studio project.

  3. Use BuildWSP to build your WSP!
Note: in this example, I will create a new Visual Studio project. However, I want to add the reverse-engineered list templates into my existing project with my web parts. To do this, all I have to do is copy the contents of my FEATURES folder from my new Visual Studio project (where I reverse engineered my list templates) into my existing Visual Studio project's FEATURES folder (where my web parts already are). When I run BuildWSP, it will just simply see the new list template features and add those to the .wsp. I will not show the copy in the steps below because I don't know if you need to add your list templates to a pre-existing project, but like I said, just copy the list templates from the FEATURES folder if you want to do this.

Ok, so now for a little bit of detail.
  1. In your SharePoint site, create two lists. I will call one Contact List Template and the other Sent Emails Template in this example. Add some columns to them.


  2. Download SPSource from CodePlex. I put spsource.exe in a folder called d:\deploy, but just put it somewhere easy to get to.


  3. If you haven't already downloaded and installed WSPBuilder, do it. WSPBuilder will be added under the Tools menu in Visual Studio.


  4. Create the Visual Studio project that will be the destination for the reverse-engineered list templates.
    1. In Visual Studio, click File -> New -> Project -> WSPBuilder -> WSPBuilder Project

    2. Name it MyListTemplates and click OK.

  5. Add a feature for each list template you wish to reverse engineer. (Note: Trying to add both list templates to the same feature has proven to be problematic. If you add them to the same feature, when you deploy the feature and create lists from the list templates, all the lists you create will only use the definition from the first list template that was deployed, even if you create a list based on the second list template. Therefore, create one feature for each list template!)
    1. In the Solution Explorer, right-click the project, then click Add -> New Item -> WSPBuilder -> Blank Feature

    2. Name it ContactListTemplate and click Add.

    3. In the Feature Settings dialog, change the Scope to Site and click OK.

    4. Do a. through c. again, but this time name the feature SentEmailsTemplate.

    5. Notice both features have been added under /12/TEMPLATE/FEATURES/ in your Solution Explorer. Each contains an elements.xml and feature.xml.

  6. Create a .spsource file for each feature. These files tell SPSource what lists to reverse engineer into list templates.
    1. In Solution Explorer, right-click the ContactListTemplate feature, then click Add -> New Item -> Common -> XML File, name it contactlist.spsource and click Add.
    2. In Solution Explorer, right-click the SentEmailsTemplate feature, then click Add -> New Item -> Common -> XML File, name it contactlist.spsource and click Add.

    3. Copy the contents of elements.xml into both of these .spsource files.

    4. In contactlist.spsource, add the following inside the Elements tag:
      <ListTemplate Name="Contact List Template" />
    5. In sentemails.spsource, add the following inside the Elements tag:
      <ListTemplate Name="Sent Emails Template" />
  7. In your project's folder, create a text file called SPSource.cmd with these contents:
    @ECHO OFF

    SET SPSOURCE="d:\deploy\SPSource.exe"
    SET DEVSITEURL="http://sharepoint2007:1001/marketing"

    %SPSOURCE% -designsite %DEVSITEURL%

    PAUSE
    Be sure to set DEVSITEURL to your SharePoint site where you created the lists back in step 1.


  8. Double-click SPSource.cmd so it runs. If you get an error message saying something like "Method not found...SPOpenBinaryOptions...), you need to install Windows SharePoint Services 3.0 Service Pack 1 (SP1). This is because SPSource requires methods that are in WSS 3.0 SP1.

  9. In Solution Explorer, click Show All Files. Under the two features, you'll see a new folder under each with the same name. Right-click the new folders and click Include In Project.


  10. Build the project.


  11. Create the wsp solution file by clicking Tools -> WSP Builder -> Build WSP.


  12. You can now deploy MyListTemplates.wsp with stsadm as you normally deploy wsps! Use addsolution, deploysolution, and activatefeature to deploy however you want to deploy it (ie using GAC or CAS, etc).

Thursday, August 20, 2009

How to create a SharePoint State Machine Workflow: Part 6 - Add task notification emails

Here in Part 6, we'll add email notification events so when a task is assigned, approved, or rejected, the appropriate emails are sent to the proper people. This part assumes you have completed part 4.

1. go back to the design surface and double-click PeerReviewerApprovalInitialization to drill down into it

2. drag and drop a SendEmail activity below hlogPeerReviewerApprovalTaskCreated
and set the following properties:
a. Name: sendPeerReviewerApprovalTaskEmail
b. Body: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
c. CC: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
d. CorrelationToken: choose workflowToken from dropdownlist
e. Subject: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
f. To: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
g. MethodInvoking: type PeerReviewerApprovalTaskEmail and hit enter
h. You will be taken to the code stub for PeerReviewerApprovalTaskEmail. Copy and paste the following code:

'get PeerReviewer's email address
Dim PeerReviewerObject As SPUser = GetUserObject(PeerReviewer)
Dim PeerReviewerEmail = PeerReviewerObject.Email

'get CC's email address
Dim CCObject As SPUser = GetUserObject(CC)
Dim CCEmail = CCObject.Email


sendPeerReviewerApprovalTaskEmail_To = PeerReviewerEmail
sendPeerReviewerApprovalTaskEmail_CC = CCEmail
sendPeerReviewerApprovalTaskEmail_Subject = "Approval of " & workflowProperties.Item.File.Name & " has been assigned to you."
sendPeerReviewerApprovalTaskEmail_Body = "<span style='font-family: arial; font-size: medium'>" & _
"Task assigned by " & workflowProperties.OriginatorUser.Name & " on " & DateTime.Now & _
"</span>" & _
"<br>" & _
"<span style='font-family: arial; font-size: x-small'>" & _
"Due by " & DateTime.Today & _
"<br><br>" & _
"Instructions: <br>" & Instructions & _
"<br><br>" & _
"Please approve " & workflowProperties.Item.File.Name & _
"<br><br>" & _
"To complete this task:" & _
"<br><br>" & _
"    1. Review " & "<a href='" & workflowProperties.SiteUrl & "\" & workflowProperties.ItemUrl & "'>" & workflowProperties.Item.File.Name & "</a>" & "." & _
"<br>" & _
"    2. Use the <b>Edit this task</b> button to Approve/Reject the document." & _
"<br><br>" & _
"To view this workflow's history, click " & "<a href=" & workflowProperties.SiteUrl & "/_layouts/WrkStat.aspx?List=" & workflowProperties.ListId.ToString & "&WorkflowInstanceID=" & workflowProperties.WorkflowId.ToString & ">here</a>" & "." & _
"</span>"

'if this is the first time we have gone to the peer reviewer state, do not show comments
' in email
If ArrivedFromInitiatorState = True Then
'get the comments entered by the initiator
Dim Comments As String = onInitiatorApprovalChanged.AfterProperties.ExtendedProperties("txtComments").ToString

sendPeerReviewerApprovalTaskEmail_Body = sendPeerReviewerApprovalTaskEmail_Body & _
"<span style='font-family: arial; font-size: x-small'>" & _
"<br><br>" & _
"<font color=red>Comments:</font> <br>" & Comments & _
"</span>"
End If

3. go back to the design surface and double-click InitiatorApprovalInitialization to drill down into it

4. drag and drop a SendEmail activity below hlogPeerReviewerApprovalTaskCreated
and set the following properties:
a. Name: sendInitiatorApprovalTaskEmail
b. Body: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
c. CC: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
d. CorrelationToken: choose workflowToken from dropdownlist
e. Subject: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
f. To: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
g. MethodInvoking: type InitiatorApprovalTaskEmail and hit enter
h. You will be taken to the code stub for PeerReviewerApprovalTaskEmail. Copy and paste the following code:

'get Initiator's email address
Dim InitiatorObject As SPUser = GetUserObject(workflowProperties.Originator)
Dim InitiatorEmail = InitiatorObject.Email

'get CC's email address
Dim CCObject As SPUser = GetUserObject(CC)
Dim CCEmail = CCObject.Email

'get PeerReviewer's name
Dim PeerReviewerObject As SPUser = GetUserObject(PeerReviewer)
Dim PeerReviewerName = PeerReviewerObject.Name

'get the comments entered by the peer reviewer
Dim Comments As String = onPeerReviewerApprovalChanged.AfterProperties.ExtendedProperties("txtComments").ToString

sendInitiatorApprovalTaskEmail_To = InitiatorEmail
sendInitiatorApprovalTaskEmail_CC = CCEmail
sendInitiatorApprovalTaskEmail_Subject = "Approval of " & workflowProperties.Item.File.Name & " has been assigned to you."
sendInitiatorApprovalTaskEmail_Body = "<span style='font-family: arial; font-size: medium'>" & _
"Task assigned by " & PeerReviewerName & " on " & DateTime.Now & _
"</span>" & _
"<br>" & _
"<span style='font-family: arial; font-size: x-small'>" & _
"Due by " & DateTime.Today & _
"<br><br>" & _
"Instructions: <br>" & Instructions & _
"<br><br>" & _
"Please approve " & workflowProperties.Item.File.Name & _
"<br><br>" & _
"To complete this task:" & _
"<br><br>" & _
"    1. Review " & "<a href='" & workflowProperties.SiteUrl & "\" & workflowProperties.ItemUrl & "'>" & workflowProperties.Item.File.Name & "</a>" & "." & _
"<br>" & _
"    2. Use the <b>Edit this task</b> button to Approve/Reject the document." & _
"<br><br>" & _
"To view this workflow's history, click " & "<a href=" & workflowProperties.SiteUrl & "/_layouts/WrkStat.aspx?List=" & workflowProperties.ListId.ToString & "&WorkflowInstanceID=" & workflowProperties.WorkflowId.ToString & ">here</a>" & "." & _
"<br><br>" & _
"<font color=red>Comments:</font> <br>" & Comments & _
"</span>"

5. go back to the design surface and double-click PeerReviewerApprovalActivities to drill down into it

6. drag and drop a SendEmail activity between hlogPeerReviewerApproved and hlogWorkflowCompleted
and set the following properties:
a. Name: sendPeerReviewerApprovedEmail
b. Body: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
c. CC: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
d. CorrelationToken: choose workflowToken from dropdownlist
e. Subject: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
f. To: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
g. MethodInvoking: type InitiatorApprovalTaskEmail and hit enter
h. You will be taken to the code stub for PeerReviewerApprovedEmail. Copy and paste the following code:

'get Initiator's email address
Dim InitiatorObject As SPUser = GetUserObject(workflowProperties.Originator)
Dim InitiatorEmail = InitiatorObject.Email

'get CC's email address
Dim CCObject As SPUser = GetUserObject(CC)
Dim CCEmail = CCObject.Email

'get PeerReviewer's name
Dim PeerReviewerObject As SPUser = GetUserObject(PeerReviewer)
Dim PeerReviewerName = PeerReviewerObject.Name

sendPeerReviewerApprovedEmail_To = InitiatorEmail
sendPeerReviewerApprovedEmail_CC = CCEmail
sendPeerReviewerApprovedEmail_Subject = workflowProperties.Item.File.Name & " has been approved by " & PeerReviewerName & ". Peer Review Complete."
sendPeerReviewerApprovedEmail_Body = "<span style='font-family: arial; font-size: medium'>" & _
workflowProperties.Item.File.Name & " was approved by " & PeerReviewerName & " on " & DateTime.Now & _
"<br><br>" & _
"Peer Review Complete" & _
"<br><br>" & _
"<span style='font-family: arial; font-size: x-small'>" & _
"To view this workflow's history, click " & "<a href=" & workflowProperties.SiteUrl & "/_layouts/WrkStat.aspx?List=" & workflowProperties.ListId.ToString & "&WorkflowInstanceID=" & workflowProperties.WorkflowId.ToString & ">here</a>" & "." & _
"</span></span>"

7. drag and drop a SendEmail activity between hlogPeerReviewerRejected and setStateInitiatorApproval
and set the following properties:
a. Name: sendPeerReviewerRejectedEmail
b. Body: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
c. CC: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
d. CorrelationToken: choose workflowToken from dropdownlist
e. Subject: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
f. To: click the ellipses, click the "Bind to a new member" tab,
remove the 1 from the end of the "New member name" and click OK
g. MethodInvoking: type InitiatorApprovalTaskEmail and hit enter
h. You will be taken to the code stub for PeerReviewerRejectedEmail. Copy and paste the following code:

'get Initiator's email address
Dim InitiatorObject As SPUser = GetUserObject(workflowProperties.Originator)
Dim InitiatorEmail = InitiatorObject.Email

'get CC's email address
Dim CCObject As SPUser = GetUserObject(CC)
Dim CCEmail = CCObject.Email

'get PeerReviewer's name
Dim PeerReviewerObject As SPUser = GetUserObject(PeerReviewer)
Dim PeerReviewerName = PeerReviewerObject.Name

sendPeerReviewerRejectedEmail_To = InitiatorEmail
sendPeerReviewerRejectedEmail_CC = CCEmail
sendPeerReviewerRejectedEmail_Subject = workflowProperties.Item.File.Name & " has been rejected by " & PeerReviewerName
sendPeerReviewerRejectedEmail_Body = "<span style='font-family: arial; font-size: medium'>" & _
workflowProperties.Item.File.Name & " was rejected by " & PeerReviewerName & " on " & DateTime.Now & _
"<br><br>" & _
"<span style='font-family: arial; font-size: x-small'>" & _
"To view this workflow's history, click " & "<a href=" & workflowProperties.SiteUrl & "/_layouts/WrkStat.aspx?List=" & workflowProperties.ListId.ToString & "&WorkflowInstanceID=" & workflowProperties.WorkflowId.ToString & ">here</a>" & "." & _
"</span></span>"

Posts in this series:
Part 1: Introduction
Part 2: Create the initiation form
Part 3: Create the task form
Part 4: Create the state machine workflow
Part 5: Add workflow history logging
Part 6: Add task notification emails