Nintex Custom Actions – Credential Picker


Continuing on from my previous post of Nintex Custom Actions – Error Handling, I thought I would also blog about using the Credential Control within a Custom Action.

The Credential Control allows the selection of predefined credential workflow constants (Central Admin > Nintex Workflow Management > Mange Workflow Constants) or a username and password to be typed in. When the action is run, the action can be run under the context those credentials. (Please Note: Username should be
domain\username, not just username as the picture indicates)

There are several Nintex actions that use this control already, a few are noted below:

  • Query User Profile
  • Web Request
  • Create AD Group
  • Add user to AD Group

Recently from working with Nintex I have used the Query User Profile


and Update User Profile actions.

Nintex doesn’t provide you with a Create User Profile, because normally with the SharePoint User Profile service, this would automatically import new users in. However, I needed the user to be in the User Profile database as soon as the user existed, I didn’t want to wait for SharePoint to complete the User Profile import. I therefore was required to make this custom action, and had the first opportunity to use the Credential Control Picker.

This blog post will show you which files you will need to update and what code is required, to make your custom action have a Credential Control Picker. I will also show you how to run the code within Nintex in a different user context. Please ensure you know how to create a Custom Action from reading my previous blog post on Creating Nintex Custom Actions, as this doesn’t show you every method and what needs to be filled in, only areas for the Credential control. What you use the Credential Picker is down to you, but you can find a working version in my Create User Profile in my previous Nintex Custom action project on my One Drive.

Looking at the project files below, these are the typical files required to create a Custom Nintex Action.

  • [name]Action.nwa – A XML file that contains Name, Category, Description, Assembly information of Activity and Adapter.
  • [name]Activity.cs – A class inheriting Nintex.Workflow.Activities.ProgressTrackingActivity which is the main execution of the custom action.
  • [name]Adapter.cs – A class inheriting Nintex.Workflow.Activities.Adapters.GenericRenderingAction, which sets up the bindings, and configuration of the custom action.
  • [name]Dialog.aspx – The aspx page to allow the user to configure the custom action.

Below is all the files required for one custom action.

Activity file.

First we will need to add public static DependencyProperties for the Username and one for the Password.

public static DependencyProperty UsernameCredentialsProperty = DependencyProperty.Register("UsernameCredentials", typeof(string), typeof(CreateUserProfileActivity));
public static DependencyProperty PasswordProperty = DependencyProperty.Register("Password", typeof(string), typeof(CreateUserProfileActivity));

Then each of these Dependency Properties will have its own public property.

//Implement Security
public string UsernameCredentials
{
  get { return (string)base.GetValue(UsernameCredentialsProperty); }
  set { base.SetValue(UsernameCredentialsProperty, value); }
}
 
public string Password
{
  get { return (string)base.GetValue(PasswordProperty); }
  set { base.SetValue(PasswordProperty, value); }
}

As you know the Activity file is where the main execution of the custom action takes place. The case is the same when using a Credential Picker, however you need to run the main bulk of your code in a different context, which will run in a different thread to the main execution.

We need to create an internal class, this class will hold all the information we need to pass between the threads. As you can see, this class will be storing the Username and Password from the control, the SiteID and the WebID of the current site the action is running in. There are two properties to pass a value in and a value out. This is a standard C# class, you can have as many or as little properties as required, minimum you need is the username and password obviously.

internal class RunDifferentThread {
    internal string username { get; set; }
    internal string password { get; set; }
    internal string valueToPassIn { get; set; }
    internal string valueToReturn { get; set; }
    internal Guid siteId { get; set; }
    internal Guid webId { get; set; }
}

Now this class has been created, I can next work on the Execute method.

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
   string runtimeUsername = string.Empty;
   string runtimePassword = string.Empty;

   ActivityActivationReference.IsAllowed(this, __Context.Web);
   NWWorkflowContext ctx = NWWorkflowContext.GetContext(
          this.__Context,
          new Guid(this.__ListId),
          this.__ListItem.Id,
          this.WorkflowInstanceId,
          this);
 
   base.LogProgressStart(ctx);
   try
   {
       //Create RunDifferentThread Object
       CreateUserProfileActivity.RunDifferentThread runDifferentThread = new CreateUserProfileActivity.RunDifferentThread();

       //Get the runtimeUserName and runtimePassword from the Credential picker values.
       CredentialValue.DetermineRuntimeCredentials(this.UsernameCredentials, this.Password, out runtimeUsername, out runtimePassword, ctx.Web.ID, ctx.Web.Site.ID);

       //Setup values to pass to other thread.
       runDifferentThread.username = runtimeUsername;
       runDifferentThread.password = runtimePassword;

       //User Picker field within the Custom Action
       runDifferentThread.valueToPassIn = ctx.AddContextDataToString(this.AccountName);
       runDifferentThread.siteId = this.__Context.Site.ID;
       runDifferentThread.webId = this.__Context.Web.ID;
       runDifferentThread.accountToAdd = ctx.AddContextDataToString(this.AccountName);
 
       //Using System.Threading.Thread, create a new thread passing in the parameters required.
       Thread thread = new Thread(new ParameterizedThreadStart(this.Process));
       thread.Start((object)runDifferentThread);
       thread.Join();

       //A workflow variable to store the result in
       this.returnValue = runDifferentThread.valueToReturn
       base.LogProgressEnd(ctx, executionContext);
       return ActivityExecutionStatus.Closed;
   }
   catch (Exception ex)
   {
       throw;
   }
}

The code for the Execute function starts off the same as any other custom action by getting the Workflow Context. Then you need to call Nintex.Workflow.CredentialValue.DeterminRuntimeCredentials passing in the values from the UserName and Password fields. This will output the correct format values of the Username and Password. We put all the values we want to pass through to the new thread running as the new context to our runDifferentThread class. Next we create a new thread passing in the method to run Process, and the runDifferentThread. (At this point we haven’t written the Process method.) Calling thread.Join() blocks the calling thread until the other thread has terminated, which means it will wait until the method Process being called on the other thread has finished.

Then we can grab the returning value and pass it back to be stored within a workflow variable, or whatever the user has chosen to store the value in.

Below is the code for the Process method. I worked out how to do this code mainly from reflecting on how the custom action Update User Profile.

