30 August 2014

PowerShell cmdlet to Import Nintex User Defined Actions (UDAs)

Anyone who is doing non-trivial Workflow development in Nintex should be aware of UDAs; Nintex has a great summary of them here, and I've already blogged about important considerations for automated importing of these here.

The next logical step from my last blog post then is to create a PowerShell cmdlet to make this process nice and neat - so here it is! As with my PowerShell cmdlet to provision Nintex Workflow Constants, this needs to be run on a SharePoint server within the Farm - sorry to those who don't have server access! I'm always interested to hear about remote methods that can achieve these results, let me know in the comments in you find out how this can be done.

You can view and download the cmdlet here. Note that the file has been renamed with a .ps1.txt file extension as some data transfer mechanisms block .ps1 files as a security concern - so you'll need to rename the file to have a .ps1 extension after you download it.
<#
.SYNOPSIS
   Publishes a Nintex Workflow UDA (User Defined Action), keeping it's original GUID intact!
.DESCRIPTION
   Because this approach retains GUIDs, you should avoid deploying the same UDA multiple times
   in an environment. If you want to reuse your UDA in a broader scope that where it is defined,
   you should promote it to a higher level rather than duplicating it.
   This script is designed for moving UDAs between farms.
.PARAMETER <Scope>
   Mandatory - where the UDA is defined. Must be one of: Farm, SiteCollection, Web
.PARAMETER <Url>
   Mandatory - the URL of the Site. Note that even for a Farm UDA you need to specify the URL of a valid Web for the publishing process to work.  
.PARAMETER <UdaFilePath>
   Mandatory - the full Path to the .uda file.
.PARAMETER <ChangeComments>
   Optional - but allows comments to be added to the publish process.
.PARAMETER <Publish>
   Optional - deafult value is $true. Using $false will allow it to be imported without publishing.
.EXAMPLE
   #Here's an example that publishes a Farm level UDA:
   .\Publish-NintexUda.ps1 `
  -Scope "Farm" `
  -Url "http://myfarm/sites/myweb" `
  -UdaFilePath "C:\ExampleUda.uda" `
#>

Param(
 [Parameter(Mandatory = $true, Position = 1)]
 [string] $Scope,
 [Parameter(Mandatory = $true, Position = 2)]
 [string] $Url,
 [Parameter(Mandatory = $true, Position = 3)]
 [string] $UdaFilePath,
 [Parameter(Mandatory = $false, Position = 4)]
 [string] $ChangeComments = "",
 [Parameter(Mandatory = $false, Position = 5)]
 [bool] $Publish = $true
 )

