Wednesday, December 01, 2010

Deploy SharePoint Designer 2010 Reusable Workflow As *.WSP File

SharePoint Designer 2010 makes workflow development really fast and simple. Much simpler than using Visual Studio. But unlike Visual Studio this tool has some limitations from developer's point of view.

This is due to SPD was created as a tool for end (SharePoint) users, but not for solution developers. As a result we have one serious limitation that prevents developers to use this tool: it doesn't allow deploy created workflows to another SharePoint servers. Which means you cannot develop and test workflows on a development SharePoint server and then move it to production. You forced to develop in production, which is not right.

In particular what I said is true for reusable workflows that work with custom list instances. The problem is once you reference some list in workflow, SPD will link workflow template (*.xoml) to this list using its ListId attribute which is unique identifier that is valid for that particular site. This ListId is a random value that SharePoint generates when the list deployed. Note that you deploy list instances not only when staging ready solution from development to production, but also repeatedly during development cycle.

There are two ways you can notice your workflow corrupted. The first is in SPD you will see GUIDs instead of list names, and if you click these GUIDs you'll see unbinded dialogs:




And if you deploy such workflow to another SharePoint site (with prior export to *.WSP) you will get the error like this (in SharePoint logs) when try to run it:

SOAP exception: System.Runtime.InteropServices.COMException (0x82000006): List does not exist. The page you selected contains a list that does not exist. It may have been deleted by another user.
at Microsoft.SharePoint.SoapServer.SPBaseImpl.GetSPListByTitle(SPWeb spWeb, String strListName)
at Microsoft.SharePoint.SoapServer.SPBaseImpl.GetSPList(SPWeb spWeb, String strListName, Boolean bGetMetaData, Boolean bGetSecurityData)
at Microsoft.SharePoint.SoapServer.SPBaseImpl.GetSPList(String strListName, Boolean bGetMetaData, Boolean bGetSecurityData)
at Microsoft.SharePoint.SoapServer.ListSchemaImpl.GetList(String strListName)
at Microsoft.SharePoint.SoapServer.ListSchemaValidatorImpl.GetList(String strListName)
at Microsoft.SharePoint.SoapServer.Lists.GetList(String listName)


The most common approach you may find on the Internet is to export SPD reusable workflow to *.WSP solution package, import that *.WSP to Visual Studio and continue development there. I don't like this approach for two reasons: first is once you do this you can't open that workflow in SharePoint designer again to made any changes (and you don't want to change it in Visual Studio because, like every auto-generated code, SPD generated *.xoml is not very human-friendly):


And the second (which I'm not sure, though)---you can't deploy such workflow as a sandboxed solution (correct me if I wrong).

Fortunately, there is nothing that prevents us from deploying SPD reusable workflows except ListIds. All we need to do is replace broken Ids with the new ones. Here's how you may do this:

  1. Export reusable workflows to *.WSP files using SPD 2010
  2. To fix ListIds change contents of the process*.xoml file contained in *.WSP file (which is *.CAB file that contain (inter alia) Feature.xml)
    To extract and package contents of *.WSP I recommend to use PowerShell + built-in expand command and WSPBuilder's CabLib.dll accordingly
  3. *.xoml is an regular text/xml file so we can simply find and replace GUID strings
  4. We know what GUIDs to be replaced by examining contents of the *.xoml file.
    Look for entries like this:
    <ns1:LookupActivity ListId="{}{909E9DFD-A30B-4E28-BF2E-5BA47095967D}"  
    x:Name="ID10" FieldName="ID" LookupFunction="LookupInt"
    __Context="{ActivityBind ROOT,Path=__context}"
    ListItem="{ActivityBind ID11,Path=ReturnValue}" />

  5. We know the replacement for old GUIDs by using PowerShell automation: get SPWeb object of site where we want to deploy the workflow, get list in that web by list title, get ID of that list and convert that ID to string. Note that GUID string should be in upper case, otherwize you won't be able to edit workflow in SPD, though it will run okay on site (thats probably SPD bug)
  6. After replacing GUIDs we create new *.WSP with relevant ListIds which may be deployed to SharePoint


Below is sample PowerShell script that you can use as a reference to implement steps described above. To run it save contents to file, say Deploy-Workflows.ps1, change values of $siteUrl, $wspDir and $listIds to match your environment. You will also have to place CabLib.dll and AnjLab-SharePoint.ps1 files to the same folder as Deploy-Workflows.ps1. After that open SharePoint 2010 Management Shell, CD to directory with Deploy-Workflows.ps1 and run the script with command .\Deploy-Workflows.ps1.

# Allow running *.ps1 scripts from network shares 
# Set-ExecutionPolicy Unrestricted

# Copy CabLib.dll to user's temp (to prevent security exceptions if your project files are on network share)
# Note: $cablibFullName is a global variable used in AnjLab-SharePoint.ps1
$cablibFullName = "$env:TEMP\CabLib.dll"
if ((test-path $cablibFullName) -eq $false)
{
cp "CabLib.dll" $env:TEMP
}

# Import AnjLab-SharePoint functions
. .\AnjLab-SharePoint.ps1

# Replace with yours

$siteUrl = "http://dev-en/gls/" # SharePoint site to deploy *.WSP workflows to
$wspDir = "bin\Debug\Workflows" # Directory with *.WSP files
# (all files from this folder will be updated and deployed)
$wspTempDir = "$wspDir\temp" # Temp directory
$wspFinalDir = "$wspDir\final" # Directory where final *.WSP files will be placed

###################################################################################################
# Define mapping for GUID replacement in the hashtable below.
# All GUIDs that match keys from this hashtable will be replaced with corresponding GUIDs of lists
# taken from $siteUrl by specified list titles.
###################################################################################################

$listIds = @{ # ListId in workflow's *.WSP List Title on SharePoint Site
# ------------------------------------- -----------------------------
"909E9DFD-A30B-4E28-BF2E-5BA47095967D" = "Consumers";
"2F311150-7360-45DA-A4B1-C64339F3B931" = "Warehouses";
"435E8D1B-FC3F-42A9-B761-1958A31D9BDE" = "Leads";
}

# Replace List Ids

$wspFiles = (Get-ChildItem "$wspDir\*.wsp")
Update-WspListIds $siteUrl $wspFiles $wspTempDir $wspFinalDir $listIds

# Deploy Packages

$wspFiles = (Get-ChildItem "$wspFinalDir\*.wsp")
Deploy-Wsp $siteUrl $wspFiles $wspTempDir

Write-Host "Done"