private void Process(object contextObject)
{
    //Hydrate the runDifferentThread properties
    CreateUserProfileActivity.RunDifferentThread runDifferentThread = (CreateUserProfileActivity.RunDifferentThread)contextObject;

   //This built in Nintex class allows the code to run as different user.
   Nintex.Workflow.Administration.Security security = new Nintex.Workflow.Administration.Security();

   try
   {
       //Check that the username is in the format Domain\UserName
       string[] strArray = null;

       if (runDifferentThread.username.Contains(@"\"))
       {
            strArray = runDifferentThread.username.Split(new char[] { '\\' });
       }

       if(strArray.Length != 2)
           throw new InvalidOperationException(NWResource.GetString("Exception_InvalidWindowsCredentials"));

       //Run the following code as the given user. Passing in Domain, Username and Password.        
       security.RunAsUser(strArray[1], strArray[0], runDifferentThread.password);

       //This site will run as passed in user.
       using(SPSite site = new SPSite(runDifferentThread.siteId))
       {
           //Perform you elevated permission code here

           //Return value
           runDifferentThread.valueToReturn = "My value to return from wherever"
        }
    }
    catch (Exception ex)
    {
        //Exception handling
    }
    finally
    {
       //Ensure you revert back to original user who is running the workflow.
       security.RevertToLoggedInUser();
    }
}

Walking through the Process method above, first covert the contextObject back to RunDifferentThread class. The Nintex.Workflow.Administration.Security class allows you to run as different context. After checking that the username passed in is in the correct format of Domain\Username, we then call the security.RunAsUser passing in domain, username and password. Everything after this line is ran in the user context. Please remember to finally call security.RevertToLoggedInUser(). When you update the RunDifferentThread class with new properties, these properties will be available back in the Execute method.

Adapter.cs

The adapter class has 3 main methods to change AddActivityToWorkflow, GetConfig and GetDefaultConfig. These would be the same 3 methods you would change for any controls added. At the top of the adapter class create 2 constant properties for Username and Password. These properties values should match the property names in the Activity class.

private const string UserNameCredentialsProperty = "UsernameCredentials";
private const string PasswordProperty = "Password";

Within the AddActivityToWorkflow method we need to ensure the properties are assigned to the correct Dependency property in the Activity Class.

parameters[UserNameCredentialsProperty].AssignTo(activity, CreateUserProfileActivity.UsernameCredentialsProperty, context);
parameters[PasswordProperty].AssignTo(activity, CreateUserProfileActivity.PasswordProperty, context);

The GetConfig method needs to return the values from the correct Dependency property in the Activity Class. Put this just before you return config.

parameters[UserNameCredentialsProperty].RetrieveValue(context.Activity, CreateUserProfileActivity.UsernameCredentialsProperty, context.ListId, context.Variables);
parameters[PasswordProperty].RetrieveValue(context.Activity, CreateUserProfileActivity.PasswordProperty, context.ListId, context.Variables);

The GetDefaultConfig method defines the parameters that the user can configure, therefore we need to ensure there are two parameters for Username and password.

config.Parameters[0] = new ActivityParameter();
config.Parameters[0].Name = UserNameCredentialsProperty;
config.Parameters[0].PrimitiveValue = new PrimitiveValue();
config.Parameters[0].PrimitiveValue.Value = string.Empty;
 
config.Parameters[1] = new ActivityParameter();
config.Parameters[1].Name = PasswordProperty;
config.Parameters[1].PrimitiveValue = new PrimitiveValue();
config.Parameters[1].PrimitiveValue.Value = string.Empty;

Optionally, if you want to ensure that the user fills in the User Credential field you could add the following code within the ValidateConfig method within the Adapter class.

if (!parameters[UserNameCredentialsProperty].Validate(typeof(string), context))
{
    isValid &= false;
    validationSummary.AddError(string.Empty, ValidationSummaryErrorType.RequiredCredential);
}

Dialog.aspx

Lastly the dialog.aspx page requires some code to ensure the user has an interface to enter the username/password values. Here you will need to register the Credential control, update the two JavaScript function to read in and read out the values, and lastly update the HTML to display it.

You will need to register the CredentialControl in the page.

<%@ Register TagPrefix="Nintex" TagName="CredentialControl" Src="~/_layouts/15/NintexWorkflow/CredentialControl.ascx" %>

Within the TPARetrieveConfig function you can retrieve the stored values as below. The built in Nintex JavaScript function cc_setUsername and cc_setPassword does the main work of setting the control to display previous values to the user.

if (configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='UsernameCredentials']") && configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='Password']")) {

  cc_setUsername("<%=credentialPicker.ClientID %>", configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='UsernameCredentials']/PrimitiveValue/@Value").text);

  cc_setPassword("<%=credentialPicker.ClientID %>", configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='Password']/PrimitiveValue/@Value").text);
}

The TPAWriteConfig function writes back to the configuration file. Nintex has provided a corresponding get function to read from the control.

function TPAWriteConfig() {
    EnsurePrimitiveValueNode(configXml, "UsernameCredentials");
    EnsurePrimitiveValueNode(configXml, "Password");

    configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='UsernameCredentials']/PrimitiveValue/@Value").text = cc_getUsername("<%= credentialPicker.ClientID %>");

    configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='Password']/PrimitiveValue/@Value").text = cc_getPassword("<%= credentialPicker.ClientID %>");

    return true;
}

The HTML code sits within <TemplateRowsArea>.

&lt;Nintex:ConfigurationProperty runat=&quot;server&quot; FieldTitleResourceKey=&quot;General_Credentials&quot; RequiredField=&quot;True&quot;&gt;
&lt;TemplateControlArea&gt;
        &lt;Nintex:CredentialControl DisplayMode=&quot;dialog&quot; runat=&quot;server&quot; id=&quot;credentialPicker&quot; /&gt;
&lt;/TemplateControlArea&gt;
&lt;/Nintex:ConfigurationProperty&gt;

That is all there is to it. As previously stated a working version of the Create User Profile custom action can be found in my One Drive. To use, the account that you are creating must already exist within your Active Directory. (There is already a Nintex action that creates an Active Directory user, also using the credential control picker).

Advertisements

Nintex Custom Actions – Error Handling


A while ago now, I wrote a blog post about Nintex Custom Actions. This was written for SharePoint 2010. I have recently worked on a project where I’ve got a chance to use Nintex again. My own blog post came in very handy, and I have updated it so it now explains what you need to do for SharePoint 2013. To be honest there isn’t much difference. Mainly location of Nintex dll’s, and remembering to put /_layouts/15 when pointing to layout files.

Some of the out of the box Custom actions has an error handling section when configuring.

There are 3 parts to error handling.

  1. Capture Errors – If there is an error should the custom action capture it (Yes/No)
  2. Store error occurrence in – This stores if an error occurred or not. (Yes/No)
  3. Store error text in – This stores the actual error message returned if there is one (Single line of text)

This blog post will show you which files you will need to update and what code is requried, to make your custom action have Error handling section. Please ensure you know how to create a Custom Action from reading my previous blog post, as I won’t be going over old ground. I have added custom error handling to my previous project “Retrieve from Property Bag.” If the property doesn’t exist in the property bag then it will throw an error, this will allow my workflow to take a different path.

ReadFromPropertyBagActivity.cs

First we will need to add public static DependencyProperties for the 3 fields within Error Handling, add these below the previous Dependency properties.

#region Error Handling
//Determines if error handling has been switched on for this activity by the workflow designer.
public static DependencyProperty CaptureErrorsProperty = DependencyProperty.Register("CaptureErrors", typeof(bool), typeof(ReadFromPropertyBagActivity));

//Stores whether or not an error occured.
public static DependencyProperty ErrorOccurredOutputProperty = DependencyProperty.Register("ErrorOccurredOutput", typeof(bool), typeof(ReadFromPropertyBagActivity));