[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SharePoint') | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName('Nintex.Workflow') | Out-Null

function global:ImportNintexWorkflowUDA($filePath, $web, $publish, $configScope, $publishScope, $comments)
{ 
 $fs = New-Object System.IO.FileStream($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
 $ms = New-Object System.IO.MemoryStream
 # This only works for .NET 4 and above, so it won't work in SP 2010
 # $fs.CopyTo($ms)
 
 # Here's the more compatible (and ugly) alternative
 $buffer = New-Object Byte[] 4096
    $read = $fs.Read($buffer, 0, $buffer.Length)
 while ($read -gt 0)
 {
  $ms.Write($buffer, 0, $read)
  $read = $fs.Read($buffer, 0, $buffer.Length)
 }

 
 $fs.Close()
 $fs.Dispose() 
 $uda = [Nintex.Workflow.UserDefinedActions.UserDefinedAction]::Import($web, $ms, $configScope)
 $uda.Update($web, $publish, $publishScope, $comments)
 $ms.Close()
 $ms.Dispose()
}

$ArgsValid = $true

if ($Scope -ne "Farm" -and $Scope -ne "SiteCollection" -and $Scope -ne "Web")
{
 Write-Host "Error - Scope parameter must be one of: Farm, SiteCollection, Web" -ForegroundColor Red
 $ArgsValid = $false
}

$configScope = $null
$publishScope = $null

if ($Scope -eq "Farm")
{
 $configScope = [Nintex.Workflow.ConfigurationScope]::Farm
 $publishScope = [Nintex.Workflow.Publishing.Scope]::Farm
}
if ($Scope -eq "SiteCollection")
{
 $configScope = [Nintex.Workflow.ConfigurationScope]::Site
 $publishScope = [Nintex.Workflow.Publishing.Scope]::SiteCollection
}
if ($Scope -eq "Web")
{
 $configScope = [Nintex.Workflow.ConfigurationScope]::Web
 $publishScope = [Nintex.Workflow.Publishing.Scope]::Web
}

if ($ArgsValid)
{  
 $web = $null
 $web = Get-SPWeb $Url
 
 if ($web -ne $null)
 {
  Write-Host ("Attempting to publish UDA... ") -NoNewline 
  ImportNintexWorkflowUDA $UdaFilePath $web $Publish $configScope $publishScope $ChangeComments
  Write-Host "done"
 } 
}

26 July 2014

PowerShell cmdlet to provision Nintex Workflow Constants

If you are building Nintex Workflows, and especially if you are deploying them across multiple environments, then you should be considering using Nintex Workflow constants. These constants are remarkably useful to make your Nintex Workflows portable, so that they can easily be deployed into multiple environments without confusion or wasted effort. Any workflow parameters which are specific to the environment in which the workflow is running, or which are likely to be changed by the business at some stage, should be built into your workflows using these constants.

Many of our clients are looking to improve their processes for deploying and managing SharePoint and Nintex based functionality, and the critical aspects tend to fall around governance and repeatability. So in the interests of repeatability, here's a PowerShell cmdlet to create a new Nintex Workflow constant.

With this script you can specify any type of constant, including credentials which are useful for service accounts. You can also define the constant at a scope of Farm, Site Collection or Site.

The cmdlet needs to be run on a server within the SharePoint farm where the constant is to be created. If anyone is aware of alternate methods that can be run remotely, please let me know in the comments!

You can view and download the cmdlet here. Note that the file has been renamed with a .ps1.txt file extension as some data transfer mechanisms block .ps1 files as a security concern - so you'll need to rename the file to have a .ps1 extension after you download it.
<#
.SYNOPSIS
   Creates a Nintex Workflow Constant
.DESCRIPTION
   
.PARAMETER <Name>
   Mandatory - the Name of the Constant. Keep these unique!
.PARAMETER <Description>
   Mandatory - the Description of the Constant. Use this to help other developers understand what it's used for.
.PARAMETER <Type>
   Mandatory - the Type of the Constant. Must be one of: Number, String, Date, SecureString, Credential
.PARAMETER <Scope>
   Mandatory - where the Constant is defined. Must be one of: Farm, SiteCollection, Web
.PARAMETER <Value>
   Optional - the value of the Constant. This isn't used for Credential type constants, but it's Mandatory for every other type.
.PARAMETER <Url>
   Optional - but Mandatory is the Scope is SiteCollection or Web. Defines where the Constant should live.
.PARAMETER <Sensitive>
   Optional - default value is $false.
.PARAMETER <AdminOnly>
   Optional - default value is $false.
.PARAMETER <Username>
   Optional - but Mandatory if Type is Credential.
.PARAMETER <Password>
   Optional - but Mandatory if Type is Credential.
.EXAMPLE
   #Here's an example that generates a Sensitive, Credential type constant at Web level:
   .\Create-NintexWFConstant.ps1 `
  -Name "test" `
  -Description "Example only" `
  -Scope "Web" `
  -Type "Credential" `
  -Sensitive $true `
  -Username "DOMAIN\user" `
  -Password "password123" `
  -Url "http://myfarm/sites/myweb"

 #Here's a second example that creates a Farm string type Constant:
 .\Create-NintexWFConstant.ps1 `
  -Name "test2" `
  -Description "Example only" `
  -Scope "Farm" `
  -Type "String" `
  -Value "example constant value"
#>

Param(
 [Parameter(Mandatory = $true, Position = 1)]
 [string] $Name,
 [Parameter(Mandatory = $true, Position = 2)]
 [string] $Description,
 [Parameter(Mandatory = $true, Position = 3)]
 [string] $Type,
 [Parameter(Mandatory = $true, Position = 4)]
 [string] $Scope,
 [Parameter(Mandatory = $true, Position = 5)]
 [string] $Value,
 [Parameter(Mandatory = $false, Position = 6)]
 [string] $Url,
 [Parameter(Mandatory = $false, Position = 7)]
 [bool] $Sensitive = $false,
 [Parameter(Mandatory = $false, Position = 8)]
 [bool] $AdminOnly = $false,
 [Parameter(Mandatory = $false, Position = 9)]
 [string] $Username = "",
 [Parameter(Mandatory = $false, Position = 10)]
 [string] $Password = ""
)

[System.Reflection.Assembly]::LoadWithPartialName('Nintex.Workflow') | Out-Null

$ArgsValid = $true

if ($Type -ne "Number" -and $Type -ne "String" -and $Type -ne "Date" -and $Type -ne "SecureString" -and $Type-ne "Credential")
{
 Write-Host "Error - Type parameter must be one of: Number, String, Date, SecureString, Credential." -ForegroundColor Red
 $ArgsValid = $false
}

if ($Type -eq "Credential")
{
 if ($Username -eq $null -or $Username -eq "" -or $Password -eq $null -or $Password -eq "")
 {
  Write-Host "Credential Error - Username and Password combination not supplied." -ForegroundColor Red
  $ArgsValid = $false
 }

 # Generate string for Credential
 $cred = New-Object Nintex.Workflow.CredentialValue($Username, $Password)
 $serialiser = New-Object System.Xml.Serialization.XmlSerializer($cred.GetType())
 $sb = New-Object System.Text.StringBuilder
 $sw = New-Object System.IO.StringWriter($sb)
 $serialiser.Serialize($sw, $cred)
 $Value = $sb.ToString()

}
else
{
 if ($Value -eq $null -or $Value -eq "")
 {
  Write-Host "Error - Value must be supplied for Constants of Type other than Credential." -ForegroundColor Red
  $ArgsValid = $false
 }
}

$SiteId = [Guid]::Empty
$WebId = [Guid]::Empty

if ($Scope -eq "Farm")
{
 #Use default SiteId and WebId values
}
else
{
 if ($Scope -eq "SiteCollection")
 {
  $SiteId = (Get-SPSite $Url).Id
 }
 else
 {
  if ($Scope -eq "Web")
  {
   $Web = Get-SPWeb $Url
   $WebId = $Web.Id
   $SiteId = $Web.Site.Id
  }
  else
  {
   Write-Host "Error - Scope parameter not valid: must be one of Farm, SiteCollection, Web." -ForegroundColor Red
   $ArgsValid = $false
  }
 }
}


if ($ArgsValid)
{
 Write-Host ("Attempting to create Workflow Constant " + $Name + " ... ") -NoNewline 

 $constant = New-Object Nintex.Workflow.WorkflowConstant(`
  $Name, $Description, $Value, $Sensitive, $SiteId, $WebId, $Type, $AdminOnly)

 $constant.Update()

 Write-Host "done"
}

