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}&amp;ItemId={ItemId}&amp;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”>


                       &nbsp;


                       </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%”>


                       &nbsp;


                       </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 (1)

  1. rdcpro says:

    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