//Stores the details of an error if one occurred.
public static DependencyProperty ErrorMessageOutputProperty = DependencyProperty.Register("ErrorMessageOutput", typeof(string), typeof(ReadFromPropertyBagActivity));
#endregion

Then each of these Dependency Properties will have its own public property.

#region Error Dependency Properties
public bool CaptureErrors
{
    set { base.SetValue(CaptureErrorsProperty, value); }
    get { return (bool)base.GetValue(CaptureErrorsProperty); }
}

public bool ErrorOccurredOutput
{
    set { base.SetValue(ErrorOccurredOutputProperty, value); }
    get { return (bool)base.GetValue(ErrorOccurredOutputProperty); }
}

public string ErrorMessageOutput
{
    set { base.SetValue(ErrorMessageOutputProperty, value); }
    get { return (string)base.GetValue(ErrorMessageOutputProperty); }
}
#endregion

Still within the ReadFromPropertyBagActivity.cs file, the Execute method will needs to be updated. Below I have put the original Execute method and highlighted the extra code required for Error Handling.

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
    //Standard Nintex code to obtain context.
    ActivityActivationReference.IsAllowed(this, __Context.Web);
    NWWorkflowContext ctx = NWWorkflowContext.GetContext(
       this.__Context,
       new Guid(this.__ListId),
       this.__ListItem.Id,
       this.WorkflowInstanceId,
       this);
    base.LogProgressStart(ctx);
 
   //Get the property value.
   string resolvedProperty = ctx.AddContextDataToString(this.Property);

   var result = "";

   //Using the context get the property if it exists.
   if (ctx.Web.AllProperties.ContainsKey(resolvedProperty))
   {
       result = ctx.Web.AllProperties[resolvedProperty].ToString();

       if(CaptureErrors)
            ErrorOccurredOutput = false;

        //store the result.
        this.ResultOutput = result;
    }
    else
    {
        if (CaptureErrors)
        {
            ErrorOccurredOutput = true;
            ErrorMessageOutput = String.Format("Unable to find property bag key:{0}", resolvedProperty);
        }
    }

    //End Execution.
    base.LogProgressEnd(ctx, executionContext);
    return ActivityExecutionStatus.Closed;
}

What is happening here is that if the web site contains the property, then if the user sets the custom action to capture errors then set the ErrorOccurredOutput to false. If the property doesn’t exist, and the user has set the custom action to capture errors then will need to set the ErrorOccurredOutput to true and set the error message in the ErrorMessageOutput property.

ReadFromPropertyBagAdapter.cs

In this file there are only two methods that require code change to include the Error Handing. These methods are AddActivityToWorkflow and GetConfig.

Within the AddActivityToWorkflow, after setting the activity.SetBinding but before the ActivityFlags add the following piece of code.

if (context.Config.ErrorHandling != null)
{
    context.Config.ErrorHandling.AssignTo(activity,
        ReadFromPropertyBagActivity.CaptureErrorsProperty,
        ReadFromPropertyBagActivity.ErrorOccurredOutputProperty,
        ReadFromPropertyBagActivity.ErrorMessageOutputProperty,
        context);
}

Within the GetConfig method, add the error handling code just before we return the NWActionConfig.

config.ErrorHandling = ErrorHandling.BuildFrom(context.Activity,
                ReadFromPropertyBagActivity.CaptureErrorsProperty,
                ReadFromPropertyBagActivity.ErrorOccurredOutputProperty,
                ReadFromPropertyBagActivity.ErrorMessageOutputProperty,
                context.Variables);

ReadFromPropertyBagDialog.cs

This is the final piece of the puzzle. This is the configuration screen when you edit the custom action. There is two javaScript functions (TPARetrieveConfig and TPAWriteConfig) that needs updating, then lastly we will need to update the HTML to display, that uses a registered control.

Add the register control.

<%@ Register TagPrefix="Nintex" TagName="ErrorHandlingConfig" Src="~/_layouts/15/NintexWorkflow/ErrorHandlingConfig.ascx" %>

Nintex has already written the JavaScript to make the error handling work, therefore we just need to call it.

TPARetrieveConfig – This loads and displays previous saved values.

function TPARetrieveConfig() {
            setRTEValue('<%=propertyProperty.ClientID%>', configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='Property']/PrimitiveValue/@Value").text);
            document.getElementById('<%=resultOutput.ClientID%>').value = configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='ResultOutput']/Variable/@Name").text;
 
            LoadErrorHandlingSection();
}

TPAWriteConfig – This saves properties back to the custom action.

function TPAWriteConfig() {
            configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='Property']/PrimitiveValue/@Value").text = getRTEValue('<%=propertyProperty.ClientID%>');

            var resultOuputCtrl = document.getElementById('<%=resultOutput.ClientID%>');

            if (resultOuputCtrl.value.length > 0) {
                configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='ResultOutput']/Variable/@Name").text = resultOuputCtrl.value;
            }

            SaveErrorHandlingSection();

            return true;
        }

Finally we need to add the HTML to the page. This is within the content place holder ID PlaceHolderMain. This is added just before the closing </TemplateRowsArea>.

<Nintex:ConfigurationPropertySection runat="server" Id="MainControls1">
              <TemplateRowsArea>
               <Nintex:ConfigurationProperty runat="server" FieldTitle="Property Bag Property" RequiredField="True">
                   <TemplateControlArea>
                        <Nintex:SingleLineInput clearFieldOnInsert="true" filter="number" runat="server" id="propertyProperty"></Nintex:SingleLineInput>
                  </TemplateControlArea>
                </Nintex:ConfigurationProperty>

                <Nintex:ConfigurationProperty runat="server" FieldTitle="Result Output" RequiredField="False">
                  <TemplateControlArea>
                    <Nintex:VariableSelector id="resultOutput" runat="server" IncludeTextVars="True"></Nintex:VariableSelector>
                  </TemplateControlArea>
                </Nintex:ConfigurationProperty>

                <Nintex:ErrorHandlingConfig runat="server" id="errorHandlingConfig1"></Nintex:ErrorHandlingConfig>
              </TemplateRowsArea>
            </Nintex:ConfigurationPropertySection>

  <Nintex:DialogBody runat="server" id="DialogBody">
  </Nintex:DialogBody>

After I have deployed this custom action to SharePoint (All explained how in previous Nintex blog) my custom action when configured now shows the Error Handling section. I have already configured my custom action in the below screenshot.

You will need to create two workflow variables. I have created ErrorOccurred (Yes/No) and ErrorMessage (Single line of text).

Then in my workflow I have a “Set a condition” workflow action, that is configured to equal to true if ErrorOccured equals Yes. If no error occurs then I set my field value as I did previously before I include error handling, however if there is an error I log this error in the history list.

When running the workflow I have looked for a property bag key called NotThere. This has caused the workflow to error when retrieving the property from the property bag.

And below is the error in the history list.

You can download my updated SharePoint 2013 version of my ReadFromPropertyBag custom action with Error Handling from my OneDrive.

Creating Nintex Workflow Custom Actions


As promised in my previous blog, I am going to explain how to create your own custom action for Nintex Workflow.

(New Post Nintex Custom Action – Error Handling)