22 June 2014

Building a SharePoint Forms Platform: SharePoint Saturday Slide Deck

SharePoint Saturday Perth 2014 was a huge success - we had more attendees than any other SharePoint Saturday in Australian history! Massive thanks to everyone who came along, to all the presenters, and of course to the sponsors and organisers.

As promised, here's the slide deck that I presented on the day - Building a SharePoint Forms Platform: Real World Considerations and Lessons. Enjoy!

http://1drv.ms/1pzksEZ

04 April 2014

Automated publishing of Nintex Workflow User Defined Actions, with workflow references

One of my absolute favourite features in Nintex Workflow is the User Defined Action (UDA). It lets you wrap up a commonly used piece of functionality with defined input and output parameters, into a neat, centrally managed object, which can be easily reused in workflows across the entire farm. A UDA can also easily be updated whereby Nintex Workflow automatically updates all workflows that reference the UDA.

For a workflow project we are currently building, I was a little disappointed to find that there is no supported method to automate the import of a UDA. This slows down our planned deployment process which is nearly entirely automated through PowerShell.

What was even more troubling was that importing a UDA through the user interface did not work correctly for imported workflows that referenced the UDA. Each UDA referenced within a workflow appeared like this:
This is how a UDA appears in a workflow if the reference is broken, for example through an import process
Attempting to configure the UDA only threw exceptions. The only way to resolve this was to manually delete the UDA references, replace them with new references, and update all the parameters in the references. This is very slow, error-prone and frustrating!

With some investigation, it turns out that each UDA has an internal GUID identifier which is used by workflows to reference the UDA. This GUID can been seen in both the exported .uda and .nwf files. It seems that when importing a UDA through the user interface, a new GUID is assigned to the UDA. This is what was breaking the references.

Warning: the solution I am presenting here is not technically part of the supported Nintex Workflow API, so it is not guaranteed to work for all future releases, but it works for my initial testing against Nintex Workflow 2013 version 3.0.6.0. This process is based on the suggestion from user tburdin on the excellent Nintex Connect Forums, in this thread. It uses the publicly exposed members of the Nintex Workflow assemblies to automate the import of UDAs. Here is the PowerShell snippet:

[System.Reflection.Assembly]::LoadWithPartialName('Nintex.Workflow') | Out-Null
 
$fs = New-Object System.IO.FileStream($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
$ms = New-Object System.IO.MemoryStream
$fs.CopyTo($ms)
$fs.Close()
$fs.Dispose() 
$uda = [Nintex.Workflow.UserDefinedActions.UserDefinedAction]::Import($web, $ms, [Nintex.Workflow.ConfigurationScope]::Site)
$uda.Update($web, $publish, [Nintex.Workflow.Publishing.Scope]::SiteCollection, "")
$ms.Close()
$ms.Dispose()

Note that the code assumes you've populated the following variables:
  • $name: the name of your UDA.
  • $filePath: the full path to your exported .uda file.
  • $web: the SPWeb object defining the context of where the UDA is going to be imported
  • $publish: a Boolean value indicating whether to publish the UDA or simply import it
The above code publishes to the Site Collection level, but you can also publish to a single site or even to the entire Farm, by altering the values of the Nintex enumerations referenced in the code. To publish to a single site, use [Nintex.Workflow.ConfigurationScope]::Web and [Nintex.Workflow.Publishing.Scope]::Web, and to publish to a farm, use [Nintex.Workflow.ConfigurationScope]::Farm and [Nintex.Workflow.Publishing.Scope]::Farm.

The best aspect of this process is that it retains the original GUID reference of each UDA. So, when you import UDA using this method and then import workflows that reference the original UDAs, no re-wiring is required! You can fully automate the process and have a portable workflow deployment strategy.

It's worth noting that Nintex probably updates those GUIDs during the import process to ensure that you can never get in a situation where you have multiple UDAs in a farm that use the same GUID. For example, you could import the same UDA into multiple sites (you wouldn't want to do this, but it would certainly be possible). In this scenario, having two or more UDAs with the same GUID would almost certainly cause conflicts. Use with caution!