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