(Update: I have got this working in a 2013 SharePoint On Premise project, the C# code will indicate that some methods are deprecated now, but this is OK as Nintex Workflow still runs using the SharePoint 2010 Workflow engine, not the new 2013).

Nintex is a great product to create Workflows with within SharePoint. Much more flexible than SharePoint Designer, and far less complicated than using Visual Studio. There is an extensive list of Actions already for Nintex Workflow sometimes you need a Custom Action that is specific for your business such as bring back data from your bespoke application, or UNIX text file. This blog will explain the different parts of creating a custom action to you.

Above picture showing you collection of Actions that comes with Nintex Workflow.

To build a custom action, unfortunately, there is a collection of files you are require to create, even though there is only one real section that performs the actual business logic. The rest of it is all supporting code. Below is a basic project setup of all the files you need. I will explain each section throughout this post. With a walkthrough at the end of how to create a ReadFromPropertyBag Custom Action.

References

Before we can even start we require the following References.

  • System.Workflow.Activities.dll (Not shown in above picture. Forgot to add before took picture.)
  • Microsoft.SharePoint.dll
  • Microsoft.SharePoint.WorkflowActions.dll
  • System.Workflow.ComponentModel.dll
  • Nintex.Workflow.dll – Can be found at C:\Program Files\Nintex\Nintex Workflow 2010\Binaries.
  • Nintex.Workflow.ServerControls.dll – Can be found at C:\Program Files\Nintex\Nintex Workflow 2010\Binaries. (Not shown in above picture. Forgot to add before took picture.)

For SharePoint 2013, you will need to get the Nintex dll’s from the GAC. To do this you will need to browse to c:\Windows\Microsoft.NET\assembly\GAC_MSIL\ and find the Nintex DLL’s there.

Features

A WebApplication feature that when activated it will add the Custom Action to the Web Application and authorize it to be used within the web application.

CustomActions –ActionName – NWAFile

An element file, which holds an XML file which is all the required details for a NintexWorkflowActivity. It is this file that is read in by the Feature receiver to be able to add the custom action to the web application.

CustomActions – ActionName – ActionNameActivity

A class that is inherited by Nintex.Workflow.Activities.ProgressTrackingActivity. This file contains all the Dependency properties to the activity. The Dependency Properties are object properties that can be bound to other elements of a workflow, such as workflow variables or dependency properties of other activities. They are used to store the data that the activity will require or to output data from the activity. This is also the file that contains the execution of the actual business logic.

CustomActions – ActionName – ActionNameAdapter

A class that is inherited by Nintex.Workflow.Activities.Adapters.GenericRenderingAction. You will need to implement the abstract class of GenericRenderingAction. These implementations

  • GetDefaultConfig() – define the parameters that the user can configure for this action and sets the default label for the action.
  • ValidateConfig() – Adds logic to validate the configuration here, and will display any error messages to the user if there is an issue.
  • AddActivityToWorkflow() – Creates an instance of the Activity and set its properties based on config. Then it adds it to the parent activity.
  • GetConfig() – Reads the property from the context.Activity and update the values in the NWActionConfig.
  • BuildSummary() – Constructs an Action Summary class to display details about this action.

I find the Adapter is very similar for every Action. Once you have the basic of one, just by adding an extra parameter or removing one you can quickly put an adapter together for any Custom Action.

Layouts – NintexWorkflow – CustomActions – ActionName – Images

I have two icon .png files in here. One is sized at 49×49 pixels and the other at 30×30 pixels. These files are referenced in the NWAFile, and used to display the custom action to the user in the toolbox area (30×30), or in the actual workflow itself (49×49).

You could add a third one here for Warning Icon. This is where the custom action isn’t configured. This would be a 49×49 pixel too.

Layouts – NintexWorkflow – CustomActions – ActionName – ActionNameDialog

An application page inherited from Nintex.Workflow.ServerControls.NintexLayoutBase. The dialog is what appears to the user when they have to configure the custom action through the browser. Here there is no code behind. You mainly display the controls in the aspx and set up two javascript functions to read in and read out the configuration on load and save.

Walkthrough creating ReadFromPropertyBag Custom Action.

Now that you understand the basic roles of all the files required to make one custom action I will walk through creating a custom action that will read from the current SPWeb property bag. The user will pass in the “Property name” to obtain the value.

If you create a Solution with the similar layout my solution layout above, replacing “ActionName” with ReadFromPropertyBag. Your solution and file layouts should look similar to below.

ReadFromPropertyBagActivity.cs

Starting with the ReadFromPropertyBagActivity file. This inherits Nintex.Workflow.Activities.ProgressTrackingActivity. We will first add all the public static DependencyProperties. The default ones are __ListItem, __Context and __ListId. Then we will add 2 of our own, One to hold the the Property Name and the ResultOuput. You can delete the designer.cs file.

Each DependencyProperty will have its own public property.

using System;
using System.Workflow.ComponentModel;
using Microsoft.SharePoint.Workflow;
using Microsoft.SharePoint.WorkflowActions;
using Nintex.Workflow;
using Microsoft.SharePoint;
namespace CFSP.CustomActions.ReadFromPropertyBag
{
    public class ReadFromPropertyBagActivity : Nintex.Workflow.Activities.ProgressTrackingActivity
    {
        public static DependencyProperty __ListItemProperty = DependencyProperty.Register(&amp;quot;__ListItem&amp;quot;, typeof (SPItemKey), typeof (ReadFromPropertyBagActivity));
        public static DependencyProperty __ContextProperty = DependencyProperty.Register(&amp;quot;__Context&amp;quot;, typeof (WorkflowContext), typeof (ReadFromPropertyBagActivity));

        public static DependencyProperty __ListIdProperty = DependencyProperty.Register(&amp;quot;__ListId&amp;quot;, typeof (string),typeof (ReadFromPropertyBagActivity));
        public static DependencyProperty PropertyProperty = DependencyProperty.Register(&amp;quot;Property&amp;quot;, typeof (string), typeof (ReadFromPropertyBagActivity));
        public static DependencyProperty ResultOutputProperty = DependencyProperty.Register(&amp;quot;ResultOutput&amp;quot;, typeof (string), typeof (ReadFromPropertyBagActivity));

        public WorkflowContext __Context
        {
            get { return (WorkflowContext) base.GetValue(__ContextProperty); }
            set { base.SetValue(__ContextProperty, value); }
        }

        public SPItemKey __ListItem
        {
            get { return (SPItemKey) base.GetValue(__ListItemProperty); }
            set { base.SetValue(__ListItemProperty, value); }
        }

        public string __ListId
        {
            get { return (string) base.GetValue(__ListIdProperty); }
            set { base.SetValue(__ListIdProperty, value);}
        }

        public string Property
        {
            get { return (string) base.GetValue(PropertyProperty); }
            set { base.SetValue(PropertyProperty, value);}
        }

        public string ResultOutput
        {
            get { return (string) base.GetValue(ResultOutputProperty); }
            set {base.SetValue(ResultOutputProperty, value);}
        }

        public ReadFromPropertyBagActivity()
        {
        }

       protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
        }
        protected override ActivityExecutionStatus HandleFault(ActivityExecutionContext executionContext, Exception exception)
        {
        }
    }
}

