Custom Action con Nintex y SharePoint 2013

En esta entrada vamos a ver cómo crear una Custom Action para utilizar en flimageujos de Nintex. Esto puede ser necesario cuando necesitamos realizar alguna acción en el flujo que no podemos realizar con las propias que ya nos proporciona la herramienta.

Se puede consultar el SDK de Nintex para ver cómo realizar estas Custom Actions, aunque ha día de hoy, todavía no han liberado la de Nintex Workflow 2013.

Es un proceso que puede parecer complejo al principio. Yo recomiendo ir trazando el código para localizar los posibles errores que puedan surgir durante la ejecución posterior.

En esta caso, la Custom Action va a ser muy sencilla, simplemente vamos a crear un componente que logue excepciones en el ULS.

Vamos a empezar creando una solución de SharePoint 2013 vacía:

image

Para este proyecto, necesitaremos al menos las siguientes referencias a DLL:

Microsoft.SharePoint.WorkflowActions.dll

Nintex.Workflow.dll

Nintex.Workflow.ServerControls.dll

System.Workflow.Activities.dll

System.Workflow.ComponentModel.dll

image

Aunque luego pongo el esqueleto del proyecto, ahora vamos a seguir, creando una Feature con ámbito de Web Application para desplegar la Custom Action:

image

Lo siguiente es crear una estructura para el fichero NWA (la definición XML de la Custom Action), y las clases para la activity y el adapter, similar a la siguiente:

image

Y otra rama en la carpeta mapeada de Layouts para guardar los iconos y un formulario de configuración de la Custom Action, con una estructura similar a la siguiente (importante que la carpeta que cuelga de Layouts sea NintexWorkflow, que es donde se guardan los archivos de definición y recursos de la herramienta, y va a ir ahí en tiempo de ejecución):

image

Bien, vamos a ver un resumen de la estructura que necesitamos:

image

Tenemos por lo tanto los siguientes elementos:

  • Feature: Que va a ser necesaria para activar la Custom Action a nivel de Web Application. Esta feature debe llevar un Event Receiver donde colocaremos el código necesario para la correcta instalación de la definición de la Custom Action.
  • Un fichero NWA, que contendrá la definición XML de la Custom Action
  • Una clase Activity, que contendrá entre otras cosas la lógica que debe hacer nuestra Custom Action
  • Una clase Adapter, que lleva ciertas lógicas para guardar y obtener las propiedades de nuestra Custom Action
  • Las imáges de nuestra Custom Action. Por el momento son dos, la que se muestra en el panel de componentes, y la que se muestra una vez que la hemos insertado en el área de trabajo.
  • Un formulario ASPX, que pinta los controles para poder dar valor a las propiedades de nuestra Custom Action.
  • Y una clase Logger que contiene la lógica del loguer que queremos implementar.

Bien, vamos lo primero a implementar un loguer propio, que será el que llame nuestra activity. No tiene mucha importancia está clase, ya que es para el ejemplo, pero podría ser algo así:

image

Ahora vamos a implementar la activity. Lo primero es que esta clase debe heredar de la clase ProgressTrackingActivity. Además debe de tener una serie de propiedades definidas (__listItem, __Context, __ListId). Y también las propiedades que vayamos a usar en nuestra Custom Action; en esta caso añadimos una más (ULSMessage, que será el mensaje que trazaremos en el ULS).

public static DependencyProperty __ListItemProperty = DependencyProperty.
Register("__ListItem", typeof(SPItemKey), typeof(NintexULSLogActivity));
public static DependencyProperty __ContextProperty = DependencyProperty.
Register("__Context", typeof(WorkflowContext), typeof(NintexULSLogActivity));
public static DependencyProperty __ListIdProperty = DependencyProperty.
Register("__ListId", typeof(string), typeof(NintexULSLogActivity));

