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

How to create a SharePoint State Machine Workflow: Part 5 - Add workflow history logging

Here in Part 5, We'll add history logging to the workflow. This is really useful as it allows the user to see messages about the status of the workflow, such as who approved it or rejected it. This part assumes you have completed part 4.

1. add this function to the code:

Private Function GetUserObject(ByVal accountID As String) As SPUser
If accountID.IndexOf("\") > 0 Then
Dim user As SPUser = Me.workflowProperties.Web.SiteUsers(accountID)
Return user
Else
Dim user As SPUser = Me.workflowProperties.Web.SiteUsers("OSS\" + accountID)
Return user
End If
End Function

2. go back to the design surface and double-click InitialStateActivities to drill down into it

3. drag and drop a LogToHistoryListActivity activity between onWorkflowActivated and setStatePeerReviewerApproval1
and set the following properties:
a. name: hlogWorkflowStarted
b. EventId: WorkflowStarted
c. HistoryDescription: Workflow started.
d. HistoryOutcome: create property hlogWorkflowStarted_HistoryOutcome
e. UserId: create property hlogWorkflowStarted_UserId

4. double-click the onWorkflowActivated activity to be taken to its code stub

5. copy and paste the following code to have 1. the workflow's originator assigned to the UserId property of the
logToHistory activity, and 2. the instructions entered by the originator assigned to the HistoryOutcome property:

'assign workflow's executor to the LogToHistory activity's user id
Dim executor As SPUser = GetUserObject(workflowProperties.Originator)
hlogWorkflowStarted_UserId = executor.ID

'assign the instructions to the HistoryOutcome property
hlogWorkflowStarted_HistoryOutcome = "Instructions: " & initform.txtInstructions

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

7. drag and drop a LogToHistoryListActivity activity below createInitiatorApprovalTask
and set the following properties:
a. name: hlogInitiatorApprovalTaskCreated
b. EventId: TaskCreated
c. HistoryDescription: Initiator approval task created. Awaiting approval.
d. UserId: 0

8. go back to the design surface and double-click InitiatorApprovalActivities to drill down into it

9. drag and drop a LogToHistoryListActivity activity between ifInitiatorApproved and setStatePeerReviewerApproval
and set the following properties:
a. name: hlogInitiatorApproved
b. EventId: TaskCompleted
c. HistoryDescription: Initiator approved.
d. HistoryOutcome: create property hlogInitiatorApproved_HistoryOutcome
e. UserId: create property hlogInitiatorApproved_UserId

10. click on the onInitiatorApprovalChanged activity and set the following property
a. Executor: create property onInitiatorApprovalChanged_Executor

11. double-click onInitiatorApprovalChanged activity to be taken to its code stub

12. copy and paste the following code to have the task's executor assigned to the UserId property of the
logToHistory activity:

'assign task's executor to the LogToHistory activity's user id
Dim executor As SPUser = GetUserObject(onInitiatorApprovalChanged_Executor)
hlogInitiatorApproved_UserId = executor.ID

'assign the task's comments to the HistoryOutcome property
hlogInitiatorApproved_HistoryOutcome = "Comments: " & onInitiatorApprovalChanged.AfterProperties.ExtendedProperties("txtComments").ToString

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

14. drag and drop a LogToHistoryListActivity activity below createPeerReviewerApprovalTask
and set the following properties:
a. name: hlogPeerReviewerApprovalTaskCreated
b. EventId: TaskCreated
c. HistoryDescription: Peer Reviewer approval task created. Awaiting approval.
d. UserId: 0

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

16. drag and drop a LogToHistoryListActivity activity between ifPeerReviewerApproved and setStateFinalState
and set the following properties:
a. name: hlogPeerReviewerApproved
b. EventId: TaskCompleted
c. HistoryDescription: Peer Reviewer approved.
d. HistoryOutcome: create property hlogPeerReviewerApproved_HistoryOutcome
e. UserId: create property hlogPeerReviewerApproved_UserId

17. drag and drop a LogToHistoryListActivity activity between ifPeerReviewerRejected and setStateInitiatorApproval
and set the following properties:
a. name: hlogPeerReviewerRejected
b. EventId: TaskCompleted
c. HistoryDescription: Peer Reviewer rejected.
d. HistoryOutcome: create property hlogPeerReviewerRejected_HistoryOutcome
e. UserId: create property hlogPeerReviewerRejected_UserId

18. click on the onPeerReviewerApprovalChanged activity and set the following property
a. Executor: create property onPeerReviewerApprovalChanged_Executor

19. double-click onPeerReviewerApprovalChanged activity to be taken to its code stub

20. copy and paste the following code to have the task's executor assigned to the UserId property of the
logToHistory activity:

'assign task's executor to the LogToHistory activity's user id
Dim executor As SPUser = GetUserObject(onPeerReviewerApprovalChanged_Executor)
hlogPeerReviewerApproved_UserId = executor.ID
hlogPeerReviewerRejected_UserId = executor.ID

'assign the task's comments to the HistoryOutcome property
hlogPeerReviewerApproved_HistoryOutcome = "Comments: " & onPeerReviewerApprovalChanged.AfterProperties.ExtendedProperties("txtComments").ToString
hlogPeerReviewerRejected_HistoryOutcome = "Comments: " & onPeerReviewerApprovalChanged.AfterProperties.ExtendedProperties("txtComments").ToString

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

22. drag and drop a LogToHistoryListActivity activity between hlogPeerReviewerApproved and setStateFinalState
and set the following properties:
a. name: hlogWorkflowCompleted
b. EventId: WorkflowCompleted
c. HistoryDescription: Workflow completed.
d. UserId: 0

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

Wednesday, August 19, 2009

How to create a SharePoint State Machine Workflow: Part 4 - Create the state machine workflow

Here in Part 4, we create the workflow and deploy it to the SharePoint server. I have done these steps with both Visual Studio 2005 and 2008. When deploying, it tells you what to do based on whether you are using 2005 or 2008. Also, I included VB as well as C# code depending on your tastes. It seems like most everybody is using C#. We use VB in-house traditionally so I figured I would stay with VB. You'll notice that there really isn't a whole lot of code here at all - just lots and lots of steps. This workflow works off of four states: InitialState, InitiatorApproval, PeerReviewerApproval and FinalState.

1. In Visual Studio 2005, File -> New -> Project -> SharePoint -> SharePoint Server State Machine, call it AgendaItemPeerReview
In Visual Studio 2008, File -> New -> Project -> Visual Basic -> Office -> 2007 -> SharePoint 2007 State Machine Workflow,
call it AgendaItemPeerReview
set location to d:\asp or whatever you want
Rename Workflow1.vb to PeerReviewWorkflow.vb and doubleclick it to open the design surface.
It will start out with a state called Workflow1InitialState. Rename Workflow1InitialState to InitialState.

Note: The workflow MUST have a different name than the solution/project. Do NOT rename file workflow1.cs (and thus the
workflow1 class) to the same name you chose as the solution/project.

2. add three more states and name them InitiatorApproval, PeerReviewerApproval and FinalState

3. right click workflow and set CompletedStateFinish property to the new state you created called FinalState
be sure InitialStateName is InitialState

We will build the workflow by adding activities to the various states.
Afterwards, we will come back and add the necessary code. I do it this way because some code is autogenerated while
other code must be written by us, and by building the whole workflow first, all the generated code will be together,
then all our code will be together below the generated code. Also, this provides more of a top-down approach to
designing (and understanding) the workflow.

4. in state InitialState, click eventDrivenActivity1 and rename it to InitialStateActivities

5. double-click this activity to drill down into it; in here we will create a sequential workflow; notice there is
already an onWorkflowActivated1 event
a. Rename onWorkflowActivated1 to onWorkflowActivated.
b. Drill down into CorrelationToken and change OwnerActivityName to PeerReviewWorkflow.
c. Drill down into WorkflowProperties and change Name to PeerReviewWorkflow.

6. in the Toolbox, find the Windows Workflow section and drag and drop a SetState event below onWorkflowActivated
a. rename setState activity to setStatePeerReviewerApproval
b. set TargetStateName property to PeerReviewerApproval
c. click on PeerReviewWorkflow to go back to workflow view; notice the line drawn from InitialState to
state PeerReviewerApproval

For the two approval states, we are going to add the same stuff, ie initialization code for when a state is entered,
code to execute while in the state, and code to execute when the state is completed.

7. for each of the two states, drag and drop into them:
a. a StateInitialization activity
b. an EventDriven activity
c. a StateFinalization activity

8. in stateInitiatorApproval, rename the activities to:
InitiatorApprovalInitialization
InitiatorApprovalActivities
InitiatorApprovalFinalization

9. in statePeerReviewerApproval, rename the activities to:
PeerReviewerApprovalInitialization
PeerReviewerApprovalActivities
PeerReviewerApprovalFinalization

We will now add the specific tasks within each of these three activities (initialization, activities, finalization) for each state.
Essentially, we will be creating sequential workflows within each of these activities.

10. in InitiatorApproval, double-click InitiatorApprovalInitialization to drill down into it

11. drag and drop a CreateTask activity and set the following properties:
a. rename it createInitiatorApprovalTask
b. set CorrelationToken to InitiatorApprovalToken; click the + next to it to set the OwnerActivityName to
InitiatorApproval
c. for TaskId, 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. follow the same step for TaskProperties
e. exit this sequential workflow view by clicking on PeerReviewWorkflow on the design surface

12. double-click InitiatorApprovalFinalization to drill down into it

13. drag and drop a CompleteTask activity and set the following properties:
a. rename it completeInitiatorApprovalTask
b. set CorrelationToken to InitiatorApprovalToken; click the + next to it to set the OwnerActivityName to
InitiatorApproval
c. for TaskId, click the ellipses, on the "Bind to an existing member" tab, click createInitiatorApprovalTask_TaskId
d. exit this sequential workflow view by clicking on PeerReviewWorkflow on the design surface

14. double-click InitiatorApprovalActivities to drill down into it

15. drag and drop an OnTaskChanged activity and set the following properties:
a. rename it onInitiatorApprovalChanged
b. set CorrelationToken to InitiatorApprovalToken; click the + next to it to set the OwnerActivityName to
InitiatorApproval
c. for TaskId, click the ellipses, on the "Bind to an existing member" tab, click createInitiatorApprovalTask_TaskId
d. for AfterProperties and BeforeProperties, 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

16. drag and drop an IfElse activity and rename it InitiatorApprovalStatus

17. click on the left branch and set the following properties:
a. rename it ifInitiatorApproved
b. for Condition, click Code Condition

18. drag and drop a setState activity under ifInitiatorApproved
a. rename it setStatePeerReviewerApproval
b. set the TargetStateName to PeerReviewerApproval


19. click PeerReviewWorkflow to go back to higher-level view of workflow

20. in PeerReviewerApproval, double-click PeerReviewerApprovalInitialization to drill down into it

21. drag and drop a CreateTask activity and set the following properties:
a. rename it createPeerReviewerApprovalTask
b. set CorrelationToken to PeerReviewerApprovalToken; click the + next to it to set the OwnerActivityName to
PeerReviewerApproval
c. for TaskId, 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. follow the same step for TaskProperties
e. exit this sequential workflow view by clicking on PeerReviewWorkflow on the design surface

22. double-click PeerReviewerApprovalFinalization to drill down into it

23. drag and drop a CompleteTask activity and set the following properties:
a. rename it completePeerReviewerApprovalTask
b. set CorrelationToken to PeerReviewerApprovalToken; click the + next to it to set the OwnerActivityName to
PeerReviewerApproval
c. for TaskId, click the ellipses, on the "Bind to an existing member" tab, click createPeerReviewerApprovalTask_TaskId
d. exit this sequential workflow view by clicking on PeerReviewWorkflow on the design surface

24. double-click PeerReviewerApprovalActivities to drill down into it

25. drag and drop an OnTaskChanged activity and set the following properties:
a. rename it onPeerReviewerApprovalChanged
b. set CorrelationToken to PeerReviewerApprovalToken; click the + next to it to set the OwnerActivityName to
InitiatorApproval
c. for TaskId, click the ellipses, on the "Bind to an existing member" tab, click createPeerReviewerApprovalTask_TaskId
d. for AfterProperties and BeforeProperties, 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

26. drag and drop an IfElse activity and rename it PeerReviewerApprovalStatus

27. click on the left branch and set the following properties:
a. rename it ifPeerReviewerApproved
b. for Condition, click Code Condition

28. drag and drop a setState activity under ifPeerReviewerApproved
a. rename it setStateFinalState
b. set the TargetStateName to FinalState

29. click on the right branch and set the following properties:
a. rename it ifPeerReviewerRejected
b. for Condition, click Code Condition

30. drag and drop a setState activity under ifPeerReviewerRejected
a. rename it setStateInitiatorApproval
b. set the TargetStateName to InitiatorApproval




Now we are ready to start adding code. First we will add code in InitialState, for when the workflow starts.

31. Go to the code-behind and find the following line (this is the VB version, but the C# version looks almost identical):
Public workflowProperties As SPWorkflowActivationProperties = New Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties

Above it, type:

VISUAL BASIC
------------
Public workflowId As System.Guid

VISUAL C#
---------
public Guid workflowId = default(System.Guid);


Below the workflowProperties line, type:

VISUAL BASIC
------------
Dim PeerReviewer As String
Dim CC As String
Dim Instructions As String

VISUAL C#
---------
private String PeerReviewer = default(String);
private String CC = default(String);
private String Instructions = default(String);


32. back on design surface, double-click InitialStateActivitites to drill down into it

33. double-click onWorkflowActivated. This will take you to the code behind and show the generated code stub
onWorkflowActivated_Invoked. Copy and paste the following code into it:

VISUAL BASIC
------------
workflowId = workflowProperties.WorkflowId

Dim serializer As XmlSerializer = New XmlSerializer(GetType(InitForm))
Dim reader As XmlTextReader = New XmlTextReader(New System.IO.StringReader(workflowProperties.InitiationData))
Dim initform As InitForm = CType(serializer.Deserialize(reader), InitForm)


Dim prPeerReviewer() As Person
prPeerReviewer = initform.gpPeerReviewer
PeerReviewer = prPeerReviewer(0).AccountId

Dim prCC() As Person
prCC = initform.gpCC
CC = prCC(0).AccountId

Instructions = initform.txtInstructions


VISUAL C#
---------
workflowId = workflowProperties.WorkflowId;

XmlSerializer serializer = new XmlSerializer(typeof(InitForm));
XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(workflowProperties.InitiationData));
InitForm initform = (InitForm)serializer.Deserialize(reader);


Person prPeerReviewer = new Person();
prPeerReviewer = (Person)initform.gpPeerReviewer[0];
//PeerReviewer = prPeerReviewer.AccountId.GetValue(0).ToString();
PeerReviewer = prPeerReviewer.AccountId;

Person prCC = new Person();
prCC = (Person)initform.gpCC[0];
//CC = prCC.AccountId.GetValue(0).ToString();
CC = prCC.AccountId;

Instructions = initform.txtInstructions;



34. go to the top of PeerReviewWorkflow.vb and add the following code:

VISUAL BASIC
------------
imports System.Xml.Serialization
imports System.Xml

VISUAL C#
---------
using System.Xml.Serialization;
using System.Xml;


Next, we will add code to the InitiatorApproval state.

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

36. double-click createInitiatorApprovalTask to be taken to its MethodInvoking code stub. Copy and paste
the following code in it.


VISUAL BASIC
------------
createInitiatorApprovalTask_TaskId = Guid.NewGuid
createInitiatorApprovalTask_TaskProperties = new SPWorkflowTaskProperties
createInitiatorApprovalTask_TaskProperties.Title = "Initiator Approval of " & workflowProperties.Item.File.Name
createInitiatorApprovalTask_TaskProperties.AssignedTo = workflowProperties.Originator
createInitiatorApprovalTask_TaskProperties.Description = ""
createInitiatorApprovalTask_TaskProperties.TaskType = 0
createInitiatorApprovalTask_TaskProperties.ExtendedProperties("txtInstructions") = Instructions


VISUAL C#
---------
createInitiatorApprovalTask_TaskId = Guid.NewGuid();
createInitiatorApprovalTask_TaskProperties = new SPWorkflowTaskProperties();
createInitiatorApprovalTask_TaskProperties.Title = "Initiator Approval of " + workflowProperties.Item.File.Name;
createInitiatorApprovalTask_TaskProperties.AssignedTo = workflowProperties.Originator;
createInitiatorApprovalTask_TaskProperties.Description = "";
createInitiatorApprovalTask_TaskProperties.TaskType = 0;
createInitiatorApprovalTask_TaskProperties.ExtendedProperties["txtInstructions"] = Instructions;


37. go back to the design surface and double-click InitiatorApprovalActivities to drill down into it

38. double-click onInitiatorApprovalChanged to be taken to its Invoked code stub. Copy and paste
the following code in it.

VISUAL BASIC
------------
onInitiatorApprovalChanged_AfterProperties = onInitiatorApprovalChanged.AfterProperties
onInitiatorApprovalChanged_BeforeProperties = onInitiatorApprovalChanged.BeforeProperties

VISUAL C#
---------
onInitiatorApprovalChanged_AfterProperties = onInitiatorApprovalChanged.AfterProperties;
onInitiatorApprovalChanged_BeforeProperties = onInitiatorApprovalChanged.BeforeProperties;


39. go back to the design surface and click ifInitiatorApproved and set the following properties:
a. drill down into the Condition property (already set to Code Condition), and next to the second
Condition property, type InitiatorApproved and hit enter
b. you will be taken to the code stub for InitiatorApproved routine; copy and paste the following
code into it:

VISUAL BASIC
------------
If onInitiatorApprovalChanged_AfterProperties.ExtendedProperties("status").ToString = "accepted" Then
e.Result = True
End If

VISUAL C#
---------
if (onInitiatorApprovalChanged_AfterProperties.ExtendedProperties["status"].ToString() == "accepted")
{
e.Result = true;
}


Next, we will add code to the PeerReviewerApproval state.

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

41. double-click createPeerReviewerApprovalTask to be taken to its MethodInvoking code stub. Copy and paste
the following code in it.


VISUAL BASIC
------------
createPeerReviewerApprovalTask_TaskId = Guid.NewGuid
createPeerReviewerApprovalTask_TaskProperties = new SPWorkflowTaskProperties
createPeerReviewerApprovalTask_TaskProperties.Title = "Peer Reviewer Approval of " & workflowProperties.Item.File.Name
createPeerReviewerApprovalTask_TaskProperties.AssignedTo = PeerReviewer
createPeerReviewerApprovalTask_TaskProperties.Description = ""
createPeerReviewerApprovalTask_TaskProperties.TaskType = 0
createPeerReviewerApprovalTask_TaskProperties.ExtendedProperties("txtInstructions") = Instructions


VISUAL C#
---------
createPeerReviewerApprovalTask_TaskId = Guid.NewGuid();
createPeerReviewerApprovalTask_TaskProperties = new SPWorkflowTaskProperties();
createPeerReviewerApprovalTask_TaskProperties.Title = "Peer Reviewer Approval of " + workflowProperties.Item.File.Name;
createPeerReviewerApprovalTask_TaskProperties.AssignedTo = PeerReviewer;
createPeerReviewerApprovalTask_TaskProperties.Description = "";
createPeerReviewerApprovalTask_TaskProperties.TaskType = 0;
createInitiatorApprovalTask_TaskProperties.ExtendedProperties["txtInstructions"] = Instructions;


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

43. double-click onPeerReviewerApprovalChanged to be taken to its Invoked code stub. Copy and paste
the following code in it.

VISUAL BASIC
------------
onPeerReviewerApprovalChanged_AfterProperties = onPeerReviewerApprovalChanged.AfterProperties
onPeerReviewerApprovalChanged_BeforeProperties = onPeerReviewerApprovalChanged.BeforeProperties

VISUAL C#
---------
onPeerReviewerApprovalChanged_AfterProperties = onPeerReviewerApprovalChanged.AfterProperties;
onPeerReviewerApprovalChanged_BeforeProperties = onPeerReviewerApprovalChanged.BeforeProperties;


44. go back to the design surface and click ifPeerReviewerApproved and set the following properties:
a. drill down into the Condition property (already set to Code Condition), and next to the second
Condition property, type PeerReviewerApproved and hit enter
b. you will be taken to the code stub for PeerReviewerApproved routine; copy and paste the following
code into it:

VISUAL BASIC
------------
If onPeerReviewerApprovalChanged_AfterProperties.ExtendedProperties("status").ToString = "accepted" Then
e.Result = True
End If

VISUAL C#
---------
if (onPeerReviewerApprovalChanged_AfterProperties.ExtendedProperties["status"].ToString() == "accepted")
{
e.Result = true;
}


45. go back to the design surface and click ifPeerReviewerRejected and set the following properties:
a. drill down into the Condition property (already set to Code Condition), and next to the second
Condition property, type PeerReviewerRejected and hit enter
b. you will be taken to the code stub for PeerReviewerApproved routine; copy and paste the following
code into it:

VISUAL BASIC
------------
If onPeerReviewerApprovalChanged_AfterProperties.ExtendedProperties("status").ToString = "rejected" Then
e.Result = True
End If

VISUAL C#
---------
if (onPeerReviewerApprovalChanged_AfterProperties.ExtendedProperties["status"].ToString() == "rejected")
{
e.Result = true;
}


46. sign the assembly
a. right-click the project, then click properties
b. click Signing
c. if Sign the assembly is not already checked...
d. check Sign the assembly
e. choose <new> from the dropdown
f. enter MyKeyFile for the key file name
g. uncheck Protect my key file with a password
h. click OK

47. follow instructions above for creating the workflow's Initiation and Task forms using Microsoft Office InfoPath

48. If developing in Visual Studio 2005: copy PeerReviewInit.xsn and PeerReviewTask.xsn to DeploymentFiles\FeatureFiles folder

49. be sure feature.xml is set up correctly
a. title: Agenda Item Peer Review feature
b. description: This feature installs the Agenda Item Peer Review workflow
c. if deploying via Visual Studio 2008:
under the <ElementManifests> tag, add:
<ElementFile Location="PeerReviewInit.xsn" />
<ElementFile Location="PeerReviewTask.xsn" />

50. be sure workflow.xml is set up correctly
a. name: AgendaItemPeerReview
b. description: This is the Agenda Item Peer Review workflow
c. CodeBesideClass: AgendaItemPeerReview.PeerReviewWorkflow
d. after CodeBesideAssembly, you must have the following:
TaskListContentTypeId="0x01080100C9C9515DE4E24001905074F980F93160"
e. after TaskListContentTypeId, you must have the following:
AssociationUrl="_layouts/CstWrkflIP.aspx"
InstantiationUrl="_layouts/IniWrkflIP.aspx"
ModificationUrl="_layouts/ModWrkflIP.aspx"
f. copy initiation form's urn into the <Association_FormURN> and <Instantiation_FormURN> tags
g. copy task form's urn into the <Task0_FormURN> tag

51. deploy solution
a. Visual Studio 2005: right click project, click Properties, click Build Events, change NODEPLOY to
DEPLOY at the end of the Post-build event command line, click Build, click Rebuild Solution
b. Visual Studio 2008: click Build, then click Deploy Solution

52. The first time you deploy it, the AgendaItemPeerReview assembly is added to the GAC. The workflow.xml
file needs the assembly's public key token.
a. Start, Settings, Control Panel, Administrative Tools, Microsoft .NET Framework 2.0 Configuration
b. click Manage the Assembly Cache
c. click View List of Assemblies in the Assembly Cache
d. right-click on AgendaItemPeerReview and click Properties
e. copy the public key token and click OK
f. paste the public key token in workflow.xml where you find PublicKeyToken
g. redeploy solution (step 51)





Another method for retrieving the assembly's public key token
-------------------------------------------------------------
If you want to skip step 52, retrieve the public key token using the sn.exe utility then
update workflow.xml (step 50) with it

1. copy sn.exe from c:\program files\microsoft visual studio 8\sdk\v2.0\bin to d:\
2. change directory to d:\asp\agendaitempeerreview\agendaitempeerreview\bin\debug
3. d:\sn -T agendaitempeerreview.dll
4. copy (or type) the resulting public key token into the workflow.xml

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

How to create a SharePoint State Machine Workflow: Part 3 - Create the task form

In Part 3, we will create the workflow's task form. In Part 4, we will configure this form to be called as the task form.

1. load Microsoft Office InfoPath 2007
2. Under Design a form, click Design a Form Template, click Blank, then click OK
3. click Insert, Layout Table..., 2 columns, 3 rows, click OK
4. add three buttons to the right column, last row
5. in Design Tasks, click Data Source
a. double-click myFields and rename it to TaskForm and click OK
b. right-click TaskForm and click Add...
c. for name, type status
d. click OK
6. double-click the first button
a. change Label to Approve
b. click Rules...
c. click Add...
d. click Add Action...
e. select Submit using a data connection from dropdown
f. click Add...
g. be sure Create a new connection to submit data is selected and click Next
h. select To the hosting environment, such as an ASP.NET page or a hosting application
i. leave Submit as the name for the data connection and click Finish
j. click OK
k. click Add Action...
l. select Set a field's value
m. click icon to the right of the Field textbox
n. click status and click OK
o. for value, type: accepted
p. click OK
q. click Add Action...
r. select Close the form from the dropdown
s. uncheck If changes have not been saved...
t. click OK
u. click OK
v. click OK
w. click OK
7. double-click the second button
a. change Label to Reject
b. click Rules...
c. click Add...
d. click Add Action...
e. select Submit using a data connection from dropdown
f. click Add...
g. be sure Create a new connection to submit data is selected and click Next
h. select To the hosting environment, such as an ASP.NET page or a hosting application
i. leave Submit as the name for the data connection and click Finish
j. click OK
k. click Add Action...
l. select Set a field's value
m. click icon to the right of the Field textbox
n. click status and click OK
o. for value, type: rejected
p. click OK
q. click Add Action...
r. select Close the form from the dropdown
s. uncheck If changes have not been saved...
t. click OK
u. click OK
v. click OK
w. click OK
8. double-click the third button
a. change Label to Cancel
b. click Rules...
c. click Add...
d. click Add Action...
e. select Close the form from the dropdown
f. uncheck If changes have not been saved...
g. click OK
h. click OK
i. click OK
j. click OK
9. in left column, first row, type: Instructions
10. in left column, second row, type: Comments
11. in the right column, first row, drop a text box control into it
a. double-click the text box control
b. change field name to txtInstructions
c. click Display tab and check Multi-line
d. click OK
e. drag the bottom edge of the control down a little ways so a few lines of text will show
12. in the right column, second row, drop a text box control into it
a. double-click the text box control
b. change field name to txtComments
c. click Display tab and check Multi-line
d. click OK
e. drag the bottom edge of the control down a little ways so a few lines of text will show
13. when the task form loads, we want txtInstructions to populate automatically with what was typed in
to txtInstructions on the initiation form
a. on the Desktop, create a file called ItemMetadata.xml (CASE IS EXTREMELY IMPORTANT!!!) with the following text in it:
<z:row xmlns:z="#RowsetSchema" ows_txtInstructions="" />
b. in Design Tasks, click Data Source, click Manage Data Connections... (near bottom)
c. click Add
d. click Create a connection to Receive data
e. click Next
f. be sure XML document is selected and click Next
g. click Browse and select ItemMetadata.xml from Desktop
h. click Next
i. be sure Include the data as a resource file... is selected and click Next
j. click Finish
k. click Close
l. double-click txtInstructions
m. click the fx button next to the value text field
n. click Insert Field or Group...
o. change data source in drop down to ItemMetadata (Secondary)
p. click :ows_txtInstructions
q. click Ok
r. click Ok
s. click Ok
Note: In the code that creates the tasks in the workflow, we will add txtInstructions to the task's
ExtendedProperties and populate it with txtInstructions from the initiation form. ItemMetaData.xml
will get this value from the task's txtInstructions property (added in ExtendedProperties) and will
then populate the task form's txtInstructions textbox on load.
14. make form be able to open in a browser or in a client application
a. in Design Tasks, click Design Checker, click Change Compatibility Settings...
b. in the Compatibility category, check Design a form template that can be opened in a browser or InfoPath
c. in the Security and Trust category, uncheck Automatically determine security level, click Full Trust
d. click OK
15. save form in My Documents as PeerReviewTask.xsn
16. retrieve the form's id
a. click File, Properties
b. copy text in ID textbox into notepad to be used in a later step. it should look like this:
urn:schemas-microsoft-com:office:infopath:PeerReviewTask:-myXSD-2008-07-30T18-33-30
17. publish the InfoPath form
a. click File, Publish
b. select To a network location
c. click Next
d. browse to the workflow project's location and name the file PeerReviewTask.xsn
e. click OK
f. click Next
g. delete the path in the textbox and click Next
h. click Publish
i. click Close

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

How to create a SharePoint State Machine Workflow: Part 2 - Create the initiation form

In Part 2, we will create the workflow's initiation form. In Part 4, we will configure this form to be called in two locations: first, when a user associates the workflow with a document library, and second, when a user instantiates the workflow on a particular document. Thus, we are using the same form as both the association form and the initiation form. Its possible to use different forms collecting different information, but for simplicity, we will use the same form for both.

1. load Microsoft Office InfoPath 2007
2. Under Design a form, click Design a Form Template, click Blank, then click OK
3. if the Contact Selector control has not been added to the controls list, you will need to add it
a. on the Design Tasks pane, click Controls; if Contact Selector does not appear
under the Custom controls in the bottom of the list, continue with the following
steps. If it does appear there (ie you already added it), skip to step 4
b. click Add or Remove Custom Control...
c. click Add button
d. click ActiveX Control and click Next
e. click Contact Selector from the list and click Next
f. click Don't include a .cab file and click Next
g. click Value and click Next
h. in the Field or group type dropdown, select Field or Group (any data type) and click Finish, Close, OK
4. click Insert, Layout Table..., 2 columns, 4 rows, click OK
5. add two buttons to the right column, last row
6. double-click the first button
a. change Label to OK
b. click Rules...
c. click Add...
d. click Add Action...
e. select Submit using a data connection from dropdown
f. click Add...
g. be sure Create a new connection to submit data is selected and click Next
h. select To the hosting environment, such as an ASP.NET page or a hosting application
i. leave Submit as the name for the data connection and click Finish
j. click OK
k. click Add Action...
l. select Close the form from the dropdown
m. uncheck If changes have not been saved...
n. click OK
o. click OK
p. click OK
q. click OK
r. stretch out OK button
7. double-click the second button
a. change Label to Cancel
b. click Rules...
c. click Add...
d. click Add Action...
e. select Close the form from the dropdown
f. uncheck If changes have not been saved...
g. click OK
h. click OK
i. click OK
j. click OK
8. in left column, first row, type: Peer Reviewer
9. in left column, second row, type: CC
10. in left column, third row, type: Instructions
11. in the right column, third row, drop a text box control into it
a. double-click the text box control
b. change field name to txtInstructions
c. click Display tab and check Multi-line
d. click OK
12. resize left column to be smaller than right column, resize txtInstructions to allow
for multiple lines
13. in the right column, first row, drop a Contact Selector control into it
a. double-click the control, rename it to gpPeerReviewer, click OK
14. in the right column, second row, drop a Contact Selector control into it
a. double-click the control, rename it to gpCC, click OK
15. in Design Tasks, click Data Source
a. double-click myFields and rename it to InitForm and click OK
b. right click group1 and delete it
c. right click gpPeerReviewer and click Add
d. name is Person, the type is Group, check Repeating
e. right click Person, click Add..., enter name as DisplayName, click OK
f. right click Person, click Add..., enter name as AccountId, click OK
g. right click Person, click Add..., enter name as AccountType, click OK
h. right click Person, click Reference..., click gpCC, click OK
16. make form be able to open in a browser or in a client application
a. in Design Tasks, click Design Checker, click Change Compatibility Settings...
b. in the Compatibility category, check Design a form template that can be opened in a browser or InfoPath
c. in the Security and Trust category, uncheck Automatically determine security level, click Full Trust
d. click OK
17. create the context for the form so it knows where to retrieve people's names from:
a. open Notepad and type in the following and save it as Context.xml in the same folder as the workflow:
<Context
isStartWorkflow="true"
isRunAtServer="true"
provideAllFields="true"
siteUrl="http://sharepoint2007:100"
/>
Note: be sure siteUrl is for your site!!!
18. add a secondary data source using Context.xml so form can retrieve contact data:
a. in Design Tasks, Data Source, click Manage Data Connections...
b. click Add...
c. Create a new connection to Receive Data
d. click Next
e. click XML document
f. click Next
g. browse to Context.xml
h. click Next
i. click Include the data as a resource file in the form template or template part
j. click Next
k. leave Context as the name of the data connection
l. ensure Automatically retrieve data when form is opened is checked
m. click Finish
n. click Close
19. save form in My Documents as PeerReviewInit.xsn
20. retrieve the form's id
a. click File, Properties
b. copy text in ID textbox into notepad to be used in a later step. it should look like this:
urn:schemas-microsoft-com:office:infopath:PeerReviewInit:-myXSD-2008-07-23T13-50-07
21. publish the InfoPath form
a. click File, Publish
b. select To a network location
c. click Next
d. browse to the workflow project's location and name the file PeerReviewInit.xsn
e. click OK
f. click Next
g. delete the path in the textbox and click Next
h. click Publish
i. click Close
22. create the schema for the form so the workflow can reference the contents of the form
a. click File, Save as source files...
b. browse to the folder where the workflow is
c. click OK
d. close Microsoft InfoPath (you will not be able to make any changes to the form once InfoPath is closed)
e. click Start, Run, type CMD and click OK
f. change folder to the workflow's folder
g. type: xsd myschema.xsd /c /language:VB (Note: you may have to find xsd.exe on your HD first)
h. rename the resulting myschema.vb to InitForm.vb
23. add file to workflow project
a. in Visual Studio in Solution Explorer, Show All Files
b. right-click InitForm.vb and click Include In Project

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

How to create a SharePoint State Machine Workflow: Part 1 - Introduction

A while back I was tasked with creating a SharePoint workflow that would enable peer review of a document. (BTW, this whole article series will be all about creating workflows with Visual Studio, not SharePoint Designer.) An initiator would assign the workflow to a document in a document library and set who they wanted to peer review it. Now, if you've searched the internet for SharePoint workflows, you've undoubtedly seen that one sequential workflow that always pops up in examples. The problem is that sometimes a sequential workflow doesn't cut it - you just might need a state machine workflow. Imagine having a workflow where someone wants to send a document out for approval to three people sequentially, like a manager, a division director, and a department director. After the manager approves it, it goes to the division director, then when that person approves it, the department director gets it. But what if the division director rejects it? You might want it to bounce back to the manager. Similarly, if the department director rejects it, it goes back to the division director. The state machine workflow is the way to do this as each person will be a state.

This will be a six part series on how to create a SharePoint state machine workflow, although only the first four parts are necessary to create a functioning workflow
.
This example will employ a simple peer review model. An initiator will start a workflow on a document in a document library, assigning the person they want to act as the peer reviewer in the Initiation form. The workflow then starts. The peer reviewer will get a task. If the peer reviewer approves the task, the workflow is complete. If the peer review rejects the task, the initiator will get a task. In theory, the initiator could make changes to their document, and once they are done, they would approve the task, thus moving the flow back to the peer reviewer again. Once again, the peer reviewer would need to accept the task (which ends the workflow) or reject it (which sends the flow back to the initiator).

You are currently reading Part 1 of this series.
Part 2 will focus on using InfoPath to create the workflow's Initiation form.
Part 3 will focus on using InfoPath to create the workflow's Task form.
Part 4 will focus on creating the state machine workflow itself.

After part 4, you will have a fully functioning workflow. The following two parts are more like enhancements, but you'll likely want them.

Part 5 will focus on adding workflow history logging to the workflow.
Part 6 will focus on adding task notification emails to the workflow.

Thursday, July 23, 2009

How to deploy a SharePoint web part to bin

As I was learning how to deploy my web parts as features in a solution package .wsp (I used WSPBuilder to create the .wsp), I realized I needed to make sure they were deployed not to the GAC (which is the default), but rather to the web application's /bin folder (under the web application in IIS). Bin is preferred over GAC because of security reasons: deploying to GAC unnecessarily exposes the web part's assembly .dll to every application on the server.

This post will show you how to deploy a web part to the /bin folder (using WSPBuilder) AND how to configure it using CAS (Code Access Security) so your web part can actually run from that folder too! I kept finding articles that discuss one or the other, but not both. Those articles that did help me quite a bit have been referenced in the last part of this post.

So, from the top, when you create a web part in Visual Studio 2008 and hit F5, the web part will deploy to the site you selected (right-click Project's Properties, click Debug, then whatever site you typed in Start browser with URL). In your Solution Explorer, you will notice a folder called /bin with another folder called /debug under that, and the compiled code will sit under there. This means its being deployed into the GAC.

At a high level, there's really two steps we must do to get our web part to run from the web application's (your SharePoint site's) /bin folder. (You can get to /bin either by going into IIS and choosing the web application you want it deployed to, or by going to c:\inetpub\wwwroot\wss\VirtualDirectories\<web application>\bin.)

The first step is actually deploying to the above-mentioned location. Once this is done, sure, the web part is right where you want it to be, but it won't run until we do step two, which involves setting the proper permissions to allow it to run from that spot. This second step involves modifying the web.config to look at a custom policy file where we will make changes to the CAS policies.


First, deploy the web part assembly to the /bin:



1. In Solution Explorer, delete the /bin folder. It will reappear in a few seconds with a Debug subfolder, but it will be empty other than that.

2. In Solution Explorer, create a folder called 80, and under that, create a folder called bin. So you will have /80/bin.

3. Right-click the project in Solution Explorer, and on the Compile tab, change Build Output Path from bin\Debug\ to 80\bin\.

4. In Solution Explorer, in AssemblyInfo.vb, you must add two lines:
a. below the other Imports statements, add the following:
Imports System.Security
(Note: this will be a Using statement if you are doing C#)

b. Below one of the other Assembly statements, add the following:
<assembly: AllowPartiallyTrustedCallers()>
(Note: This step doesn't affect the deployment of the web part, but comes into play when you try to actually use the web part on a page after its been deployed to /bin.)

5. Now, when you compile the application you will notice the assembly (the dll) appears in /80/bin instead of where it was being put before, /bin/debug. If you hit F5 to deploy it, you will notice all the other deployment files Visual Studio creates is also placed in this folder. This leads me to my next point.

Visual Studio creating deployment files automatically is no good. Sure, it will get everything up and running on the server, but maybe not the way you want. For instance, even though I have created a /12/TEMPLATE/FEATURES/AddContactFromAD folder structure in my Solution Explorer, and put in my feature.xml, elements.xml (mine is actually called AddContactFromAD.xml), and a web part definitions file AddContactFromAD.webpart, Visual Studio generates its own feature.xml (which doesn't have my pretty description I put in the real feature.xml). It also tends to write its own manifest.xml, wanting to deploy to GAC! So, let's deploy a much better way, using WSPBuilder! You can download WSPBuilder here. Once installed, it will be added to Visual Studio's Tools menu.

6. Assuming you have the above-mentioned 12 hive folder structure with a folder for each web part under FEATURES, and with that, a feature.xml, elements.xml, and .webpart file under each web part's folder, you can simply click Tools -> WSPBuilder -> Build WSP. WSP Builder will create the resulting .wsp and drop it in your project folder. Renaming it to .cab and opening it you will see each web part's feature.xml, elements.xml, .webpart, plus the assembly .dll and an autogenerated manifest.xml. Looking inside the manifest.xml will reveal two things: 1. the DeploymentTarget is WebApplication (no longer GlobalAssemblyCache as it was before), and 2. CAS policy permissions were added.

7. Ok, so let's deploy this bad boy. To do this, I add the solution, deploy the solution, then activate the features (1 feature per web part). Below is my own batch file add.bat:
stsadm -o addsolution -filename contactuswebpart.wsp

stsadm -o deploysolution -name contactuswebpart.wsp -url http://sharepoint2007:1001/ -immediate -allowCasPolicies

stsadm -o activatefeature -name contactlist -url http://sharepoint2007:1001

stsadm -o activatefeature -name emailcontact -url http://sharepoint2007:1001

stsadm -o activatefeature -name addcontactfromad -url http://sharepoint2007:1001


Also, my remove.bat comes in handy when testing:
stsadm -o deactivatefeature -name contactlist -url http://sharepoint2007:1001

stsadm -o deactivatefeature -name emailcontact -url http://sharepoint2007:1001

stsadm -o deactivatefeature -name addcontactfromad -url http://sharepoint2007:1001

stsadm -o retractsolution -name contactuswebpart.wsp -url http://sharepoint2007:1001/ -immediate

stsadm -o deletesolution -name contactuswebpart.wsp


At this point, if you check your web application's /bin, you should see the assembly .dll there. If you go to the Web Part Gallery and click the web part, or if you attempt to add the web part to a page, you will get an error. This is because we still need to do Part 2 of this grand adventure: modify the permissions.


For the second and final step, set the CAS policy permissions to enable the web part to run from /bin:



1. We are going to copy the existing wss_minimaltrust.config, rename it, then make our policy changes in that file. Go to c:\Program Files\Common Files\Microsoft Shared\web server extensions\12\CONFIG, copy wss_minimaltrust.config and rename it wss_webpartbintrust.config.


2. In our new CAS policy file, wss_webpartbintrust.config, we have three changes.

a. First, add a reference to SharePointPermission in the <SecurityClasses> section. Add the following:
<SecurityClass Name="SharePointPermission" Description="Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />


b. Next, add a new custom permission set. Find the NamedPermissionSet that is named SPRestricted and copy and paste this below it:
<PermissionSet class="NamedPermissionSet" version="1" Name="WebPartBinTrust">
<IPermission class="AspNetHostingPermission" version="1" Level="Minimal" />
<IPermission class="SecurityPermission" version="1" Flags="Execution" />
<IPermission class="WebPartPermission" version="1" Connections="True" />
<IPermission class="SharePointPermission" version="1" ObjectModel="True" />
</PermissionSet>


c. Finally, add the code group. This will indicate when the permissions are to be set. It should be noted this can be done one of two ways, either using a strong name membership (assuming your assembly has been strongnamed) or a url membership, which would give you the option of having the permissions apply to all assemblies in the specified bin or the specific assembly itself.

Under <CodeGroup class="FirstMatchCodeGroup"...> you will see several <CodeGroup class="UnionCodeGroup"...> tags. Place the following code as the first one of these.
<CodeGroup class="UnionCodeGroup" version="1" PermissionSetName="WebPartBinTrust">
<IMembershipCondition class="StrongNameMembershipCondition" version="1" PublicKeyBlob="00240000048000009400000006020000002400005253413100040000010001000DAF8ED8D945CD2ABB2EE7953A6039B791A725F11B4588AC6D70B3E0648F955E9ED4C3C43CB044B8B0E8A6FF4D4FFBE9E3B9297D45F688A7264534E12414E17539305207EC961DA94DF294E7722CCD9BDBFC95A896E996F57156705D281EC39280BD604E87724556AF5807D146963F19F5B43DB69E1F22695463153A553260D2" Name="ContactUsWebPart" />
</CodeGroup>
(Note: The PublicKeyBlob must be the public key blob of your assembly. You may see the PublicKeyBlob in another tag referencing your assembly, in which case, just copy and paste it. Otherwise, you will need to run the command: secutil -hex -s contactuswebpart.dll > publickeyblob.txt, then copy and paste it from the output file publickeyblob.txt.)


3. Almost there... Just two more changes, but this time in your site's web.config. Go to c:\inetpub\wwwroot\wss\VirtualDirectories\<your site>\web.config.

a. In the SecurityPolicy tag, make sure there is an entry for a custom TrustLevel that points to the policy file we just edited:
<trustLevel name="WSS_Custom" policyFile="C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\config\wss_webpartbintrust.config" />

b. Scroll down a bit and make sure the trust level is set like this:
<trust level="WSS_Custom" originUrl="" />


That's it! Restart IIS and have fun!


Some good pages that helped me figure this all out:

WSPBuilder - Walkthrough of the Visual Studio Add-in

MOSS 2007 and Code Access Security

Gaurav Taneja article on bin and GAC deployment

Deploy dlls to the webApplications bin rather than GAC

Code Access Security in SharePoint 2007 for Administrators

Sunday, July 12, 2009

How to create connected web parts: Part 2

Here in Part 2, you will find my tutorial on how to create Contact List/Email Contact connected web parts, along with entire code to be copy and pasted. I recommend following the tutorial, the at the end I will describe what's happening with respect to the transfer of a selected person from one web part to the other.

At a high level:

In the following project, we will create two web parts, a web part called Contact List that will display data from a custom list also called Contact List; and another web part called Email Contact, which will 1. save the message a user types into its form to a custom list called Sent Emails, then 2. email that message to the selected contact person. When a user clicks a contact in the Contact List web part, the person will then appear at the top of the Email Contact web part above the form the user can enter a message into. This transfer of a selected person from the Contact List web part to the Email Contact web part is what requires the web parts to be connected web parts. This connected web part setup also allows us the flexibility of displaying the Contact List by itself, or pairing it with other consumer web parts we develop down the road.

In summary:
  1. Contact List web part requires a custom list called Contact List. Contact List web part will be the provider web part, providing a person to a consumer web part, in this example it will be the Email Contact web part.
  2. Email Contact web part requires a custom list called Sent Emails. Email Contact web part will be the consumer, consuming a person from a provider web part, in this example it will be the Contact List web part.
  3. The two web parts will both use the same interface, we'll call it IContact.
  4. Once both web parts are built and deployed, they can then be individually added to a page on your SharePoint site, then you will connect them by clicking either web part -> Modify Shared Web Part -> Connections
Ok, so that was the all the high-level stuff. Let's do this step by step from the beginning. All you have to do is click along and paste code.

I. First, the easy part - we have to create the two custom lists Contact List
and Sent Emails in your SharePoint site.
  1. In your SharePoint site, click Site Actions -> Create -> Custom List
  2. for Name, type Contact List
  3. click Create
  4. click Contact List in the left menu
  5. click Settings -> List Settings
  6. click Title column, rename to Contact Name, click OK
  7. click Create Column, for Column name type Job Title, click OK
  8. click Create Column, for Column name type Email Address, click OK
  9. click Create Column, for Column name type Phone, click OK
  10. click Create Column, for Column name type Fax, click OK
  11. Now that you have created the contact list, enter in 2 or 3 contacts by clicking Contact List, then New -> New Item, then fill out ALL the fields (including the phone and fax) and click OK (I still need to add error handling on the web part if a value is empty)
  12. In your SharePoint site, click Site Actions -> Create -> Custom List
  13. for Name, type Sent Emails
  14. click Create
  15. click Sent Emails in the left menu
  16. click Settings -> List Settings
  17. click Title column, and rename to Message Subject, click OK
  18. click Create Column, for Column name type Submitter Name, click OK
  19. click Create Column, for Column name type Submitter Email, click OK
  20. click Create Column, for Column name type Message, click OK
  21. click Create Column, for Column name type Contact Email, click OK

II. Now, we create the interface and web parts in Visual Studio 2008.
  1. load Visual Studio 2008
  2. File -> New -> Project -> Visual Basic -> SharePoint -> Web Part
  3. for Name and Solution Name, type: ContactUs
  4. Click OK
  5. In Solution Explorer, right-click the project name ContactUs, then click Properties
  6. Click the Debug tab and in "Start browser with URL:", type the SharePoint site you want these web parts to be deployed to
  7. Click X to close out of Properties
  8. In Solution Explorer, right-click WebPart1 and click Delete, then OK
  9. In Solution Explorer, right-click project ContactUs, then click Add -> New Item -> SharePoint -> Web Part and for the name type ContactList, then click Add
  10. In Solution Explorer, right-click project ContactUs, then click Add -> New Item -> SharePoint -> Web Part and for the name type ContactList, then click Add
  11. In Solution Explorer, right-click project ContactUs, then click Add -> New Item -> Code -> Interface and for the name type IContact.vb, then click Add
  12. In IContact.vb, copy and paste the following code:

    Property ContactName() As String
    Property JobTitle() As String
    Property EmailAddress() As String
    Property Phone() As String
    Property Fax() As String

  13. In Solution Explorer, right-click References, then click Add Reference; in the .NET tab, select System.Drawing and click OK
  14. In ContactList.vb, comment out the first two lines, Option Explicit On and Option Strict On
  15. In ContactList.vb, add the following code directly below Public Class ContactList (delete the other pre-generated code within the class):

    Inherits System.Web.UI.WebControls.WebParts.WebPart
    Implements IContact

    WithEvents oContactNameLink As LinkButton
    Private ContactNameToEmail As String = String.Empty
    Private JobTitleToEmail As String = String.Empty
    Private EmailAddressToEmail As String = String.Empty
    Private PhoneToEmail As String = String.Empty
    Private FaxToEmail As String = String.Empty


    Dim strContactListDataSource As String = ""

    <WebBrowsable(True), Personalizable(True), FriendlyName("Contact List Data Source"), SPWebCategoryName("Data")> _
    Property ContactListDataSource() As String
    Get
    Return strContactListDataSource
    End Get
    Set(ByVal value As String)
    strContactListDataSource = value
    End Set
    End Property


    Public Sub New()
    End Sub


    <Personalizable()> _
    Public Property ContactName() As String _
    Implements IContact.ContactName

    Get
    Return ContactNameToEmail
    End Get
    Set(ByVal value As String)
    ContactNameToEmail = value
    End Set
    End Property


    <Personalizable()> _
    Public Property JobTitle() As String _
    Implements IContact.JobTitle

    Get
    Return JobTitleToEmail
    End Get
    Set(ByVal value As String)
    JobTitleToEmail = value
    End Set
    End Property


    <Personalizable()> _
    Public Property EmailAddress() As String _
    Implements IContact.EmailAddress

    Get
    Return EmailAddressToEmail
    End Get
    Set(ByVal value As String)
    EmailAddressToEmail = value
    End Set
    End Property


    <Personalizable()> _
    Public Property Phone() As String _
    Implements IContact.Phone

    Get
    Return PhoneToEmail
    End Get
    Set(ByVal value As String)
    PhoneToEmail = value
    End Set
    End Property


    <Personalizable()> _
    Public Property Fax() As String _
    Implements IContact.Fax

    Get
    Return FaxToEmail
    End Get
    Set(ByVal value As String)
    FaxToEmail = value
    End Set
    End Property



    ' This is the callback method that returns the provider.
    <ConnectionProvider("Contact Provider", "ContactProvider")> _
    Public Function ProvideIContact() As IContact
    Return Me
    End Function


    Protected Overrides Sub CreateChildControls()
    MyBase.CreateChildControls()


    'if ContactListSource is a valid list, display controls
    If ContactListIsValid() Then

    Try
    'get the site the web part is running on
    Dim oWeb As SPWeb
    oWeb = Microsoft.SharePoint.WebControls.SPControl.GetContextWeb(Context)

    'get the list we are going to work with
    Dim oList As SPList
    oList = oWeb.Lists(ContactListDataSource)


    'create table object
    Dim oTable As New Table
    'oTable.Width = 100
    oTable.CellPadding = 0
    oTable.CellSpacing = 0


    'add the table row
    Dim oRow As New TableRow
    oTable.Rows.Add(oRow)


    'add the cells
    Dim oCellHeaderName As New TableCell
    oCellHeaderName.Width = 180
    oCellHeaderName.Text = "Name"
    oRow.Cells.Add(oCellHeaderName)

    Dim oCellHeaderJobTitle As New TableCell
    oCellHeaderJobTitle.Width = 180
    oCellHeaderJobTitle.Text = "Job Title"
    oRow.Cells.Add(oCellHeaderJobTitle)

    Dim oCellHeaderEmail As New TableCell
    oCellHeaderEmail.Width = 180
    oCellHeaderEmail.Text = "Email Address"
    oRow.Cells.Add(oCellHeaderEmail)

    Dim oCellHeaderPhone As New TableCell
    oCellHeaderPhone.Width = 100
    oCellHeaderPhone.Text = "Phone"
    oRow.Cells.Add(oCellHeaderPhone)

    Dim oCellHeaderFax As New TableCell
    oCellHeaderFax.Width = 100
    oCellHeaderFax.Text = "Fax"
    oRow.Cells.Add(oCellHeaderFax)


    'for all rows in list, display them
    For Each oContactListItem As SPListItem In oList.Items
    'add the table row for the new list item
    Dim oContactRow As New TableRow
    oTable.Rows.Add(oContactRow)


    Dim strPhone As String = ""
    If Not oContactListItem.Item("Phone") Is Nothing Then
    strPhone = oContactListItem.Item("Phone").ToString
    End If

    Dim strFax As String = ""
    If Not oContactListItem.Item("Phone") Is Nothing Then
    strFax = oContactListItem.Item("Fax").ToString
    End If



    'add the cells
    Dim oCellName As New TableCell
    oContactNameLink = New LinkButton
    oContactNameLink.Text = oContactListItem.Item("Contact Name").ToString
    oContactNameLink.CommandArgument = oContactListItem.Item("Job Title").ToString & "|" & _
    oContactListItem.Item("Email Address").ToString & "|" & _
    strPhone & "|" & _
    strFax & "|"
    AddHandler oContactNameLink.Click, AddressOf ContactSelected
    oCellName.Controls.Add(oContactNameLink)
    oContactRow.Cells.Add(oCellName)


    Dim oCellJobTitle As New TableCell
    oCellJobTitle.Text = oContactListItem.Item("Job Title").ToString
    oContactRow.Cells.Add(oCellJobTitle)

    Dim oCellEmailAddress As New TableCell
    oCellEmailAddress.Text = oContactListItem.Item("Email Address").ToString
    oContactRow.Cells.Add(oCellEmailAddress)

    Dim oCellPhone As New TableCell
    If Not oContactListItem.Item("Phone") Is Nothing Then
    oCellPhone.Text = oContactListItem.Item("Phone").ToString
    Else
    oCellPhone.Text = ""
    End If
    oContactRow.Cells.Add(oCellPhone)

    Dim oCellFax As New TableCell
    If Not oContactListItem.Item("Fax") Is Nothing Then
    oCellFax.Text = oContactListItem.Item("Fax").ToString
    Else
    oCellFax.Text = ""
    End If

    oContactRow.Cells.Add(oCellFax)

    Next

    'add the table to the web part controls collection
    Controls.Add(oTable)

    Catch ex As Exception
    Dim lblErrorMessage As New Label
    lblErrorMessage.ForeColor = Drawing.Color.Red
    lblErrorMessage.Text = "Error: " & ex.Message & _
    "<BR><BR>" & "Details: " & ex.ToString
    Controls.Add(lblErrorMessage)

    End Try

    Else
    'else display message telling user they must configure the ContactListDataSource property
    Dim lblConfigureMessage As New Label
    lblConfigureMessage.ForeColor = Drawing.Color.Red
    lblConfigureMessage.Text = "<BR><BR>" & "You must configure the Contact List Data Source property. " & _
    "<BR><BR>" & "Please click Modify Shared Web Part -> Data -> Contact List Data Source " & _
    "and set it to a list based on the ContactListTemplate list template."
    Controls.Add(lblConfigureMessage)
    End If

    End Sub


    Private Sub ContactSelected(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles oContactNameLink.Click
    'when a contact is selected, assign their info to local variables

    Dim ContactFields As String
    Dim ContactFieldsArray(4) As String

    'assign person's name to local variable ContactNameToEmail
    ContactNameToEmail = CType(sender, LinkButton).Text

    'pull contact fields from button's CommandArgument property
    ContactFields = CType(sender, LinkButton).CommandArgument

    'split contact fields into an array
    ContactFieldsArray = ContactFields.Split("|")

    'assign contact fields to local variables
    JobTitleToEmail = ContactFieldsArray(0)
    EmailAddressToEmail = ContactFieldsArray(1)
    PhoneToEmail = ContactFieldsArray(2)
    FaxToEmail = ContactFieldsArray(3)

    End Sub


    Private Function ContactListIsValid() As Boolean
    'PURPOSE: returns true if specified contact list is valid for the web part

    Try
    'get the site the web part is running on
    Dim oWeb As SPWeb
    oWeb = Microsoft.SharePoint.WebControls.SPControl.GetContextWeb(Context)

    'get the list specified by user in property, if list is not found in site, flow will hit Catch
    Dim oList As SPList
    oList = oWeb.Lists(ContactListDataSource)


    'check if fields exist in list

    Dim blnContactNameFound As Boolean = False
    Dim blnJobTitleFound As Boolean = False
    Dim blnEmailAddressFound As Boolean = False
    Dim blnPhoneFound As Boolean = False
    Dim blnFaxFound As Boolean = False

    'loop through the fields in the list and check to see if all the required fields are in the list
    For Each oField As SPField In oList.Fields
    If Not oField.Hidden And Not oField.ReadOnlyField And oField.Type <> SPFieldType.Attachments Then

    If oField.Title = "Contact Name" Then
    blnContactNameFound = True
    End If

    If oField.Title = "Job Title" Then
    blnJobTitleFound = True
    End If

    If oField.Title = "Email Address" Then
    blnEmailAddressFound = True
    End If

    If oField.Title = "Phone" Then
    blnPhoneFound = True
    End If

    If oField.Title = "Fax" Then
    blnFaxFound = True
    End If

    End If
    Next

    'if all fields were found, return true
    If blnContactNameFound And blnJobTitleFound And blnEmailAddressFound And blnPhoneFound And blnFaxFound Then
    Return True
    Else
    Return False
    End If

    Catch ex As Exception
    Return False

    End Try

    End Function

  16. In EmailContact.vb, add the following code below the Imports statements at the top of the file:

    Imports System.Net.Mail

  17. In EmailContact.vb, add the following code below Public Class EmailContact (delete the other pre-generated code within the class):

    Inherits System.Web.UI.WebControls.WebParts.WebPart

    Private _provider As IContact
    Private _contactName As String
    Private _jobTitle As String
    Private _emailAddress As String
    Private _phone As String
    Private _fax As String

    Private lblContactNameValue As Label
    Private lblJobTitleValue As Label
    Private lblEmailAddressValue As Label
    Private lblPhoneValue As Label
    Private lblFaxValue As Label

    Dim oMessageSubject As TextBox
    Dim oSubmitterName As TextBox
    Dim oSubmitterEmail As TextBox
    Dim oMessage As TextBox

    Dim lblStatusMessage As New Label

    WithEvents btnSave As New Button


    Dim strEmailContactDestination As String = ""

    <WebBrowsable(True), Personalizable(True), FriendlyName("Email Contact Destination"), SPWebCategoryName("Data")> _
    Property EmailContactDestination() As String
    Get
    Return strEmailContactDestination
    End Get
    Set(ByVal value As String)
    strEmailContactDestination = value
    End Set
    End Property


    Dim strSMTPMailHost As String = ""

    <WebBrowsable(True), Personalizable(True), FriendlyName("SMTP Mail Host"), SPWebCategoryName("Data")> _
    Property SMTPMailHost() As String
    Get
    Return strSMTPMailHost
    End Get
    Set(ByVal value As String)
    strSMTPMailHost = value
    End Set
    End Property


    'Public Sub New()
    'End Sub


    ' This method is identified by the ConnectionConsumer
    ' attribute, and is the mechanism for connecting with
    ' the provider.
    <ConnectionConsumer("Contact Consumer", "ContactConsumer")> _
    Public Sub GetIContact(ByVal Provider As IContact)
    _provider = Provider
    End Sub


    Protected Overrides Sub CreateChildControls()
    MyBase.CreateChildControls()

    'if EmailContactDestination is a valid list, display controls
    If EmailContactDestinationIsValid() Then
    If SMTPMailHostIsValid() Then
    'create table object
    Dim oTable As New Table
    oTable.CellPadding = 0
    oTable.CellSpacing = 0

    'create a row
    Dim oContactNameRow As New TableRow
    oTable.Rows.Add(oContactNameRow)

    'create the cells and add them to the row
    Dim oContactNameLabelCell As New TableCell
    oContactNameRow.Cells.Add(oContactNameLabelCell)
    Dim oContactNameValueCell As New TableCell
    oContactNameRow.Cells.Add(oContactNameValueCell)

    oContactNameLabelCell.Width = 125
    oContactNameValueCell.Width = 350

    'create label objects for field name and field value
    Dim lblContactNameHeader As New Label
    lblContactNameHeader.Text = "Contact Name"
    lblContactNameValue = New Label
    lblContactNameValue.Text = ""

    'add the controls to the cells
    oContactNameLabelCell.Controls.Add(lblContactNameHeader)
    oContactNameValueCell.Controls.Add(lblContactNameValue)



    'create a row
    Dim oJobTitleRow As New TableRow
    oTable.Rows.Add(oJobTitleRow)

    'create the cells and add them to the row
    Dim oJobTitleLabelCell As New TableCell
    oJobTitleRow.Cells.Add(oJobTitleLabelCell)
    Dim oJobTitleValueCell As New TableCell
    oJobTitleRow.Cells.Add(oJobTitleValueCell)

    'create label objects for field name and field value
    Dim lblJobTitleHeader As New Label
    lblJobTitleHeader.Text = "Job Title"
    lblJobTitleValue = New Label
    lblJobTitleValue.Text = ""

    'add the controls to the cells
    oJobTitleLabelCell.Controls.Add(lblJobTitleHeader)
    oJobTitleValueCell.Controls.Add(lblJobTitleValue)



    'create a row
    Dim oEmailAddressRow As New TableRow
    oTable.Rows.Add(oEmailAddressRow)

    'create the cells and add them to the row
    Dim oEmailAddressLabelCell As New TableCell
    oEmailAddressRow.Cells.Add(oEmailAddressLabelCell)
    Dim oEmailAddressValueCell As New TableCell
    oEmailAddressRow.Cells.Add(oEmailAddressValueCell)

    'create label objects for field name and field value
    Dim lblEmailAddressHeader As New Label
    lblEmailAddressHeader.Text = "Email Address"
    lblEmailAddressValue = New Label
    lblEmailAddressValue.Text = ""

    'add the controls to the cells
    oEmailAddressLabelCell.Controls.Add(lblEmailAddressHeader)
    oEmailAddressValueCell.Controls.Add(lblEmailAddressValue)



    'create a row
    Dim oPhoneRow As New TableRow
    oTable.Rows.Add(oPhoneRow)

    'create the cells and add them to the row
    Dim oPhoneLabelCell As New TableCell
    oPhoneRow.Cells.Add(oPhoneLabelCell)
    Dim oPhoneValueCell As New TableCell
    oPhoneRow.Cells.Add(oPhoneValueCell)

    'create label objects for field name and field value
    Dim lblPhoneHeader As New Label
    lblPhoneHeader.Text = "Phone"
    lblPhoneValue = New Label
    lblPhoneValue.Text = ""

    'add the controls to the cells
    oPhoneLabelCell.Controls.Add(lblPhoneHeader)
    oPhoneValueCell.Controls.Add(lblPhoneValue)



    'create a row
    Dim oFaxRow As New TableRow
    oTable.Rows.Add(oFaxRow)

    'create the cells and add them to the row
    Dim oFaxLabelCell As New TableCell
    oFaxRow.Cells.Add(oFaxLabelCell)
    Dim oFaxValueCell As New TableCell
    oFaxRow.Cells.Add(oFaxValueCell)

    'create label objects for field name and field value
    Dim lblFaxHeader As New Label
    lblFaxHeader.Text = "Fax"
    lblFaxValue = New Label
    lblFaxValue.Text = ""

    'add the controls to the cells
    oFaxLabelCell.Controls.Add(lblFaxHeader)
    oFaxValueCell.Controls.Add(lblFaxValue)



    'build form to allow user to submit comments and have them emailed to selected contact

    'create a row
    Dim oMessageSubjectRow As New TableRow
    oTable.Rows.Add(oMessageSubjectRow)

    'create the cells and add them to the row
    Dim oMessageSubjectLabelCell As New TableCell
    oMessageSubjectRow.Cells.Add(oMessageSubjectLabelCell)
    Dim oMessageSubjectValueCell As New TableCell
    oMessageSubjectRow.Cells.Add(oMessageSubjectValueCell)

    'create the field label control
    Dim lblMessageSubject As New Label
    lblMessageSubject.Text = "Message Subject"

    'create the form field control
    oMessageSubject = New TextBox
    oMessageSubject.Width = 300

    'create validator
    Dim oMessageSubjectRequired As New RequiredFieldValidator

    'add controls to the cells
    oMessageSubjectLabelCell.Controls.Add(lblMessageSubject)
    oMessageSubjectValueCell.Controls.Add(oMessageSubject)
    oMessageSubjectValueCell.Controls.Add(oMessageSubjectRequired)

    'configure validator
    oMessageSubjectRequired.ErrorMessage = " * required"
    oMessageSubject.ID = "oMessageSubject"
    oMessageSubjectRequired.Display = ValidatorDisplay.Dynamic
    oMessageSubjectRequired.ControlToValidate = oMessageSubject.ID
    oMessageSubjectRequired.ValidationGroup = "EmailContactValidationGroup"





    'create a row
    Dim oSubmitterNameRow As New TableRow
    oTable.Rows.Add(oSubmitterNameRow)

    'create the cells and add them to the row
    Dim oSubmitterNameLabelCell As New TableCell
    oSubmitterNameRow.Cells.Add(oSubmitterNameLabelCell)
    Dim oSubmitterNameValueCell As New TableCell
    oSubmitterNameRow.Cells.Add(oSubmitterNameValueCell)

    'create the field label control
    Dim lblSubmitterName As New Label
    lblSubmitterName.Text = "Submitter Name"

    'create the form field control
    oSubmitterName = New TextBox
    oSubmitterName.Width = 300

    'create validator
    Dim oSubmitterNameRequired As New RequiredFieldValidator

    'add controls to the cells
    oSubmitterNameLabelCell.Controls.Add(lblSubmitterName)
    oSubmitterNameValueCell.Controls.Add(oSubmitterName)
    oSubmitterNameValueCell.Controls.Add(oSubmitterNameRequired)

    'configure validator
    oSubmitterNameRequired.ErrorMessage = " * required"
    oSubmitterName.ID = "oSubmitterName"
    oSubmitterNameRequired.Display = ValidatorDisplay.Dynamic
    oSubmitterNameRequired.ControlToValidate = oSubmitterName.ID
    oSubmitterNameRequired.ValidationGroup = "EmailContactValidationGroup"





    'create a row
    Dim oSubmitterEmailRow As New TableRow
    oTable.Rows.Add(oSubmitterEmailRow)

    'create the cells and add them to the row
    Dim oSubmitterEmailLabelCell As New TableCell
    oSubmitterEmailRow.Cells.Add(oSubmitterEmailLabelCell)
    Dim oSubmitterEmailValueCell As New TableCell
    oSubmitterEmailRow.Cells.Add(oSubmitterEmailValueCell)

    'create the field label control
    Dim lblSubmitterEmail As New Label
    lblSubmitterEmail.Text = "Submitter Email"

    'create the form field control
    oSubmitterEmail = New TextBox
    oSubmitterEmail.Width = 300

    'create validator
    Dim oSubmitterEmailRequired As New RequiredFieldValidator
    Dim oSubmitterEmailValid As New RegularExpressionValidator

    'add controls to the cells
    oSubmitterEmailLabelCell.Controls.Add(lblSubmitterEmail)
    oSubmitterEmailValueCell.Controls.Add(oSubmitterEmail)
    oSubmitterEmailValueCell.Controls.Add(oSubmitterEmailRequired)
    oSubmitterEmailValueCell.Controls.Add(oSubmitterEmailValid)

    'configure validator
    oSubmitterEmailRequired.ErrorMessage = " * required"
    oSubmitterEmail.ID = "oSubmitterEmail"
    oSubmitterEmailRequired.Display = ValidatorDisplay.Dynamic
    oSubmitterEmailRequired.ControlToValidate = oSubmitterEmail.ID
    oSubmitterEmailRequired.ValidationGroup = "EmailContactValidationGroup"

    'configure validator
    oSubmitterEmailValid.ErrorMessage = " * Not a valid email address"
    oSubmitterEmailValid.ControlToValidate = oSubmitterEmail.ID
    oSubmitterEmailValid.Display = ValidatorDisplay.Dynamic
    oSubmitterEmailValid.ValidationExpression = "\w+\w*\@\w+\w+\w*\.(com|edu|org|gov|net|corp)"
    oSubmitterEmailValid.ValidationGroup = "EmailContactValidationGroup"




    'create a row
    Dim oMessageRow As New TableRow
    oTable.Rows.Add(oMessageRow)

    'create the cells and add them to the row
    Dim oMessageLabelCell As New TableCell
    oMessageRow.Cells.Add(oMessageLabelCell)
    Dim oMessageValueCell As New TableCell
    oMessageRow.Cells.Add(oMessageValueCell)

    'create the field label control
    Dim lblMessage As New Label
    lblMessage.Text = "Message"

    'create the form field control
    oMessage = New TextBox
    oMessage.Width = 300
    oMessage.TextMode = TextBoxMode.MultiLine
    oMessage.Rows = 4

    'create validator
    Dim oMessageRequired As New RequiredFieldValidator

    'add controls to the cells
    oMessageLabelCell.Controls.Add(lblMessage)
    oMessageValueCell.Controls.Add(oMessage)
    oMessageValueCell.Controls.Add(oMessageRequired)

    'configure validator
    oMessageRequired.ErrorMessage = " * required"
    oMessage.ID = "oMessage"
    oMessageRequired.Display = ValidatorDisplay.Dynamic
    oMessageRequired.ControlToValidate = oMessage.ID
    oMessageRequired.ValidationGroup = "EmailContactValidationGroup"




    'set the text for the save button
    btnSave.Text = "Send"
    btnSave.Enabled = False
    btnSave.ValidationGroup = "EmailContactValidationGroup"

    'create the row for the save button
    Dim oRowButton As New TableRow
    oTable.Rows.Add(oRowButton)

    'create the cell for the save button
    Dim oCellButton As New TableCell
    oCellButton.ColumnSpan = 2
    oCellButton.HorizontalAlign = Web.UI.WebControls.HorizontalAlign.Right
    oRowButton.Cells.Add(oCellButton)

    'add save button to cell
    oCellButton.Controls.Add(btnSave)



    'create the row for the status message
    Dim oRowStatus As New TableRow
    oTable.Rows.Add(oRowStatus)

    'create the cell for the status message
    Dim oCellStatus As New TableCell
    oCellStatus.ColumnSpan = 2
    oCellStatus.HorizontalAlign = Web.UI.WebControls.HorizontalAlign.Center
    oRowStatus.Cells.Add(oCellStatus)

    'add status message to cell
    lblStatusMessage.ForeColor = Drawing.Color.Green
    oCellStatus.Controls.Add(lblStatusMessage)



    'add the table to the web part controls collection
    Controls.Add(oTable)
    Else
    'else display message telling user they must configure the SMTPMailHost property
    Dim lblConfigureMessage As New Label
    lblConfigureMessage.ForeColor = Drawing.Color.Red
    lblConfigureMessage.Text = "<BR><BR>" & "You must configure the SMTP Mail Host property. " & _
    "<BR><BR>" & "Please click Modify Shared Web Part -> Data -> SMTP Mail Host " & _
    "and set it to a valid SMTP mail host."
    Controls.Add(lblConfigureMessage)
    End If

    Else
    'else display message telling user they must configure the EmailContactDestination property
    Dim lblConfigureMessage As New Label
    lblConfigureMessage.ForeColor = Drawing.Color.Red
    lblConfigureMessage.Text = "<BR><BR>" & "You must configure the Email Contact Destination property. " & _
    "<BR><BR>" & "Please click Modify Shared Web Part -> Data -> Email Contact Destination " & _
    "and set it to a list based on the SentEmailsTemplate list template."
    Controls.Add(lblConfigureMessage)
    End If

    End Sub


    Protected Overrides Sub OnPreRender(ByVal e As EventArgs)
    EnsureChildControls()

    If Not (Me._provider Is Nothing) Then
    'assign contact values to local variables
    _contactName = _provider.ContactName.Trim()
    _jobTitle = _provider.JobTitle.Trim()
    _emailAddress = _provider.EmailAddress.Trim()
    _phone = _provider.Phone.Trim()
    _fax = _provider.Fax.Trim()

    'assign contact values to labels on form
    If _contactName <> "" Then
    lblContactNameValue.Text = _contactName
    End If

    If _jobTitle <> "" Then
    lblJobTitleValue.Text = _jobTitle
    End If

    If _emailAddress <> "" Then
    lblEmailAddressValue.Text = _emailAddress
    End If

    If _phone <> "" Then
    lblPhoneValue.Text = _phone
    End If

    If _fax <> "" Then
    lblFaxValue.Text = _fax
    End If


    'enable save button if there is a value passed in otherwise disable button
    If _contactName = "" Then
    btnSave.Enabled = False

    'clear form
    ClearForm()
    Else
    btnSave.Enabled = True

    'clear status message
    lblStatusMessage.Text = ""
    End If

    End If

    End Sub 'OnPreRender


    Sub SaveAndEmail() Handles btnSave.Click
    'save message to the specified list library
    SaveMessage()

    End Sub


    Private Sub SaveMessage()
    'PURPOSE: save values from form into list library ContactUs

    Try
    'get the site the web part is running on
    Dim oWeb As SPWeb
    oWeb = Microsoft.SharePoint.WebControls.SPControl.GetContextWeb(Context)

    'get the list where we want to save the form values to
    Dim oList As SPList
    oList = oWeb.Lists(EmailContactDestination)

    'create a new list item
    Dim oNewItem As SPListItem
    oNewItem = oList.Items.Add()

    'populate the new list item with values from the form
    oNewItem.Item("Message Subject") = oMessageSubject.Text
    oNewItem.Item("Submitter Name") = oSubmitterName.Text
    oNewItem.Item("Submitter Email") = oSubmitterEmail.Text
    oNewItem.Item("Message") = oMessage.Text
    oNewItem.Item("Contact Email") = lblEmailAddressValue.Text

    'update the list with the new item
    oNewItem.Update()

    'email the message to the designated contact
    EmailMessage()

    Catch ex As Exception
    Dim lblErrorMessage As New Label
    lblErrorMessage.ForeColor = Drawing.Color.Red
    lblErrorMessage.Text = "Error: " & ex.Message & _
    "<BR><BR>" & "Details: " & ex.ToString
    Controls.Add(lblErrorMessage)

    End Try

    End Sub


    Private Sub EmailMessage()
    'PURPOSE: email the contact that was selected

    Dim strFromName As String = oSubmitterName.Text
    Dim strFromEmail As String = oSubmitterEmail.Text
    Dim strSubject As String = oMessageSubject.Text
    Dim strMessage As String = oMessage.Text
    Dim strToName As String = lblContactNameValue.Text
    Dim strToEmail As String = lblEmailAddressValue.Text

    Try
    'build from email address and to email address
    Dim FromEmailAddress As New Net.Mail.MailAddress(strFromEmail, strFromName)
    Dim ToEmailAddress As New Net.Mail.MailAddress(strToEmail, strToName)

    'create email and assign its subject and body
    Dim MailMsg As New MailMessage(FromEmailAddress, ToEmailAddress)
    MailMsg.Subject = strSubject
    MailMsg.Body = strMessage

    'create smtp email object
    Dim smtpmail As New SmtpClient

    'set email's smtp server
    smtpmail.Host = SMTPMailHost

    'send email
    smtpmail.Send(MailMsg)

    'clear form
    ClearForm()

    'display success message
    lblStatusMessage.Text = "Message Sent Successfully!"

    Catch ex As Exception
    Dim lblErrorMessage As New Label
    lblErrorMessage.ForeColor = Drawing.Color.Red
    lblErrorMessage.Text = "Error: " & ex.Message & _
    "<BR><BR>" & "Details: " & ex.ToString
    Controls.Add(lblErrorMessage)

    End Try
    End Sub


    Private Sub ClearForm()
    'PURPOSE: clear form of all values

    'clear selected contact person's info
    lblContactNameValue.Text = ""
    lblJobTitleValue.Text = ""
    lblEmailAddressValue.Text = ""
    lblPhoneValue.Text = ""
    lblFaxValue.Text = ""

    'clear form fields
    oMessageSubject.Text = ""
    oSubmitterName.Text = ""
    oSubmitterEmail.Text = ""
    oMessage.Text = ""

    End Sub


    Private Function EmailContactDestinationIsValid() As Boolean
    'PURPOSE: returns true if specified email list is valid for the web part

    Try
    'get the site the web part is running on
    Dim oWeb As SPWeb
    oWeb = Microsoft.SharePoint.WebControls.SPControl.GetContextWeb(Context)

    'get the list specified by user in property, if list is not found in site, flow will hit Catch
    Dim oList As SPList
    oList = oWeb.Lists(EmailContactDestination)


    'check if fields exist in list

    Dim blnMessageSubjectFound As Boolean = False
    Dim blnSubmitterName As Boolean = False
    Dim blnSubmitterEmail As Boolean = False
    Dim blnMessage As Boolean = False
    Dim blnContactEmail As Boolean = False

    'loop through the fields in the list and check to see if all the required fields are in the list
    For Each oField As SPField In oList.Fields
    If Not oField.Hidden And Not oField.ReadOnlyField And oField.Type <> SPFieldType.Attachments Then

    If oField.Title = "Message Subject" Then
    blnMessageSubjectFound = True
    End If

    If oField.Title = "Submitter Name" Then
    blnSubmitterName = True
    End If

    If oField.Title = "Submitter Email" Then
    blnSubmitterEmail = True
    End If

    If oField.Title = "Message" Then
    blnMessage = True
    End If

    If oField.Title = "Contact Email" Then
    blnContactEmail = True
    End If

    End If
    Next

    'if all fields were found, return true
    If blnMessageSubjectFound And blnSubmitterName And blnSubmitterEmail And blnMessage And blnContactEmail Then
    Return True
    Else
    Return False
    End If

    Catch ex As Exception
    Return False

    End Try

    End Function


    Private Function SMTPMailHostIsValid() As Boolean
    'PURPOSE: returns true if SMTP Mail Host is valid

    Try
    If SMTPMailHost <> "" Then
    Return True
    Else
    Return False
    End If

    Catch ex As Exception
    Return False

    End Try

    End Function

III. Finally, we deploy the web parts to the SharePoint site, add them to a page, configure them, connect them to each other, then test them!
  1. in Visual Studio 2008, hit F5 (this should build and deploy the web parts to your site)
  2. in your SharePoint site, select the page you want the web parts on, then click Site Actions -> Edit Page
  3. click Add A Web Part
  4. scroll down to the Miscellaneous section, select ContactList web part and EmailContact web part, then click Add
  5. In the ContactList Web Part, click Edit -> Modify Shared Web Part -> Data, and in Contact List Data Source, type our custom contact list we created, Contact List and hit OK. You should see the people you entered into the list appear in the web part.
  6. In the EmailContact Web Part, click Edit -> Modify Shared Web Part -> Data, and in Email Contact Destination, type our custom email list we created, Sent Emails and hit OK. Next, enter your SMTP Mail Host in the next textbox. If you don't have your server configured for this, just type anything as the validation function in the code just expects any value. If you put in something that isn't your host, the web part just won't be able to email. You can customize the validation code yourself to indicate what you expect the user to enter.
  7. click the Email Contact web part and click edit -> Connections -> Get Contact Consumer From -> Contact List (you could also have set the connection up from the Contact List web part as well)
  8. Click Exit Edit Mode
  9. Test the connected web parts by clicking a person in the Contact List and watching them appear in the Email Contact web part! In the Email Contact web part, enter in values and hit Send. This will save the message in the Sent Emails list. If you set up the SMTP mail host properly and the selected person has a proper email address, then that person will also be emailed the message you entered.
So there you have it! Now, exactly what is happening? In the ContactList web part, we created properties that implemented the interface IContact. The ConnectionProvider function ProvideIContact is what returns the interface. Originally, all the properties are empty, so this function won't have any values. When a contact is clicked, the ContactSelected routine fires off and populates the properties, so now ProvideIContact can return something. Now, the EmailContact web part has a ConnectionConsumer routine GetIContact that gets the values from ProvideIContact. Essentially, when we created a connection between the two web parts on step 7 above, we created a bridge between them via ContactList's ProvideIContact and EmailContact's GetIContact. As a side note, there is an interface called IWebPartRow, and I believe there would be a way to use that instead of the individual properties I created in IContact, but I went with the properties in IContact because it seemed easier at the time. Well, I hope this worked well. If you followed along and it didn't work, let me know in case I forgot something. However, I am pretty sure this should work 100%!