Now we will need to override the Execute() method. This is the main business logic of your Custom Action.

   protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
            //Standard Nintex code to obtain context.
            ActivityActivationReference.IsAllowed(this, __Context.Web);
            NWWorkflowContext ctx = NWWorkflowContext.GetContext(
               this.__Context,
               new Guid(this.__ListId),
               this.__ListItem.Id,
               this.WorkflowInstanceId,
               this);

            base.LogProgressStart(ctx);
            //Get the property value.
            string resolvedProperty = ctx.AddContextDataToString(this.Property);

            var result = "";

            //Using the context get the property if it exists.
            if (ctx.Web.AllProperties.ContainsKey(resolvedProperty))
            {
                result = ctx.Web.AllProperties[resolvedProperty].ToString();
            }
            //store the result.
            this.ResultOutput = result;

            //End Execution.
            base.LogProgressEnd(ctx, executionContext);
            return ActivityExecutionStatus.Closed;
       }

The last thing we need to do in this class is to handle if there is a fault during execution. Overwrite the HandleFault code with the following. You can make the error say whatever you like. I’m just referencing the item that called the workflow.

protected override ActivityExecutionStatus HandleFault(ActivityExecutionContext executionContext, Exception exception)
        {
            Nintex.Workflow.Diagnostics.ActivityErrorHandler.HandleFault(executionContext, exception,
                this.WorkflowInstanceId, "Error Reading from Property Bag";, __ListItem.Id, __ListId, __Context);
            return base.HandleFault(executionContext, exception);
        }

ReadFromPropertyBagAdapter.cs

Moving onto the Adapter file now. This class inherits the Nintex.Workflow.Activies.Adapters.GenericRenderingAction and needs to implement 5 overrides. I have also included two private constants strings. These are the property names we declared in the Activity class. Ensure these names match, or you will encounter errors later which takes a while to debug.

using System;
using System.Collections.Generic;
using System.Workflow.ComponentModel;
using Microsoft.SharePoint;
using Nintex.Workflow;
using Nintex.Workflow.Activities.Adapters;

namespace CFSP.CustomActions.ReadFromPropertyBag
{
    public class ReadFromPropertyBagAdapter : GenericRenderingAction
    {
       //Values should match the property names in the ReadFromPropertyBagActivity class.
        private const string PropertyProperty = "Property";
        private const string ResultOutputProperty = "ResultOutput";

       public override NWActionConfig GetDefaultConfig(GetDefaultConfigContext context)
        {
            throw new NotImplementedException();
        }

        public override bool ValidateConfig(ActivityContext context)
        {
            throw new NotImplementedException();
        }
        public override CompositeActivity AddActivityToWorkflow(PublishContext context)
        {
            throw new NotImplementedException();
        }

        public override NWActionConfig GetConfig(RetrieveConfigContext context)
        {
            throw new NotImplementedException();
        }

        public override ActionSummary BuildSummary(ActivityContext context)
        {
            throw new NotImplementedException();
        }
   }
}

I will explain each override before showing you the code.

GetDefaultConfig sections allows you to set up the parameters for user input and outputs. If you wish the user to freely type a value use a PrimitiveValue. If you would like the user to use a predefined value that would be a variable somewhere in the workflow then use NWWorkflowVariable value. Typically the output would always be written back to a Workflow Variable so this will be a Variable type of NWWorkflowVariable. Add an ActivityParameter for each property.

public override NWActionConfig GetDefaultConfig(GetDefaultConfigContext context)
       {
            NWActionConfig config = new NWActionConfig(this);
            //define the number of parameters one for each custom parameter.
            config.Parameters = new ActivityParameter[2];
            //define the parameters that the user can configure for this action.
            config.Parameters[0] = new ActivityParameter();
            config.Parameters[0].Name = PropertyProperty;
            config.Parameters[0].PrimitiveValue = new PrimitiveValue();
            config.Parameters[0].PrimitiveValue.Value = string.Empty;
            config.Parameters[0].PrimitiveValue.ValueType = SPFieldType.Text.ToString();

            config.Parameters[1] = new ActivityParameter();
            config.Parameters[1].Name = ResultOutputProperty;
            config.Parameters[1].Variable = new NWWorkflowVariable();

            //set the default label for the action.
            config.TLabel = ActivityReferenceCollection.FindByAdapter(this).Name;
            return config;
       }

ValidateConfig section allows you to validate the values entered. Here I’m just ensuring the value are not blank. You would add a validation for each input property.

public override bool ValidateConfig(ActivityContext context)
{
            //Add logic to validate the configuration here.
            bool isValid = true;
            Dictionary<string, ActivityParameterHelper> parameters = context.Configuration.GetParameterHelpers();
            if (!parameters[PropertyProperty].Validate(typeof(string), context))
            {
                isValid &= false;
                validationSummary.AddError("Property Bag", ValidationSummaryErrorType.CannotBeBlank);
            }
            return isValid;
}

Validation is shown in image below.

AddActivityToWorkflow creates an instance of the Activity and set its properties based on config. You also bind the default properties. Assign each parameter you have here. Lastly attach the Activity Flags. Then add it all to the parent activity.

public override CompositeActivity AddActivityToWorkflow(PublishContext context)
        {
            Dictionary<string, ActivityParameterHelper> parameters = context.Config.GetParameterHelpers();
            ReadFromPropertyBagActivity activity = new ReadFromPropertyBagActivity();

            parameters[PropertyProperty].AssignTo(activity, ReadFromPropertyBagActivity.PropertyProperty, context);
            parameters[ResultOutputProperty].AssignTo(activity, ReadFromPropertyBagActivity.ResultOutputProperty, context);
            activity.SetBinding(ReadFromPropertyBagActivity.__ContextProperty, new ActivityBind(context.ParentWorkflow.Name, StandardWorkflowDataItems.__context));
            activity.SetBinding(ReadFromPropertyBagActivity.__ListItemProperty, new ActivityBind(context.ParentWorkflow.Name, StandardWorkflowDataItems.__item));

            activity.SetBinding(ReadFromPropertyBagActivity.__ListIdProperty, new ActivityBind(context.ParentWorkflow.Name, StandardWorkflowDataItems.__list));

            ActivityFlags f = new ActivityFlags();
            f.AddLabelsFromConfig(context);
            f.AssignTo(activity);

            context.ParentActivity.Activities.Add(activity);
            return null;
        }

GetConfig reads the properties from the context.Activity and updates the values in the NWActionConfig. Add a new parameter for each property. You can see when we RetrieveValue from our activity, we are grabbing the corresponding DependencyProperty from our activity.

public override NWActionConfig GetConfig(RetrieveConfigContext context)
        {
            //Read the properties from the context.ACtivity and update the values in the NWActionConfig

            NWActionConfig config = this.GetDefaultConfig(context);
            Dictionary<string, ActivityParameterHelper> parameters = config.GetParameterHelpers();
            parameters[PropertyProperty].RetrieveValue(context.Activity, ReadFromPropertyBagActivity.PropertyProperty, context);
            parameters[ResultOutputProperty].RetrieveValue(context.Activity, ReadFromPropertyBagActivity.ResultOutputProperty, context);

            return config;

        }