public static DependencyProperty ULSMessageProperty = DependencyProperty.
Register("ULSMessage", typeof(string), typeof(NintexULSLogActivity));

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 ULSMessage
{
get { return (string)base.GetValue(ULSMessageProperty); }
set { base.SetValue(ULSMessageProperty, value); }
}

Y en esta misma clase, sobrescribimos dos métodos, el Execute, que contiene la lógica de nuestra Custom Action, y el HandleFault, que será el que gestione qué ocurre cuando se produce un error:


protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
ActivityActivationReference.IsAllowed(this, __Context.Web);
NWWorkflowContext ctx = NWWorkflowContext.GetContext(
this.__Context,
new Guid(this.__ListId),
this.__ListItem.Id,
this.WorkflowInstanceId,
this);

base.LogProgressStart(ctx);

string resolvedProperty = ctx.AddContextDataToString(this.ULSMessage);
ULSLogger.Current.Trace(resolvedProperty);

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

protected override ActivityExecutionStatus HandleFault(ActivityExecutionContext executionContext, Exception exception)
{
Nintex.Workflow.Diagnostics.ActivityErrorHandler.HandleFault(executionContext, exception,
this.WorkflowInstanceId, "Error writing message in ULS Log", __ListItem.Id, __ListId, __Context);
return base.HandleFault(executionContext, exception);
}

Ahora vamos con la clase del adapter. En este caso heredará de GenericRenderingAction, y debe sobrescribir una serie de métodos. Esta es su estructura:


image


Debemos añadir al comienzo de esta clase las propiedades que creamos en el activity anterior, teniendo especial cuidado con que los nombres de las mismas coincidan, o sino luego tendremos problemas.


image


Y ahora vamos a ir implementando cada método de este adapter. Por un lado, el método GetDefaultConfig, que nos permitirá configurar los parámetros de entrada / salida que vamos a usar. En este caso tenemos uno de entrada, que es el mensaje que vamos a trazar en el ULS:


public override NWActionConfig GetDefaultConfig(GetDefaultConfigContext context)
{
NWActionConfig config = new NWActionConfig(this);
config.Parameters = new ActivityParameter[1];

config.Parameters[0] = new ActivityParameter();
config.Parameters[0].Name = ULSMessageProperty;
config.Parameters[0].PrimitiveValue = new PrimitiveValue();
config.Parameters[0].PrimitiveValue.Value = string.Empty;
config.Parameters[0].PrimitiveValue.ValueType = SPFieldType.Text.ToString();

config.TLabel = ActivityReferenceCollection.FindByAdapter(this).Name;
return config;
}

El siguiente, ValidateConfig, que gestiona la validación de nuestras propiedades. En este caso vamos a validar que el mensaje que se introduce no esté vacío:


public override bool ValidateConfig(ActivityContext context)
{
bool isValid = true;
Dictionary<string, ActivityParameterHelper> parameters = context
.Configuration.GetParameterHelpers();
if (!parameters[ULSMessageProperty].Validate(typeof(string), context))
{
isValid &= false;
validationSummary.AddError("ULS Message", ValidationSummaryErrorType.CannotBeBlank);
}
return isValid;
}

Ese mensaje de salida se verá en el área de trabajo de esta forma:


image


El siguiente, AddActivityToWorkflow, que será el que cree la instancia de nuestra activity cuando la insertamos en el área de trabajo.


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

parameters[ULSMessageProperty].AssignTo(activity,
NintexULSLogActivity.ULSMessageProperty, context);

activity.SetBinding(NintexULSLogActivity.__ContextProperty,
new ActivityBind(context.ParentWorkflow.Name, StandardWorkflowDataItems.__context));
activity.SetBinding(NintexULSLogActivity.__ListItemProperty,
new ActivityBind(context.ParentWorkflow.Name, StandardWorkflowDataItems.__item));
activity.SetBinding(NintexULSLogActivity.__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;
}

La siguiente, GetConfig, que recoge las propiedades del contexto de la actividad y las actualiza en el NWActionConfig.


