
This article introduces development of custom code activities that can be deployed to a SharePoint server farm. This greatly increases the scope of use of workflows in SharePoint by making it possible to publish activities that extend the built-in capabilities of the solution.
Although activity libraries can theoretically be published by authoring just one code project, it is much easier to specify the workflow activities in one assembly and then have a separate project that registers the activities and publishes the package.
To start, create a new Visual Studio project (new solution) as a SharePoint Empty Project (Project templates, Office/SharePoint, SharePoint Solutions, and then SharePoint 2010 – Empty Project or SharePoint 2013 – Empty Project depending on what version you are targeting.) Give the project a sensible name like MySharePointCustomActivitiesPackage. When prompted for the scope of deployment, choose Farm.
Once the default project files have been created, add a 2nd project to the solution (Templates, Workflow, Workflow Activity Library.) Again give the project a sensible name like MySharePointCustomActivities.
Make sure the activities library is strong named by signing it with a key (via the project properties.) This will already have been set up for the package project.
Authoring Your Activities
Add the Microsoft.SharePoint and Microsoft.SharePoint.WorkflowActions references to the activities library project.
The activities library should already have an activity class defined. Rename this as appropriate and then change the type it inherits from to ‘Activity’ (instead of SequenceActivity which is likely to be the default case.)
Data is passed into activities using dependency injection. For an activity called ‘MyActivity’ you can define dependency properties in the activity’s code behind file like so…
public static DependencyProperty MyParameterProperty =
DependencyProperty.Register("MyParameter", typeof(string), typeof(MyActivity));
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[ValidationOption(ValidationOption.Required)]
public string MyParameter
{
get { return (string)base.GetValue(MyParameterProperty); }
set { base.SetValue(MyParameterProperty, value); }
}
Code language: C# (cs)
In this case the parameter data type is a string. You can add a default value for the parameter by adding a PropertyMetadata object after the activity type parameter in the method call, like so…
public static DependencyProperty MyParameterProperty =
DependencyProperty.Register("MyParameter", typeof(string), typeof(MyActivity),
new PropertyMetadata("default value"));
Code language: C# (cs)
Also notice that the public property can be decorated with various attributes, such as making the parameter visible in designers (such as SharePoint Designer 20XX) and specifying that the parameter is a required value for the activity.
As a general principle, its always useful to inject the workflow context into the activity.
public static DependencyProperty __ContextProperty =
DependencyProperty.Register("__Context", typeof(WorkflowContext),
typeof(MyActivity));
public WorkflowContext __Context
{
get { return (WorkflowContext)base.GetValue(__ContextProperty); }
set { base.SetValue(__ContextProperty, value); }
}
Code language: C# (cs)
The next step is to override the method that does the work in the activity class…
protected override ActivityExecutionStatus Execute(
ActivityExecutionContext executionContext)
{
SPSecurity.RunWithElevatedPrivileges(delegate ()
{
// TODO: Your activity code goes here!
});
}
Code language: C# (cs)
You can write messages to the workflow execution history via the execution context. This can be useful for general execution progress reporting and when things go wrong with your activity (altering the parameters passed into the method as necessary.)
ISharePointService service =
(ISharePointService)context.GetService(typeof(ISharePointService));
service.LogToHistoryList(__Context.WorkflowInstanceId,
SpWorkflowHistoryEventType.WorkflowComment, 0, TimeSpan.Zero, "Info (Title)",
"some message", "");
Code language: C# (cs)
Once you’ve written your activity code, you can compile your activities library and move onto setting up the package details.
Getting Ready to Publish Your Activities Library
Switch to the package project (the first one you created in the solution.)
Various things need to be set up before publishing your package to SharePoint. Most notably, you need to reference the activities library in your package project. Do this now.
In order to make your activities available you have to define a ‘.actions’ file in a specific SharePoint folder. Right click on the project node in Solution Explorer and choose the Add, SharePoint Mapped Folder… option. Then select the {SharePointRoot}\TEMPLATE\1033\Workflow folder. In Solution Explorer, create an XML file in the mapped (Workflow) folder using the name of the activities library with a ‘.actions’ extension. For example: MySharePointCustomActivities.actions.
This is an XML file that is used to define the activity configuration. For example:
<?xml version="1.0" encoding="utf-8" ?>
<WorkflowInfo>
<Actions>
<Action Name="Activity Display Name"
ClassName="MySharePointCustomActivities.MyActivity"
Assembly="MySharePointCustomActivities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=inserttokenhere"
AppliesTo="all"
Category="MY Activities">
<RuleDesigner Sentence="Update current item status to %1">
<FieldBind Field="MyParameter" Text="enter status" DesignerType="TextArea" Id="1" />
</RuleDesigner>
<Parameters>
<Parameter Name="__Context"
Type="Microsoft.SharePoint.WorkflowActions.WorkflowContext"
Direction="In" />
<Parameter Name="MyParameter" Type="System.String, mscorlib"
Direction="In" />
</Parameters>
</Action>
</Actions>
</WorkflowInfo>
Code language: HTML, XML (xml)
Aside: notice that the workflow context property is defined in the parameters section only.
The above snippet will declare MyActivity and specify the signature for its use (the ‘sentence’ in the rule designer section.) This is what will be displayed in the workflow designer to assist an author with configuring the activity.
Now add the activities library as an ‘additional assembly’ of the package, and set it up so it can run (by registering in SharePoint’s ‘SafeControls’ section of the web.config when the package is installed.)
Expand the Package folder in Solution Explorer and then double click on the Package.package node. In the package details window, click the Add button to add an assembly, and choose the Add Assembly from Project Output… option, and then choose the activities library.
Within the Safe Controls region, you are going to need to add the details in manually. Specify the namespace, leave the type name as *, specify the full assembly name including version, public key token, etc. and then tick the Safe and Safe Against Script options. Click OK to confirm and then save all the changes.
HINT: If you want to get the public key token of an assembly, open a Visual Studio command prompt, navigate to the build output folder (e.g. bin/Debug) of your activities library and type:
sn.exe -T assembly-filename
Code language: plaintext (plaintext)
The next thing to do is publish the activities library as a feature so it is properly registered with SharePoint.
Once the library has been referenced, right click on the Features folder in Solution Explorer and choose Add Feature. In the feature details window, choose either WebApplication or Farm as the scope so the activities are available across the entire SharePoint Web Application where the package is installed (or across the entire server farm, as appropriate.)
You can rename the feature to something more meaningful if you want.
If, at this point, you know your activities need some additional configuration then you can set up web.config modifications. This can be done declaratively or programmatically. There are benefits and drawbacks of each approach so we will look at both.
First of all, we’re going to need to add the activities library to the list of authorised types in the SharePoint web application (or server farm.) It’s easiest to do this programmatically by adding an event receiver to the feature we just created. Right click on the Feature1.feature node (or yourName.feature if you renamed it) and then choose Add Event Receiver.
There are a few different event receiver methods, all initially commented out, that serve different purposes.
- FeatureActivated – called whenever a workflow loads the assembly to execute an activity.
- FeatureDeactivating – called when the activity has finished executing and the feature is deactivating.
- FeatureInstalled – called when the package is installed on SharePoint.
- FeatureUninstalling – called when the package is being retracted from SharePoint.
- FeatureUpgrading – called when the package is being upgraded to a newer version.
All the methods declare a ‘properties’ parameter. It is worth mentioning at this time that the constituent properties within the ‘properties’ parameter may not always be initialised. For example the Feature and UserCodeSite properties are always null in the installed/uninstalling methods.
Let’s say we want to authorise our activities library for use by SharePoint, and also add a couple of application settings to the web.config to configure our activity.
Uncomment the FeatureInstalled method and add the following code…
public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
Assembly myAssembly = typeof(MyActivity).Assembly;
SPWebService webApp = SPWebService.ContentService;
// Authorise the activities library...
webApp.WebConfigModifications.Add(
new SPWebConfigModification(
string.Format("authorizedType[@Assembly='{0}']", myAssembly.GetName().Name),
"configuration/System.Workflow.ComponentModel.WorkflowCompiler" +
"/authorizedTypes")
{
Owner = myAssembly.GetName().Name,
Sequence = 0,
Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode,
Value = string.Format(
"<authorizedType Assembly=\"{0}\" Namespace=\"{1}\" TypeName=\"*\"" +
"Authorized=\"True\" />",
myAssembly.FullName,
typeof(MyActivity).Namespace)
}
);
// Make sure the appSettings section of the web.config exists.
webApp.WebConfigModifications.Add(
new SPWebConfigModification("appSettings", "configuration")
{
Owner = "ME",
Sequence = 0,
Type = SPWebConfigModification.SPWebConfigModificationType.EnsureSection
}
);
// Add info to the appSettings section of the web.config.
webApp.WebConfigModifications.Add(
new SPWebConfigModification(
"add[@key='EnvironmentName']", "configuration/appSettings")
{
Owner = "ME",
Sequence = 1,
Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode,
Value = "<add key=\"EnvironmentName\" value=\"Dev\" />"
}
);
webApp.ApplyWebConfigModifications();
webApp.Update();
}
public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
// De-authorise the activities library...
Assembly myAssembly = typeof(MyActivity).Assembly;
SPWebService webApp = SPWebService.ContentService;
List<SPWebConfigModification> changes =
webApp.WebConfigModifications
.Where(x => x.Owner == myAssembly.GetName().Name)
.ToList();
foreach (SPWebConfigModification change in changes)
{
webApp.WebConfigModifications.Remove(change);
}
webApp.ApplyWebConfigModifications();
webApp.Update();
}
Code language: C# (cs)
In the above method bodies, we have set up routines that authorise the activities library when the package is installed, and revoke authority when it is retracted. During installation, the routine also makes sure the appSettings section of the web.config file exists and then adds an ‘EnvironmentName’ setting to it.
Web configuration file changes can also be made using supplemental XML mark-up files. This can be useful when deploying your activities library in different scopes (development, test, production, etc.) In this way a default mark-up file can be installed with the package and it can be overwritten with an environment-specific one. The changes are then applied and the activities library will pull in the correct settings for the environment.
To use supplemental web-config files, we need to map another SharePoint folder. Right click the package project and choose Add, SharePoint Mapped Folder…, and then choose {SharePointRoot}\CONFIG.
Create a new XML file within this mapped folder. SharePoint expects a particular naming convention of the form webconfig.yourname.xml (e.g. webconfig.MySharePointCustomActivities.xml.)
Once you’ve created the file, you can define web.config changes within it.
<?xml version="1.0" encoding="utf-8" ?>
<!-- Apply changes with stsadm -o copyappbincontent in an elevated SharePoint Management Shell session. -->
<actions>
<remove path="configuration/appSettings/add[@key='EnvironmentName']"/>
<add path="configuration/appSettings">
<add key="EnvironmentName" value="TEST01" />
</add>
<update path="configuration/connectionStrings/add[@Name='MyDB']">
<add name="MyDB" connectionString="Server=TEST01\MSSQL;Database=MyStuff;Trusted_Connection=yes;" />
</update>
</actions>
Code language: HTML, XML (xml)
The XML snippet above demonstrates all 3 types of declarative modification that can be performed: add, update, remove. A drawback of declarative changes compared to programmatic ones is that they are only applied when the stsadm command described at the top of the snippet is run in a SharePoint Management Shell session, or when the SharePoint Web Application is restarted. A drawback of the programmatic approach is that they are only applied when run – and could be undone by the installation or running of other packages (especially if those other packages do anything dodgy like clear the web.config modifications list.)
All that is left to do now is deploy the completed package.