BuildSummary is the last implemented override method. The code here writes out the summary displayed to the user after that have configured the action and hovered the mouse over the custom action.

public override ActionSummary BuildSummary(ActivityContext context)
        {
           // Construct an ActionSummary class to display details about this action.

            Dictionary<string, ActivityParameterHelper> parameters = context.Configuration.GetParameterHelpers();
            return new ActionSummary("Retrieve the following Property bag: {0}", parameters[PropertyProperty].Value);

        }

BuildSummary is displayed below on mouse hover once item has been configured.

ReadFromPropertyBagDialog.aspx

The code behind for this aspx file inherits from Nintex.Workflow.ServerControls.NintexLayoutsBase. Apart from changing the inheriting type, there is no need to do anything else in the .cs file. In the aspx file we would have the basic structure. This structure contains the link up to your page behind, register all the Nintex controls required, the two main JavaScript functions to read and save the configuration, and lastly the display section of your page.

(Update:Please note to get this working for SharePoint 2013, please make sure ~/_layouts/NintexWorkflow is replaced with ~/layouts/15/NintextWorkflow)

<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>;
<%@ Page Language="C#" DynamicMasterPageFile="~masterurl/default.master" AutoEventWireup="true"; CodeBehind="ReadFromPropertyBagDialog.aspx.cs" EnableEventValidation="false"

    Inherits="CFSP.CustomActions.ReadFromPropertyBag.ReadFromPropertyBagDialog" %>
<%@ Register TagPrefix="Nintex" Namespace="Nintex.Workflow.ServerControls" Assembly="Nintex.Workflow.ServerControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=913f6bae0ca5ae12" %>;

<%@ Register TagPrefix="Nintex" TagName="ConfigurationPropertySection" src="~/_layouts/NintexWorkflow/ConfigurationPropertySection.ascx" %>

<%@ Register TagPrefix="Nintex" TagName="ConfigurationProperty" src="~/_layouts/NintexWorkflow/ConfigurationProperty.ascx&" %>;

<%@ Register TagPrefix="Nintex" TagName="DialogLoad" Src="~/_layouts/NintexWorkflow/DialogLoad.ascx" %>

<%@ Register TagPrefix="Nintex" TagName="DialogBody" Src="~/_layouts/NintexWorkflow/DialogBody.ascx" %>;

<%@ Register TagPrefix="Nintex" TagName="SingleLineInput" Src="~/_layouts/NintexWorkflow/SingleLineInput.ascx" %>

<%@ Register TagPrefix="Nintex" TagName="PlainTextWebControl" Src="~/_layouts/NintexWorkflow/PlainTextWebControl.ascx" %>

<asp:Content ID="ContentHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">

    <Nintex:DialogLoad runat="server" />
    <script type="text/javascript" language="javascript">
        function TPARetrieveConfig() {
       //To Do
        }

        function TPAWriteConfig() {
        //To Do
        }

        onLoadFunctions[onLoadFunctions.length] = function () {
            dialogSectionsArray["<%= MainControls1.ClientID >"] = true;
        };
    </script>
</asp:Content>

<asp:Content ID="ContentBody" ContentPlaceHolderID="PlaceHolderMain" runat="Server">
  <Nintex:ConfigurationPropertySection runat="server" Id="MainControls1">
             <TemplateRowsArea>
                   <!--ToDo-->
              </TemplateRowsArea>
 </Nintex:ConfigurationPropertySection>

  <Nintex:DialogBody runat="server" id="DialogBody">
  </Nintex:DialogBody>
</asp:Content>

First section we will fill in will be the Nintex:ConfigurationPropertySection within the ContentPlaceHolderID PlaceHolderMain. In here we need to create a Nintex:ConfigurationProperty for each configuration property. In our case here that will be the Property Bag Name and the Result. You can see from below for consistency I have given the ID of the controls the same name as the Dependency Properties. Also note that the output because I’m want the user to assign the results to a workflow property, I’m using the Nintex:VariableSelector control.

            <Nintex:ConfigurationProperty runat="server" FieldTitle="Property Bag Property" RequiredField="True">
 <TemplateControlArea>
 <Nintex:SingleLineInput clearFieldOnInsert="true" filter="number" runat="server" id="propertyProperty"></Nintex:SingleLineInput>
 </TemplateControlArea>
 </Nintex:ConfigurationProperty>
 <Nintex:ConfigurationProperty runat="server" FieldTitle="Result Output" RequiredField="False">
 <TemplateControlArea>
 <Nintex:VariableSelector id="resultOutput" runat="server" IncludeTextVars="True"></Nintex:VariableSelector>
 </TemplateControlArea>

 </Nintex:ConfigurationProperty>

Next we are going to look at the two JavaScript files. When the dialog page is rendered and saved it passed an XML file, known as the configXml. We need to read out and read into the XML file using XPath. Please note when you come to deploying, if you find that your dialog control loads, however the ribbon bar is disabled at the top of the dialog, it is most likely that you have an error in the JavaScript. This took me a while to diagnose, but now I know what causes the issue, it allowed me to fix it straight away.

From the TPARetrieveConfig code the [@Name=’ ‘] will always be the public property name you gave it in the ReadFromPropertyBagActivity.cs file. As you can see from the code below there is a different way to obtain the value depending if the configuration property is a PrimitiveValue or a WorkflowVariable. This you defined in the GetDefaultConfig() method within the ReadFromPropertyBagAdapter.cs file. Lastly if you are still having problems getting the value, ensure your XPath is correct by debugging the Javascript and viewing the configXML variable.

 function TPARetrieveConfig() {
 setRTEValue('<%=propertyProperty.ClientID%>', configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='Property']/PrimitiveValue/@Value").text);

 document.getElementById('<%=resultOutput.ClientID%>').value = configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='ResultOutput']/Variable/@Name").text;

 }

From the TPAWriteConfig code it is basically doing the opposite of TPARetrieveConfig, just it checks the dropdown control (resultOutput) that a value has been selected before saving.

 function TPAWriteConfig() {
 configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='Property']/PrimitiveValue/@Value").text = getRTEValue('<%=propertyProperty.ClientID%>');

 var resultOuputCtrl = document.getElementById('<%=resultOutput.ClientID%>');

 if (resultOuputCtrl.value.length > 0) {
 configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='ResultOutput']/Variable/@Name").text = resultOuputCtrl.value;
 }

 return true;
 }

ReadFromPropertyBagAction.nwa

The NWA file as stated previously is just an XML file. This file is used by the Feature Receiver to register the custom action within SharePoint WebApplication.

Add an Elements file to the project and name it NWAFile. Add an XML file and name it ReadFromPropertyBagAction.nwa. From the properties window (press F4) we need to change the build action from None to Content, change the deployment type to ElementFile and remove the Path “\NWAFile”.