public override NWActionConfig GetConfig(RetrieveConfigContext context)
{
NWActionConfig config = this.GetDefaultConfig(context);
Dictionary<string, ActivityParameterHelper> parameters = config.GetParameterHelpers();
parameters[ULSMessageProperty].RetrieveValue(context.Activity,
NintexULSLogActivity.ULSMessageProperty, context);

return config;
}

Y por último, BuildSummary, que muestra un mensaje de resumen cuando la actividad ha sido configurada en el área de trabajo.


public override ActionSummary BuildSummary(ActivityContext context)
{
Dictionary<string, ActivityParameterHelper> parameters =
context.Configuration.GetParameterHelpers();
return new ActionSummary("Write to ULS: {0}",
parameters[ULSMessageProperty].Value);
}

Este sería el resultado de este BuildSummary (al pasar el cursor sobre nuestra Custom Action ya configurada):


image


Una vez implementadas las dos clases, vamos a continuar con el formulario ASPX. Deberemos añadir las referencias a los componentes / controles de Nintex, así que registramos los siguientes:


<%@ 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/15/NintexWorkflow/ConfigurationPropertySection.ascx" %>
<%@ Register TagPrefix="Nintex" TagName="ConfigurationProperty" src="~/_layouts/15/NintexWorkflow/ConfigurationProperty.ascx" %>
<%@ Register TagPrefix="Nintex" TagName="DialogLoad" Src="~/_layouts/15/NintexWorkflow/DialogLoad.ascx" %>
<%@ Register TagPrefix="Nintex" TagName="DialogBody" Src="~/_layouts/15/NintexWorkflow/DialogBody.ascx" %>
<%@ Register TagPrefix="Nintex" TagName="SingleLineInput" Src="~/_layouts/15/NintexWorkflow/SingleLineInput.ascx" %>

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

Y continuamos insertando en nuestro PlaceHolderAdditionalPageHeader las funciones cliente que necesitamos para leer y escribir los valores en nuestras propiedades:


<asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
<Nintex:DialogLoad runat="server" />
<script type="text/javascript" language="javascript">
function TPARetrieveConfig() {
setRTEValue('<%=ULSMessageProperty.ClientID%>', configXml.selectSingleNode("/NWActionConfig/Parameters/Parameter[@Name='ULSMessage']/PrimitiveValue/@Value").text);
}

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

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

Y en el PlaceHolderMain metemos una sección para “pintar” los controles necesarios para rellenar los valores de nuestras propiedades. En este caso uso el control SingleLineInput, que muestra un botón para construir texto utilizando propiedades del contexto del flujo de trabajo.


<asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
<Nintex:ConfigurationPropertySection runat="server" Id="MainControls1">
<TemplateRowsArea>
<Nintex:ConfigurationProperty runat="server" FieldTitle="ULS Message" RequiredField="True">
<TemplateControlArea>
<Nintex:SingleLineInput clearFieldOnInsert="true" runat="server" id="ULSMessageProperty"></Nintex:SingleLineInput>
</TemplateControlArea>
</Nintex:ConfigurationProperty>
</TemplateRowsArea>
</Nintex:ConfigurationPropertySection>


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

</asp:Content>

Y por último, a la clase de este formulario ASPX le modificamos la clase de la que hereda por NintexLayoutBase:


image


Ahora vamos a configurar el fichero NWA, que será el que contenga la definición XML de la Custom Action. Lo primero será, en sus propiedades, cambiar la Build Action y otros aspectos. La configuración debe quedar así:


image


Y en ese fichero NWA definimos la Custom Action:


