SYSK 368: SharePoint – Custom List Item Action that Starts a Workflow
First, I must start with the following disclaimer – this was my first SharePoint 2007 project, so, I do not claim any expertise in the subject matter… However, I believe, this post may be of value to some readers…
Let’s say, you want to add a custom action to the context menu in a SharePoint list that fires your custom workflow, e.g.:
First, you need to let SharePoint know about the new action – in this case, Publish Project Plan.
1. You’ll need to create two files – elements.xml and feature.xml:
elements.xml
<?xml version=”1.0” encoding=”utf-8” ?>
<Elements Id=”e73e411e-bac7-44bc-a55f-83a865385a6a” xmlns=”http://schemas.microsoft.com/sharepoint/”>
<CustomAction Id=”PublishAction”
RegistrationType=”List”
RegistrationId=”101”
Location=”EditControlBlock”
Sequence=”5000”
Title=”Publish Project Plan”>
<UrlAction Url=”~site/_layouts/StartWorkflow.aspx?ListId={ListId}&ItemId={ItemId}&WFTemplateID=068591c6-be5a-4b36-8a7c-6fe2c1ae434f” />
</CustomAction>
</Elements>
feature.xml
<?xml version=”1.0” encoding=”utf-8”?>.
<Feature xmlns=”http://schemas.microsoft.com/sharepoint/”
Id=”ee4f8e3d-7956-46f7-bd38-9888cce2f0ab”
Scope=”Web”
Title=”Publish”
Version=”1.0.0.0”
Description=”Publish Project Plan Custom Action”>
<ElementManifests>
<ElementManifest Location=”elements.xml” />
</ElementManifests>
</Feature>
By doing so, you’re telling SharePoint that there is a custom action called PublishAction associated with EditControlBlock (i.e. context sensitive menu that is displayed for a list item)… You tell it what to do when clicked (UrlAction), what to display (Title), the menu item index (sequence), etc.
2. Put those files in a directory, e.g. PublishAction in “C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\FEATURES” folder.
3. The way you “register” and activate this new “feature” with SharePoint 2007 is by invoking the following commands:
"C:\Program Files\common files\microsoft shared\web server extensions\12\bin\stsadm.exe" -o installfeature -filename PublishAction\feature.xml –force
"C:\Program Files\common files\microsoft shared\web server extensions\12\bin\stsadm.exe" -o activatefeature -name PublishAction -url http://yourserver/vdir/
As you can see, the UrlAction tells SharePoint to kick off StartWorkflow.aspx file and pass it some parameters…
4. My file looks as follows:
StartWorkflow.aspx
<%@ Page Language="VB" Inherits="System.Web.UI.Page" EnableViewState="false"%>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SPSWC" Namespace="Microsoft.SharePoint.Portal.WebControls" Assembly="Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<HTML dir="<SharePoint:EncodedLiteral runat='server' text='<%$Resources:wss,multipages_direction_dir_value%>' EncodeMethod='HtmlEncode'/>">
<HEAD>
<title>
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,gear_pagetitle%>" EncodeMethod='HtmlEncode'/>
</title>
<link rel="stylesheet" type="text/css" href="/_layouts/<%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%>/styles/core.css" />
</HEAD>
<BODY>
<form runat="server" id="Form1">
<TABLE class="ms-main" CELLPADDING=0 CELLSPACING=0 BORDER=0 WIDTH="100%" HEIGHT="100%">
<!-- Global navigation -->
<tr><td>
<table CELLPADDING=0 CELLSPACING=0 BORDER=0 WIDTH="100%">
<tr>
<td colspan=4 class="ms-globalbreadcrumb" align="<SharePoint:EncodedLiteral runat='server' text='<%$Resources:wss,multipages_direction_right_align_value%>' EncodeMethod='HtmlEncode'/>">
</td>
</tr>
</table>
</td></tr>
<TR height="100%">
<TD>
<TABLE height="100%" width="100%" cellspacing="0" cellpadding="0">
<tr>
<td class="ms-titleareaframe" id="TitleAreaImageCell" valign="middle" nowrap></td>
<td class="ms-titleareaframe" id="TitleAreaFrameClass">
<table cellpadding=0 height=100% width=100% cellspacing=0>
<tr><td class="ms-areaseparatorleft"><IMG SRC="/_layouts/images/blank.gif" width=1 height=1 alt=""></td></tr>
</table>
</td>
<td valign=top id="onetidPageTitleAreaFrame" class='ms-areaseparator' nowrap>
<table id="onetidPageTitleAreaTable" cellpadding=0 cellspacing=0 border="0">
<tr>
<td valign="top" class="ms-titlearea">
</td>
</tr>
<tr>
<td ID=onetidPageTitle class="ms-pagetitle">
<!-- Page Title -->
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,gear_pagetitle%>" EncodeMethod='HtmlEncode'/>
</td>
</tr>
</table>
</td>
<td><div class='ms-areaseparatorright'><IMG SRC="/_layouts/images/blank.gif" width=8 height=100% alt=""></div></td>
</tr>
<TR>
<TD class="ms-leftareacell" valign=top height=100% id="LeftNavigationAreaCell" >
<table class=ms-nav width=100% height=100% cellpadding=0 cellspacing=0>
<tr>
<td>
<TABLE height="100%" class=ms-navframe CELLPADDING=0 CELLSPACING=0 border="0" >
<tr valign="top">
<td width="4px"><IMG SRC="/_layouts/images/blank.gif" width=4 height=1 alt=""></td>
<td valign="top" width="100%">
</td>
</tr>
<tr><td colspan=2><IMG SRC="/_layouts/images/blank.gif" width=138 height=1 alt=""></td></tr>
</TABLE>
</td>
<td></td>
</tr>
</table>
</TD>
<td>
<div class='ms-areaseparatorleft'><IMG SRC="/_layouts/images/blank.gif" width=8 height=100% alt=""></div>
</td>
<!-- Contents -->
<!-- Layout_Page_Description -->
<td class='ms-formareaframe' width=100% valign="top">
<TABLE width=100% border="0" cellspacing="0" cellpadding="0" class="ms-propertysheet">
<tr>
<td>
<table ID="Table1" cellpadding=0 cellspacing=0 width="100%" height="100%">
<tr>
<td width="100%" height="100%" align="center" valign="middle">
<IMG SRC="/_layouts/images/blank.gif" width=590 height=1 alt="">
<table cellpadding=0 cellspacing=0 width="100%">
<tr>
<td style="padding-top:0px;padding-left: 20px;padding-right:20px;" >
<img alt="<SharePoint:EncodedLiteral runat='server' text='<%$Resources:wss,gear_tooltip%>' EncodeMethod='HtmlEncode'/>" src="/_layouts/images/gears_an.gif" >
</td>
<td width=100%><span class="ms-sectionheader">
<!-- LEADING HTML -->
<SharePoint:EncodedLiteral runat="server" id="MessageDesc" text="<%$Resources:wss,htmltrredir_pleasewait%>" EncodeMethod='HtmlEncode'/>
</span><span class='ms-descriptiontext'>
<!-- TRAILING HTML -->
</span></td></tr>
<TR><TD height=1 colspan=2><IMG SRC="/_layouts/images/blank.gif" width=1 height=8 alt=""></TD></TR>
<TR><TD class=ms-sectionline height=1 colspan=2><IMG SRC="/_layouts/images/blank.gif" width=1 height=1 alt=""></TD></TR>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table>
<TR>
<TD ID=onetidXPadding height="20px"><IMG SRC="/_layouts/images/blank.gif" width=1 height=20 alt=""></TD>
</TR>
</TABLE>
</td>
<td class="ms-rightareacell">
<div class='ms-areaseparatorright'><IMG SRC="/_layouts/images/blank.gif" width=8 height=100% alt=""></div>
</td>
</TR>
</TABLE>
</TD>
</TR>
</TABLE>
<asp:Label id="ErrorLabel" runat="server" EnableViewState="False" class="ms-error" Text="">
<%
' TODO: put all strings into resource file and localize them
If Page.IsPostBack Then
Try
Using web As Microsoft.SharePoint.SPWeb = Microsoft.SharePoint.SPContext.Current.Web
Using site As Microsoft.SharePoint.SPSite = web.Site
Dim list As Microsoft.SharePoint.SPList = web.Lists.Item(New Guid(Request("ListId")))
Dim listItem As Microsoft.SharePoint.SPListItem = list.GetItemById(Request("ItemId"))
If list.WorkflowAssociations.Count > 0 Then
Dim wfAssociation As Microsoft.SharePoint.Workflow.SPWorkflowAssociation = list.WorkflowAssociations.GetAssociationByBaseID(New Guid(Request("WFTemplateID")))
If wfAssociation IsNot Nothing Then
If wfAssociation.Enabled = True Then
Dim wfRunning As Boolean = False
If listItem.Workflows.Count > 0 Then
For Each wf As Microsoft.SharePoint.Workflow.SPWorkflow In listItem.Workflows
If wf.ParentAssociation.BaseTemplate.Id = wfAssociation.BaseTemplate.Id Then
If wf.InternalState = Microsoft.SharePoint.Workflow.SPWorkflowState.Running Then
wfRunning = True
Response.Write("An instance of this workflow is already running")
End If
Exit For
End If
Next
End If
If wfRunning = False Then
web.AllowUnsafeUpdates = True
site.WorkflowManager.StartWorkflow(listItem, wfAssociation, "")
site.WorkflowManager.Dispose()
Response.Redirect(list.DefaultViewUrl)
End If
Else
Response.Write("Workflow association is not enabled")
End If
Else
Response.Write("No workflow with this id found in the list")
End If
Else
Response.Write("No workflow is associated with this list")
End If
End Using
End Using
Catch ex as Exception
Response.Write(ex.ToString())
If ex.InnerException IsNot Nothing Then
Response.Write("<br/></br/>")
Response.Write(ex.InnerException.ToString())
End If
Finally
End Try
Else
ClientScript.RegisterStartupScript(Me.GetType(), "onload", "Form1.submit();", True)
End If
%>
</asp:Label>
</form>
</body>
</html>
Important things to point out (that, unfortunately, I have not seen mentioned in some of the post I’ve come across):
· You workflow must be invoked from an HTTP POST (for security reasons), thus, the ugly (IMHO) workaround
· You need to call Dispose on any disposable SharePoint objects, e.g. site, web, workflow manager… Otherwise, after a few workflow invocations, you’ll start getting errors and will not be able to start another instance of your workflow…
5. StartWorkflow.aspx file should be placed into C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS folder.
The next step is to create and “register” your workflow…
6. Create a WF workflow project and add your workflow logic… Make sure to add Microsoft.SharePoint.WorkflowActions.OnWorkflowActivated shape as the first thing that happens in your workflow. Otherwise, SharePoint will not be successfully invoke it. To get to the SharePoint properties, e.g. the list item that triggered the workflow, etc., use Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties member variable… For example, the skeleton (custom logic removed) of my workflow (written in VB.NET since my customer is a VB shop) looked like this:
Option Explicit On
Option Strict On
Imports System
Imports System.Security.Permissions
Imports System.Runtime.InteropServices
Imports Microsoft.SharePoint.Workflow
Imports Microsoft.SharePoint
Imports System.Data.SqlClient
Public Class PublishProjectPlan
Inherits SequentialWorkflowActivity
Private onWorkflowActivated1 As Microsoft.SharePoint.WorkflowActions.OnWorkflowActivated
#Region "Member variables"
Public workflowProps As Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties _
= New Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties()
Public workflowID As Guid = workflowProps.WorkflowId
Private logToHistoryListActivity1 As Microsoft.SharePoint.WorkflowActions.LogToHistoryListActivity
#End Region
#Region "Ctor"
Public Sub New()
InitializeComponent()
End Sub
#End Region
#Region "InitializeComponent"
Private Sub InitializeComponent()
Me.CanModifyActivities = True
Dim activitybind2 As System.Workflow.ComponentModel.ActivityBind = New System.Workflow.ComponentModel.ActivityBind
Dim correlationtoken1 As System.Workflow.Runtime.CorrelationToken = New System.Workflow.Runtime.CorrelationToken
Dim activitybind1 As System.Workflow.ComponentModel.ActivityBind = New System.Workflow.ComponentModel.ActivityBind
Me.logToHistoryListActivity1 = New Microsoft.SharePoint.WorkflowActions.LogToHistoryListActivity
Me.onWorkflowActivated1 = New Microsoft.SharePoint.WorkflowActions.OnWorkflowActivated
'GetPlanData
'
Me.GetPlanData.Name = "GetPlanData"
AddHandler Me.GetPlanData.ExecuteCode, AddressOf Me.GetPlanData_ExecuteCode
'
'logToHistoryListActivity1
'
Me.logToHistoryListActivity1.Description = "Project Plan Publishing Workflow Started"
Me.logToHistoryListActivity1.Duration = System.TimeSpan.Parse("-10675199.02:48:05.4775808")
Me.logToHistoryListActivity1.EventId = Microsoft.SharePoint.Workflow.SPWorkflowHistoryEventType.None
Me.logToHistoryListActivity1.HistoryDescription = "Project Plan Publishing Workflow Started"
Me.logToHistoryListActivity1.HistoryOutcome = ""
Me.logToHistoryListActivity1.Name = "logToHistoryListActivity1"
Me.logToHistoryListActivity1.OtherData = ""
Me.logToHistoryListActivity1.UserId = -1
activitybind2.Name = "PublishProjectPlan"
activitybind2.Path = "workflowID"
'
'onWorkflowActivated1
'
correlationtoken1.Name = "workflowToken"
correlationtoken1.OwnerActivityName = "PublishProjectPlan"
Me.onWorkflowActivated1.CorrelationToken = correlationtoken1
Me.onWorkflowActivated1.EventName = "OnWorkflowActivated"
Me.onWorkflowActivated1.Name = "onWorkflowActivated1"
activitybind1.Name = "PublishProjectPlan"
activitybind1.Path = "workflowProps"
AddHandler Me.onWorkflowActivated1.Invoked, AddressOf Me.onWorkflowActivated1_Invoked
Me.onWorkflowActivated1.SetBinding(Microsoft.SharePoint.WorkflowActions.OnWorkflowActivated.WorkflowIdProperty, CType(activitybind2, System.Workflow.ComponentModel.ActivityBind))
Me.onWorkflowActivated1.SetBinding(Microsoft.SharePoint.WorkflowActions.OnWorkflowActivated.WorkflowPropertiesProperty, CType(activitybind1, System.Workflow.ComponentModel.ActivityBind))
Me.Name = "PublishProjectPlan"
Me.CanModifyActivities = False
End Sub
#End Region
Protected Overrides Function Execute(ByVal executionContext As System.Workflow.ComponentModel.ActivityExecutionContext) As System.Workflow.ComponentModel.ActivityExecutionStatus
Return MyBase.Execute(executionContext)
End Function
Protected Overrides Function HandleFault(ByVal executionContext As System.Workflow.ComponentModel.ActivityExecutionContext, ByVal exception As System.Exception) As System.Workflow.ComponentModel.ActivityExecutionStatus
Try
If workflowProps IsNot Nothing And workflowProps.Workflow IsNot Nothing Then
workflowProps.Workflow.CreateHistoryEvent(SPWorkflowHistoryEventType.WorkflowError, _
0, workflowProps.OriginatorUser, "Failed", _
String.Format("Failed to publish project plan due to the following error: {0}{1}", vbCrLf, exception.Message), _
String.Format("Activity name: {0}\{1}Exception: {2}", executionContext.Activity.Name, vbCrLf, exception.ToString()))
End If
' In either case, log to windows event log
LogEvent(exception.ToString(), EventLogEntryType.Error)
Catch ex As Exception
' TODO: safe-log to windows event log and/or database
End Try
' TODO: cancel workflow?
'SPWorkflowManager.CancelWorkflow(workflowProps.Workflow)
Return MyBase.HandleFault(executionContext, exception)
End Function
Private Sub LogEvent(ByVal message As String, ByVal eventType As EventLogEntryType)
Try
If EventLog.SourceExists("SharePoint Workflow") = False Then
EventLog.CreateEventSource("SharePoint Workflow", "Application")
End If
EventLog.WriteEntry("SharePoint Workflow", message, eventType)
Catch
EventLog.WriteEntry("Application", message, eventType)
' TODO: Add handling
End Try
End Sub
Private Sub onWorkflowActivated1_Invoked(ByVal sender As System.Object, ByVal e As System.Workflow.Activities.ExternalDataEventArgs)
'LogEvent("CIA Workflow Started", EventLogEntryType.Information)
End Sub
End Class
The important things to point out are:
· Any configuration data from app.config should be moved to web.config in your vdir folder.
· To avoid trust issues, register your assembly in GAC
· Make sure to recycle IIS worker process for your SharePoint site
7. Now we need to register this new workflow as a SharePoint “feature”. To do so, you’ll need the following two files place into PublishProjectPlanWorkflow (or choose your name) subfolder in the c:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\FEATURES folder:
workflow.xml
<?xml version="1.0" encoding="utf-8" ?>
<Elements Id="1bd66ed4-b9e0-4dc5-820e-eb7de884e902" xmlns="http://schemas.microsoft.com/sharepoint/">
<Workflow
Name="PublishProjectPlan"
Description="CIA Project Plan Publishing Workflow"
Id="068591c6-be5a-4b36-8a7c-6fe2c1ae434f"
CodeBesideClass="YourNamespace.YourClass"
CodeBesideAssembly="YourAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9c9f47ae4ce21556"
StatusUrl="_layouts/WrkStat.aspx">
<Categories/>
<MetaData>
</MetaData>
</Workflow>
</Elements>
feature.xml
<?xml version="1.0" encoding="utf-8"?>
<Feature Id="ee542b6e-7053-47e0-86e6-1f344034072e"
Title="Public CIA Project Plan"
Description="CIA Project Plan Publishing Workflow"
Version="12.0.0.0"
Scope="Site"
ReceiverAssembly="Microsoft.Office.Workflow.Feature, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
ReceiverClass="Microsoft.Office.Workflow.Feature.WorkflowFeatureReceiver"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="workflow.xml" />
</ElementManifests>
<Properties>
<Property Key="GloballyAvailable" Value="true" />
<!-- Value for RegisterForms key indicates the path to the forms relative to feature file location -->
<!-- if you don't have forms, use *.xsn -->
<Property Key="RegisterForms" Value="Forms\*.xsn" />
</Properties>
</Feature>
8. To register and activate this workflow with SharePoint, run the following commands:
"C:\Program Files\common files\microsoft shared\web server extensions\12\bin\stsadm.exe" -o installfeature -filename PublishProjectPlanWorkflow\feature.xml –force
"C:\Program Files\common files\microsoft shared\web server extensions\12\bin\stsadm.exe" -o activatefeature -name PublishProjectPlanWorkflow -url http://yoursite
9. Finally, you’ll need to “associate” your SharePoint list with this workflow:
· Navigate to http://yoursite/vdir/yourlist/Forms/AllItems.aspx
· Click on Settings -> Document Library Settings
· Under Permissions and Management, click on Workflow Settings
· Choose PublishProjectPlan workflow template and type in a unique name, e.g. Publish Project Plan
· Keep all other options as defaults and click OK
That’s it… Yes, it’s a lot of steps, but, once you do it, it’ll be more “natural” J
Note: for development purposes only, if you want to delete workflow instances, run the following command in the SharePoint SQL database:
delete from dbo.Workflow
Comments
- Anonymous
April 15, 2009
This is a nice post, since many people don't realize how easy it is to add custom actions to various SharePoint menus, and being able to kick off workflows directly from the context menu is a great idea, especially when there is a dedicated workflow to run against a list or library. However, in regards to your suggestion about deleting workflows in dev, I do want to caution anyone against running queries of any kind against the SharePoint database--even read-only selects, and even in a dev environment. Use a virtual machine and roll back to a previous snapshot if you want to completely remove workflow instances. But you can always terminate a running workflow, if you need to, from the SharePoint UI. Click on the running instance of the workflow from the list item, and select "Terminate this workflow". Regards, Mike Sharp