From within the XML file, remove all text and then place the following in.

(Update for SharePoint 2013 don’t forget Layouts is now /_layouts/15/)

<NintexWorkflowActivity>
 <Name>Retrieve from Property Bag</Name>
 <Category>CannonFodder Category</Category>
 <Description>A custom action to retrieve a property from the SharePoint Web Property Bag.</Description>
 <ActivityType>CFSP.CustomActions.ReadFromPropertyBag.ReadFromPropertyBagActivity</ActivityType>
 <ActivityAssembly>$SharePoint.Project.AssemblyFullName$</ActivityAssembly>
 <AdapterType>CFSP.CustomActions.ReadFromPropertyBag.ReadFromPropertyBagAdapter</AdapterType>
 <AdapterAssembly>$SharePoint.Project.AssemblyFullName$</AdapterAssembly>
 <HandlerUrl>ActivityServer.ashx</HandlerUrl>
 <Icon>/_layouts/NintexWorkflow/CustomActions/ReadFromPropertyBag/Images/ReadFromPropertyBagIcon49x49.png</Icon>
 <ToolboxIcon>/_layouts/NintexWorkflow/CustomActions/ReadFromPropertyBag/Images/ReadFromPropertyBagIconSmall30x30.png</ToolboxIcon>
 <ConfigurationDialogUrl>CustomActions/ReadFromPropertyBag/ReadFromPropertyBagDialog.aspx</ConfigurationDialogUrl>
 <ShowInCommonActions>yes</ShowInCommonActions>
 <DocumentLibrariesOnly>no</DocumentLibrariesOnly>
</NintexWorkflowActivity>

Let me explain each line to you.

  • Name – The display name of the custom action
  • Category – The category in the toolbox area that the custom action will be displayed under.
  • Description – A description of the category.
  • ActivityType – The Namespace of the Activity.cs file.
  • ActivityAssembly – The Full assembly name. (I’m using a token, which I’ll show how to set up afterwards)
  • AdapterType – The Namespace of the Adapter.cs file.
  • AdapterAssembly – The full assembly name. (I’m using a token, which I’ll show how to set up afterwards)
  • HandlerUrl – The Nintex handler, this will always be ActivityServer.ashx
  • Icon – The URL to the larger Icon.
  • ToolboxIcon – The URL to the smaller icon.
  • WarningIcon – The URL to the Warning Icon <-Not used in the above XML.
  • ConfigurationDialogUrl – The URL to the Action Dialog file. Note that we don’t put /_layouts/NintexWorkflow at the front.
  • ShowInCommonActions – If this custom action shows up in CommonActions on the toolbox.
  • DocumentLibrariesOnly – If this custom action should only be used in DocumentLibraries or not.

Lastly add the NWA file to the Web Application Feature.

Getting the Token $SharePoint.Project.AssemblyFullName$ to replace on a build.

At this point, save and close your solution. Now open up your .csproj file in Notepad or Notepad++. At the bottom of your first <PropertyGroup> section add the following XML. Save the file and re-open your solution in Visual Studio.

<TokenReplacementFileExtensions>nwa</TokenReplacementFileExtensions>

When you build your solution, Visual Studio will replace your token with the actual Full Assembly Name. More information about TokenReplacementFileExtensions.

WebApplication – Custom Action EventReceiver.cs

The Feature Event receiver is the final piece to our custom action. This will deploy or remove our custom action and make it available to the Web Application by modifying the Web.Config and registering the Action with Nintex within the farm. To add the custom action we use the nwa file. To remove it we need to know the namespace of the adapter, and assembly name. As you build more custom actions you can reuse this feature and just de-activate and re-activate each time you deploy a new custom action.

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Nintex.Workflow;
using Nintex.Workflow.Administration;
using Nintex.Workflow.Common;
using System.Reflection;
namespace CFSP.CustomActions.Features.WebApplication___Custom_Actions
{
    [Guid(&amp;quot;07607091-449b-422b-94e4-84e6d863eb9e&amp;quot;)]
    public class WebApplication___Custom_ActionsEventReceiver : SPFeatureReceiver
    {
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            SPWebApplication parent = (SPWebApplication) properties.Feature.Parent;
            AddCustomAction(parent, properties, "ReadFromPropertyBagAction.nwa");
           //Add additional Custom Actions nwa files here.
        }
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            SPWebApplication parent = (SPWebApplication) properties.Feature.Parent;
            RemoveCustomAction(parent, properties,
                "CFSP.CustomActions.ReadFromPropertyBag.ReadFromPropertyBagAdapter",
                Assembly.GetExecutingAssembly().FullName);
           //Remove additional Custom Actions here.
        }

        protected void AddCustomAction(SPWebApplication parent, SPFeatureReceiverProperties properties,
            string pathToNWAFile)
        {
            // First step is register the action to the Nintex Workflow database
            XmlDocument nwaXml = GetNWADefinition(properties, pathToNWAFile);

            ActivityReference newActivityReference = ActivityReference.ReadFromNWA(nwaXml);

            ActivityReference action = ActivityReferenceCollection.FindByAdapter(newActivityReference.AdapterType,
                newActivityReference.AdapterAssembly);

            if (action != null)
            {
                // update the details if the adapter already exists
                ActivityReferenceCollection.UpdateActivity(action.ActivityId, newActivityReference.Name,
                    newActivityReference.Description, newActivityReference.Category,
                    newActivityReference.ActivityAssembly, newActivityReference.ActivityType,
                    newActivityReference.AdapterAssembly, newActivityReference.AdapterType,
                    newActivityReference.HandlerUrl, newActivityReference.ConfigPage,
                    newActivityReference.RenderBehaviour, newActivityReference.Icon, newActivityReference.ToolboxIcon,
                    newActivityReference.WarningIcon, newActivityReference.QuickAccess,
                    newActivityReference.ListTypeFilter);
            }
            else
            {
                ActivityReferenceCollection.AddActivity(newActivityReference.Name, newActivityReference.Description,
                    newActivityReference.Category, newActivityReference.ActivityAssembly,
                    newActivityReference.ActivityType, newActivityReference.AdapterAssembly,
                    newActivityReference.AdapterType, newActivityReference.HandlerUrl, newActivityReference.ConfigPage,
                    newActivityReference.RenderBehaviour, newActivityReference.Icon, newActivityReference.ToolboxIcon,
                    newActivityReference.WarningIcon, newActivityReference.QuickAccess,
                    newActivityReference.ListTypeFilter);
                action = ActivityReferenceCollection.FindByAdapter(newActivityReference.AdapterType,
                    newActivityReference.AdapterAssembly);
            }

            // Second step is to modify the web.config file to allow use of the activity in declarative workflows
            string activityTypeName = string.Empty;
            string activityNamespace = string.Empty;

            Utility.ExtractNamespaceAndClassName(action.ActivityType, out activityTypeName, out activityNamespace);
            AuthorisedTypes.InstallAuthorizedWorkflowTypes(parent, action.ActivityAssembly, activityNamespace,
                activityTypeName);
            // Third step is to activate the action for the farm
            ActivityActivationReference reference = new ActivityActivationReference(action.ActivityId, Guid.Empty,
                Guid.Empty);

            reference.AddOrUpdateActivationReference();
        }
        protected void RemoveCustomAction(SPWebApplication parent, SPFeatureReceiverProperties properties,
            string adapterType, string adapterAssembly)
        {
            ActivityReference action = ActivityReferenceCollection.FindByAdapter(adapterType, adapterAssembly);
            if (action != null)
            {
                // Remove the action definition from the workflow configuration database if the Feature is not activated elsewhere
                if (!IsFeatureActivatedInAnyWebApp(parent, properties.Definition.Id))
                    ActivityReferenceCollection.RemoveAction(action.ActivityId);
                string activityTypeName = string.Empty;
                string activityNamespace = string.Empty;
                Utility.ExtractNamespaceAndClassName(action.ActivityType, out activityTypeName, out activityNamespace);

                // Remove the web.config entry
                Collection<SPWebConfigModification> modifications = parent.WebConfigModifications;

                foreach (SPWebConfigModification modification in modifications)
                {
                    if (modification.Owner == AuthorisedTypes.OWNER_TOKEN)
                        // OWNER_TOKEN is the owner for any web config modification added by Nintex Workflow
                    {
                        if (IsAuthorizedTypeMatch(modification.Value, action.ActivityAssembly, activityTypeName,
                            activityNamespace))
                        {
                            modifications.Remove(modification);
                            parent.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
                           break;
                        }
                    }
                }
            }
        }
        private bool IsAuthorizedTypeMatch(string modification, string activityAssembly, string activityType,
            string activityNamespace)
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(modification);

            if (doc.FirstChild.Name == "authorizedType")
            {
                return (doc.SelectSingleNode("//@TypeName").Value == activityType
                        && doc.SelectSingleNode("//@Namespace").Value == activityNamespace
                        && doc.SelectSingleNode("//@Assembly").Value == activityAssembly);
            }
            return false;
        }

        private bool IsFeatureActivatedInAnyWebApp(SPWebApplication thisWebApplication, Guid thisFeatureId)
        {
            SPWebService webService = SPWebService.ContentService;
            if (webService == null)
                throw new ApplicationException("Cannot access ContentService");
            SPWebApplicationCollection webApps = webService.WebApplications;
            foreach (SPWebApplication webApp in webApps)
            {
                if (webApp != thisWebApplication)
                    if (webApp.Features[thisFeatureId] != null)
                        return true;
            }

            return false;
        }

        private XmlDocument GetNWADefinition(SPFeatureReceiverProperties properties, string pathToNWAFile)
        {
            using (Stream stream = properties.Definition.GetFile(pathToNWAFile))
            {
                XmlDocument nwaXml = new XmlDocument();
                nwaXml.Load(stream);
                return nwaXml;
            }
        }
   }
}