<NintexWorkflowActivity>
<Name>ULS Log</Name>
<Category>Custom Activities</Category>
<Description>Custom Log in ULS</Description>
<ActivityType>NintexLogAction.CustomActions.NintexULSLog.NintexULSLogActivity</ActivityType>
<ActivityAssembly>$SharePoint.Project.AssemblyFullName$</ActivityAssembly>
<AdapterType>NintexLogAction.CustomActions.NintexULSLog.NintexULSLogAdapter</AdapterType>
<AdapterAssembly>$SharePoint.Project.AssemblyFullName$</AdapterAssembly>
<HandlerUrl>ActivityServer.ashx</HandlerUrl>
<Icon>/_layouts/15/NintexWorkflow/CustomActions/NintexLogAction/Images/ULS49x49.png</Icon>
<ToolboxIcon>/_layouts/15/NintexWorkflow/CustomActions/NintexLogAction/Images/ULS30x30.png</ToolboxIcon>
<ConfigurationDialogUrl>/CustomActions/NintexLogAction/NintexULSLogDialog.aspx</ConfigurationDialogUrl>
<ShowInCommonActions>yes</ShowInCommonActions>
<DocumentLibrariesOnly>no</DocumentLibrariesOnly>
</NintexWorkflowActivity>

Como en algunas etiquetas hemos metido tokens para resolver el nombre del assembly completo, debemos añadir en nuestro proyecto una regla para que Visual Studio reemplace esos tokens al compilar nuestro proyecto. Para ello editamos el fichero .csproj:


image


image


Y justo antes de cerrar el primer PropertyGroup, añadimos la regla:



<TokenReplacementFileExtensions>nwa</TokenReplacementFileExtensions>


De forma que queda así:


image


image


Y por último el código de activación de la Feature. Básicamente se trata de registrar o de desregistrar nuestro fichero NWA que contiene la definición de Custom Action. El siguiente código realiza esas operaciones:


public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPWebApplication parent = (SPWebApplication)properties.Feature.Parent;
RegisterNWA(parent, properties, "NintexULSLogAction.nwa");
}


public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
SPWebApplication parent = (SPWebApplication)properties.Feature.Parent;
UnRegisterNWA(parent, properties,
"NintexLogAction.CustomActions.NintexULSLog.NintexULSLogAdapter",
Assembly.GetExecutingAssembly().FullName);
}


protected void RegisterNWA(SPWebApplication parent, SPFeatureReceiverProperties properties,
string pathToNWAFile)
{
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);
}

string activityTypeName = string.Empty;
string activityNamespace = string.Empty;

Utility.ExtractNamespaceAndClassName(action.ActivityType, out activityTypeName, out activityNamespace);
AuthorisedTypes.InstallAuthorizedWorkflowTypes(parent, action.ActivityAssembly, activityNamespace,
activityTypeName);

ActivityActivationReference reference = new ActivityActivationReference(action.ActivityId, Guid.Empty,
Guid.Empty);

reference.AddOrUpdateActivationReference();
}

protected void UnRegisterNWA(SPWebApplication parent, SPFeatureReceiverProperties properties,
string adapterType, string adapterAssembly)
{
ActivityReference action = ActivityReferenceCollection.FindByAdapter(adapterType, adapterAssembly);
if (action != null)
{
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);

Collection<SPWebConfigModification> modifications = parent.WebConfigModifications;

foreach (SPWebConfigModification modification in modifications)
{
if (modification.Owner == AuthorisedTypes.OWNER_TOKEN)
{
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;
}

}

Y ya está. Desplegamos el proyecto, y comprobamos si nuestra Custom Action se ha creado y registrado correctamente. Para ello, desde la Administración Central vamos a las Manage allowed actions.


image


Y vemos que efectivamente está registrada:


image


Así que creamos un flujo y hacemos una prueba rápida:


image


Y nuestro diálogo ASPX de configuración:


image


Y el resultado al ejecutarlo sobre una lista cualquiera:


image


 


Espero que haya servido de ayuda.

SharePoint Between Racks © 2012
. Con la tecnología de Blogger.

¡Compártelo!


Estoy en LinkedIn!


Ve mi perfil en LinkedIn!