Deploying and checking everything has worked.

If you have done everything correctly, at the point go ahead and deploy your solution. Ensure your feature has been activated for a given web application. Then open Central Admin. Under the Nintex Workflow Management section select Manage allowed actions.

In Manage Allowed action you should see your Action, and that it is ticked. Meaning it is allowed to be used.

Let us go to our site now, and create a new Nintex Workflow for our custom list. My list has a Single line of text called Title and another one called PropertyValue. In the toolbar panel of my Nintex Workflow, I can now see my CannonFodder Category, and my custom action.

Drag this onto your page. If you find it doesn’t stick to your workflow page, go back and check your nwa file that all your Types and Assemblies match up correctly. Once it is on your page, configure this custom action. Your dialog will be presented to you.

Assign the Property Bag Property to the Item Property Title.

Create a new Workflow Variable and name this PBResult. Then assign Result Output to PBResult. Click Save on the dialog.


Under Libraries and List find the action Set field value and drag this onto the form underneath our custom action. Then configure it so that it sets our column PropertyValue to Workflow Data called PBResult that we created in the last step. Click Save on the dialog.

Lastly before we test this out, on the ribbon bar of the workflow page, under the Nintex Workflow 2010 tab, click Workflow Settings. Configure it so that it Starts when items are created.

Save and Publish the workflow.

Testing

I already have a value in my property bag called cann0nf0dderpb. So I’m going to create a new item in my list, and set the title to cann0nf0dderpb and save the form.

After a moment or two the workflow has kicked in. Once I refresh my list I can see that in PropertyValue, the value of my PropertyBag item is displayed. I purposely made the property bag value say ‘Nintex Workflow Worked’.

As I stated right at the start, there is a lot of files that need to be in place to plumb everything together, and it’s only the small Execute function that does the actual work. For further reading, please download the Nintex SDK

You can download this project directly from my OneDrive. – 2010

You can download this project directly from my OneDrive – 2013

Nintex.Workflow.Activities.Adapters.SPUpdateItemAdapter is not allowed on site


I’ve recently been working on the wonderful world of Nintex Workflow and forms. There will be a future post soon on Custom Actions.

However during my deployment to a test environment, I found that Nintex wasn’t performing the way it should. Mainly when I was activating a feature that contained a new workflow it failed to activate the feature. In the event logs all it said was:

Nintex.Workflow.Activities.Adapters.SPUpdateItemAdapter is not allowed on site http://cfsp.cannonfodder.local/system/NintexSite

After searching through the Nintex Forums I was unable to find any answers. In the end I had to resort to comparing my dev farm to my test farm.

First thing I noticed was that my Development domain had more entries in the Web.config than my test environment.


<WorkflowServices>

      <WorkflowService Class="Nintex.Workflow.Activities.Services.ReadWriteWorkflowVariablesService" Assembly="Nintex.Workflow, Version=1.0.0.0, Culture=neutral, PublicKeyToken=913f6bae0ca5ae12" />

       <WorkflowService Class="Nintex.Workflow.Activities.Services.WorkflowInstanceService" Assembly="Nintex.Workflow, Version=1.0.0.0, Culture=neutral, PublicKeyToken=913f6bae0ca5ae12" />

    </WorkflowServices>

This was a simple fix. The Nintex Workflow hadn’t been properly activated on the Web Application. Now I don’t know if there is a way to do this directly with a feature, but in Central Administration -> Nintex Workflow Management there is a link to Web Application activation. By clicking this link and selecting the correct Web Application and then clicking Activate, the web config will be correctly updated with the relevant configuration information required to run Nintex Workflow.

However, this didn’t solve my problem when I activated my feature. The next thing I compared between environments was Managed allowed actions. You can find this by going Central Administration -> Nintex Workflow Management -> Manage allowed actions

In allowed actions on my test environment I noticed that all items apart from the Custom Actions that I had added in a feature (how in a future post), we unchecked.

However in my Dev environment they were all checked. I therefore checked them all, Clicked OK on the page, and then went back to my site and retried to activate my feature. The feature activated without a problem.

Now I know what the problem was the error message makes a little sense.

Nintex.Workflow.Activities.Adapters.SPUpdateItemAdapter is not allowed on site – Each Custom Action does have an adapter, and the purpose of the Administrative page in Central admin is to allow the Custom Action to be used across the farm.