Developing workflows
Note: This topic has no later version.
The workflow extensions in EPiServer CMS are based on Windows Workflow Foundation 3.5 (WF). Workflows in EPiServer will change in coming versions of the platform due to the upgrade of WF by Microsoft (in .NET 4.5). Note that from update 40 (CMS core 7.15.0), workflows are disabled by default. Refer to Activating workflows for information on how to enable them.
Table of contents
- Introduction
- Configuring the workflow system
- Developing workflows
- Registering a workflow
- Using APIs
- Tips and tricks
- See also
Introduction
This document describes the EPiServer CMS implementation of Windows Workflow Foundation and attempts to explain how you, as a developer, can take advantage of the wide variety of possibilities that workflows provide.
Configuring the workflow system
The configuration of the workflow system is contained in the web.config file and is configured by directly editing the file.
- workflowRuntime. The configuration of the workflow system is contained in the <workflowRuntime> section of the web.config file. In this section, you can register services with the runtime, for example, persistance service and tracking service. The default persistance service is SqlWorkflowPersistanceService which uses SQL Server.
- workflowSettings. The EPiServer-specific workflow settings are configured in the <workflowSettings> section. Here you can configure which workflow definitions should be predefined (in the <definitions> element). Custom services can also be registered here (in the <externalServices> element).
Sample from EPiServerSamples web.config file showing the workflowRuntime section.
<workflowRuntime>
<Services>
<add type="System.Workflow.Runtime.Hosting.DefaultWorkflowSchedulerService,
System.Workflow.Runtime,
Version=3.0.00000.0,
Culture=neutral,
PublicKeyToken=31bf3856ad364e35"
maxSimultaneousWorkflows="5"/>
<add type="System.Workflow.Runtime.Hosting.SharedConnectionWorkflowCommitWorkBatchService,
System.Workflow.Runtime,
Version=3.0.00000.0,
Culture=neutral,
PublicKeyToken=31bf3856ad364e35" />
<add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService,
System.Workflow.Runtime,
Version=3.0.0.0,
Culture=neutral,
PublicKeyToken=31bf3856ad364e35"
UnloadOnIdle="true" />
</Services>
</workflowRuntime>
Sample from EPiServerSamples web.config file showing the workflowSettings section.
<workflowSettings
workflowDelayedLoading="false">
<workflowHost
type="EPiServer.WorkflowFoundation.AspNetWorkflowManager,
EPiServer.WorkflowFoundation" />
<definitions>
<!-- definition:
Workflow definitions that should be predefined, that is if no definition with specified type exists it will be created-->
<definition
name="Sequential Approval"
description="A sequential approval workflow for pages"
type="EPiServer.WorkflowFoundation.Workflows.SequentialApproval,
EPiServer.WorkflowFoundation"/>
<definition
name="Parallel Approval"
description="A paralell approval workflow for pages"
type="EPiServer.WorkflowFoundation.Workflows.ParallelApproval,
EPiServer.WorkflowFoundation"/>
<definition
name="Request for feedback"
description="Assigns request for feedback tasks to users/roles"
type="EPiServer.WorkflowFoundation.Workflows.RequestForFeedback,
EPiServer.WorkflowFoundation"/>
<definition
name="Ready for translation"
description="Assigns translation tasks to users/roles"
type="EPiServer.WorkflowFoundation.Workflows.ReadyForTranslation,
EPiServer.WorkflowFoundation"/>
</definitions>
<externalServices>
<!-- externalService:
Custom services that is to be registered with workflow runtime-->
<externalService
type="EPiServer.WorkflowFoundation.Workflows.ApprovalService,
EPiServer.WorkflowFoundation"/>
<externalService
type="EPiServer.WorkflowFoundation.Workflows.ReadyForTranslationService,
EPiServer.WorkflowFoundation"/>
</externalServices>
<references>
<!-- reference:
References for xoml based workflows, used at compiling of xoml based workflows-->
<!-- reference path="C:\Inetpub\wwwroot\mysite\bin\customdependency.dll" /-->
</references>
</workflowSettings>
Developing workflows
Workflow development is typically done using Visual Studio with Windows Workflow Foundation as your tool.
Activities
In EPiServer.WorkflowFoundation, there are a number of EPiServer-related activities defined. To use them within Visual Studio, select the menu option Tools > Choose Toolbox Items. On the .NET Framework Components tab, browse to locate the EPiServer.WorkflowFoundation.dll and add it to your Visual Studio toolbar.
Event activitiesWhen a workflow instance enters an event activity (that is, any activity derived from HandleExternalEventActivity), the workflow instance will become idle, persisted and finally unloaded from memory. The workflow host will keep track of which workflow instances are waiting for events. When the event occurs, the host will then load the workflow instance from persistence storage and invoke the event to the workflow instance.
Page-related event activities
These activities relate to events from DataFactory. The argument to page-related event activities is EPiServer.WorkflowFoundation.Activities.WorkflowPageEventArgs which contains the PageReference to the page. It also contains a CacheKey which can be used to get the original PageEventArgs from DataFactory.
For a list of page related event activities, see the EPiServer.WorkflowFoundation.Activities namespace.
File system-related event activitiesThese activities relate to events from the EPiServer file system. The argument to file system related event activities is WorkflowFileSystemEventArgs which for example contains the virtual path to the file or the directory.
For a complete list of file-related event activities, see the EPiServer.WorkflowFoundation.Activities namespace.
Task-related activitiesThe following task-related activities can be found in the EPiServer.WorkflowFoundation.Activities namespace:
- EPiServer.WorkflowFoundation.Activities.CreateTask
- EPiServer.WorkflowFoundation.Activities.UpdateTask
- EPiServer.WorkflowFoundation.Activities.DeleteTask
- EPiServer.WorkflowFoundation.Activities.DeleteTasksForInstance
- EPiServer.WorkflowFoundation.Activities.OnTaskDeleted
- EPiServer.WorkflowFoundation.Activities.OnTaskStatusChanged
- EPiServer.WorkflowFoundation.Activities.AssociateWithPage
- EPiServer.WorkflowFoundation.Activities.AddHistoryItem
- EPiServer.WorkflowFoundation.Activities.AddUserData
- EPiServer.WorkflowFoundation.Activities.SendEmailActivity
Custom activities
Apart from the built-in activities, it is also possible to develop your own custom activities and use them within the EPiServer workflow system. Activities that are used to send data from the host (residing outside a running workflow instance) to running instances or from an instance to the outside are primarily developed using an interface decorated with the ExternalDataExchange attribute.
A suggested way to create your own custom activities that pass data to/from workflow instances is to begin with creating an interface with the attribute ExternalDataExchange where events (event argument should be inherited from ExternalDataEventArgs) are defined for each event activity (which means that data is being sent from a host to a workflow instance). A method needs to be defined for each method that will be an Invoke-based activity (which means that data is sent from a workflow instance to a host). After creating your interface, the tool wca.exe can be used to generate activities from the interface definition (visit Windows Workflow Foundation Tools at MSDN.com).
You handle these custom activities on the host side by implementing a class that implements the custom interface specified above and register the class with the ExternalDataExchange service. This is done by deploying the assembly to the "~bin" directory of the site and then adding the class to the externalServices section in the web.config file.
To associate a task with a custom activity, the property AssociatedActivity on the CreateTask activity can be used. In this case, the custom activity should have an ActivityPlugIn attribute, specifying a URL to a user control (the user control must be deployed to specified URL before it can be loaded).
If you have custom event activities that you want to use outside tasks and you wish to be able to keep track of which instances that are currently waiting for the custom event, you can use your custom event activity as template parameter to EPiServer.WorkflowFoundation.Activities.CompositeCustomBase<T>. Then the EPiServer.WorkflowFoundation.Services.EventTrackingService will keep track over which instances that is currently registered with the event meaning you can query for waiting instances like EventTrackingService.GetInstancesForEvent ("<InterfaceName>.<EventName>").
Start parameters
It is possible to define properties on a workflow that can be used to set initial values on the workflow instance when it is created (or to get output data from a workflow instance when it is completed). The property should be defined on the workflow class and have publicly declared get/set accessors. The property type should be serializable and registered with Object Store.
The values passed to workflow instances as start arguments can be set through the API either by manually starting an instance through the method StartInstance on the IInstanceHandler interface or by an event delegate to event InstanceStarting on IInstanceHandler. Start parameters can also be set through a user interface, which is described in section below.
The values passed in the StartParameters dictionary must match the properties on the workflow class both regarding name and type.
User interfaces
A workflow might need input from a user, for example, when something of the following occurs:
- A workflow definition is registered and predefined values on start parameters are defined.
- A new instance of a workflow definition is to be created and start parameters for the instance are needed.
- The workflow instance is currently on an event-based activity and waits for input, for example, an approval event EPiServer.WorkflowFoundation.Workflows.Activities.ApprovalEvent.
If you wish to enable users to interact with the workflow instance, you can do so by associating a workflow and/or an event-based activity with a user control. See the following sections below: Configure Start Parameters and Interact With Event Activities for more information on how this is done.
Configuring start parameters
It is possible to associate a user control with a workflow class so that a user can assign values that will be passed to the workflow instance when it is created. These values can be assigned either at definition time (at the workflow configuration page in admin view) or when the instance is about to be created from an action window. The user control is associated with the workflow class by using a WorkflowPlugIn attribute where the path to the user control is specified (before usage the user control has to be deployed to the specified path).
Example on associating a user control with a workflow.
[WorkflowPlugIn(Area = EPiServer.PlugIn.PlugInArea.None,
Url = "~/UI/edit/ApprovalStartParams.ascx")]
public sealed partial class SequentialApproval : StateMachineWorkflowActivity
The specified control should implement the interface EPiServer.WorkflowFoundation.UI.IWorkflowStartParameterHandler.
The interface has two methods LoadStartParameters and SaveStartParameters which are defined as:
void LoadStartParameters( Guid definitionId,
PageReference pageLink,
bool definitionMode,
IDictionary<string, object> parameters);
IDictionary<string, object> SaveStartParameters();
LoadStartParameters will be called when the control is loaded, initializing the UI with the previously saved values. The following parameters are used:
Parameter name | Parameter definition |
---|---|
definitionId | Specifies which definition the start parameters are set for. |
pageLink | Specifies which page the instance is associated with (if any). |
definitionMode | Specifies whether the control is loaded in configuration (admin view) mode (true) or if it is loaded in action window (false). |
EPiServer.WorkflowFoundation.UI.IWorkflowStartParameterHandler.SaveStartParameters is called when the user calls either Save (in admin view) or Start (action window). Before this function is called, another call is made to Page.Validate (System.Web.UI.Page.Validate) meaning any validator on the user control will get a chance to validate user input. The items in the returned dictionary (types have to be serializable and registered with Object Store) will be validated at start time against the workflow class, which means that only entries that match both name and type of a public get/set property on the workflow class will be passed to the workflow instance.
If a workflow instance is set to be created automatically from a page-related event, information of the event will be sent to the workflow instance if the workflow class has a PageArgs property of the workflowPageArgs type as specified below:
public static DependencyProperty PageArgsProperty =
DependencyProperty.Register("PageArgs",
typeof(WorkflowEventArgs),
typeof(SequentialApproval));
public workflowPageEventArgs PageArgs
{
get { return (WorkflowPageEventArgs)this.GetValue(SequentialApproval.PageArgsProperty); }
set { this.SetValue(SequentialApproval.PageArgsProperty, value); }
}
If a workflow instance is set to be created automatically from an EPiServer file system-related event, information of the event will be sent to the workflow instance if the workflow class has a FileSystemArgs property of the workflowFileArgs type as specified below:
private WorkflowFileSystemEventArgs _fileSystemArgs;
public WorkflowFileSystemEventArgs FileSystemArgs
{
get { return _fileSystemArgs; }
set { _fileSystemArgs = value;}
}
If a workflow instance is set to be created automatically when another workflow is completed, values from all public properties on the completed workflow instance will be passed to the created instance.
Interacting with event activities
It is possible to interact with a custom event based activity by associating a UI through the use of the attribute ActivityPlugIn as specified below:
[ActivityPlugIn(Area = EPiServer.PlugIn.PlugInArea.None,
Url = "~/UI/edit/ApprovalEventControl.ascx")]
[ToolboxItemAttribute(typeof(ActivityToolboxItem))]
public partial class ApprovalEvent : HandleExternalEventActivity
{
...
...
}
When using such an activity in a workflow you should first create a task in EPiServer using the EPiServer.WorkflowFoundation.Activities.CreateTask activity. On the CreateTask activity you can set the AssociatedActivity property to the event activity. When the person (or group) who is assigned the task clicks on the task, the specified user control will be loaded and the user can then submit values to the custom event activity in the workflow instance (if a task has no associated activity specified then all event activities currently waiting on the workflow instance will be searched for an ActivityPlugIn attribute).
The user control should implement the interface EPiServer.WorkflowFoundation.UI.IWorkflowTaskControl, which has the methods ContextData, InvokeEvent, PageHasChanged and the property InvokeButtonText. ContextData is specified as below:
void ContextData(
Guid workflowInstanceId,
int taskId,
string eventQualifiedName);
ContextData is called when the control is loaded, it is used to set the context of the invoked event, containing the following information:
- Which task it is
- Which workflow instance it is
The qualified name of the event-based activity that specified the user control (given in attribute above) is eventQualifiedName. Which event a workflow instance is currently waiting for is examined by looking in the EventQueue for the workflow instance. It is possible to point out which activity is related to which task on the property AssociatedActivity on CreateTaskActivity (normally this will not be needed, but if an instance is waiting for several events at the same time, this can be used to distinguish which event the task is related to).
EPiServer.WorkflowFoundation.UI.IWorkflowTaskControl.InvokeEvent is called when the user clicks the Send button. Before the InvokeEvent is called, a call is made to Page.Validate() which means that any specified validator on the user control will be called to ensure that user input can be validated. If the page has been changed in edit view since ContextData is called method, PageHasChanged will also be called before InvokeEvent is called. In the implementation of InvokeEvent, the associated service registered with runtime should be fetched and then the corresponding event should be invoked on the service.
Example implementation of InvokeEvent.
public bool InvokeEvent(Guid workflowInstanceId, int taskId, string qualifiedName)
{
ApprovalService service = (ApprovalService)WorkflowSystem.WorkflowManager.GetService_
(typeof(ApprovalService));
service.InvokeApprovalEvent(new ApprovalEventArgs(Approved.Checked,
currentUser,
Message.Text,
workflowInstanceId));
return true;
}
The return value of the method indicates whether the task should be deleted or not.
InvokeButtonText states which text should be displayed on the Send button. If string.Empty is returned, the default value “Send” (language dependant) will be used.
CacheService
All data that is sent as arguments to activities must be serializable. The cache service is a convenient way to share non-serialized data from outside into workflow instances. You can send custom data to the cache service as described below.
CacheService cacheService = (CacheService)WorkflowSystem.WorkflowManager.GetService_
(typeof(CacheService));
string cacheKey = cacheService.AddItemToCache(cacheItem);
Then the cache key can be sent to a workflow instance as part of a custom event argument. The cache service can be achieved on the workflow instance by overriding Initialize on the workflow class as described below.
CacheService _cacheService;
protected override void Initialize(IServiceProvider provider)
{
base.Initialize(provider);
_cacheService = (CacheService)provider.GetService(typeof(CacheService));
}
Registering a workflow
Before instances can be created, the workflow has to be defined with EPiServer CMS. This is done in the admin view under workflows. Part of the definition is a class name (fully qualified) and an assembly name. Before the definition is created, the assembly should have been deployed to the site's bin directory. The UI will only support precompiled workflows although the API also offers a possibility to define XOML-based workflows that will be compiled at registration time. It is possible to define if instances of the workflow should be created automatically and if so, which EPiServer CMS related event should trigger this behavior. It is also possible to set access rights on definitions.
- Create is needed to be able to create new instances manually.
- Delete is needed to be able to terminate running instances.
- Edit is needed to be able to modify metadata on running instances.
- Read is needed to be able to list running instances.
Using APIs
The main entry point when interacting with the workflow system in EPiServer CMS is through the class EPiServer.WorkflowFoundation.WorkflowSystem which exposes the central components:
- WorkflowManager (of the type IWorkflowManager). IWorkflowManager is a component that is used to interact with runtime, for example, to register and get services. There are also a number of events related to the runtime such as WorkflowTerminated and WorkflowCompleted. There are also events that will be fired when events occurs in DataFactory or in FileSystem. In the arguments to these events you may specify that a workflow instance should be started as well as passing start arguments to the instance that is about to be started.
- DefinitionHandler (of the type IDefinitionHandler). IDefinitionHandler is the component that handles workflow definitions EPiServer.WorkflowFoundation.WorkflowDefinition. It allows you to list, register, edit or delete workflow definitions. The component handles both precompiled and XOML based workflows. The support for XOML based registrations can be used, for example, to build a designer/wizard that would allow users to create workflows (and optionally a code-beside code and rules) which then could be used to create a workflow definition.
- InstanceHandler (of the type IInstanceHandler). IInstanceHandler is the component that handles workflow instances EPiServer.WorkflowFoundation.WorkflowDefinitionInstance. It allows you to list, create, edit and terminate workflow instances. It is also possible to load tasks related to workflow instances. The component also exposes the event EPiServer.WorkflowFoundation.InstanceHandler.InstanceStarting where it is possible to alter the supplied start parameters for the workflow instance.
- WorkflowDefinition. WorkflowDefinition (or one of the concrete classes CompiledWorkflowDefinition or XomlWorkflowDefinition) is the class that represents a workflow definition. An instance of this type contains for example the ACL that controls who can, for example, create and terminate instances. It also holds the underlying compiled type and the start parameters dictionary.
- WorkflowDefinitionInstance. An instance of WorkflowDefinitionInstance represents an instance of a running workflow. The instance holds a history list of occurrences related to the instance and an accessor to the underlying System.Workflow.Runtime.WorkflowInstance.
Registering an object store
Instances of WorkflowDefinition and WorkflowDefinitionInstance are stored in EPiServer object store. This implies that objects that are stored in WorkflowDefinition.StartParameters or in WorkflowDefinitionInstance.UserData dictionaries must have schemas registered with object store before they can be used.
The registration with object store must be done before any saved definition or instance is loaded or saved. A convenient way to register custom types used with workflows is to register them in the constructor of a class that is registered as a external service, in that case you can be certain the registration is done before any attempt to load or save an instance or definition is performed. The following is an simple service that registers a custom type with object store:
using EPiServer.BaseLibrary;
using EPiServer.Implementation;
public class CustomService
{
public CustomService()
{
if (Context.Repository.SchemaForType(typeof(CustomType)) == null)
{
Type type = typeof(CustomType);
Serialization.TypeSchemaBuilder.RegisterSchemaAndType(type.ToString(), type);
}
}
}
The service should then be registered with runtime as a external service as described in configuration section.
Tips and tricks
Threading
- Consequences. The workflow instances will run in own threads meaning when the workflow host starts a new instance or sends an event to en existing instance, the workflow will be set up for execution in a new thread (it will acquire a thread from the CLR thread pool). This implies, for example, that if there is a workflow that upon execution throws an exception, that exception will not be returned to the StartInstance method (since StartInstance will set up the workflow for execution in a new thread and then return to the caller). Instead unhandled exceptions thrown in workflow instances will cause the instance to be terminated and the event can be captured by the EPiServer.WorkflowFoundation.Interfaces.IWorkflowManager.WorkflowTerminated event (which accepts the original exception as argument).
Another consequence of workflows running in separate threads is that the event model between the host and running instances are asynchronous. This means that every message/call from the host to the instance has to pass a queue associated with the workflow instance (this queuing is automatically created when using activities created by the tool wca.exe as explained in section Custom Activities).
A host can only send messages to instances that have registered for those message in their respective event queues. This has consequences. If you for example have a workflow with a “while” activity containing an OnPageCreated activity with the intention to capture all pages created. Now if you run an import package where several pages gets created in a short period of time there will be no guarantee that the OnPageCreated activity in the instance will be notified for each page.
The reason is that the host that receives the PageCreated events from DataFactory and the workflow instance that loop in the while activity runs in separate threads. Thus when the workflow host receives a page created event, the workflow instance may be busy with other actions (perhaps from a previous OnPageCreated event) and is not registered in the event queue meaning it will not get that PageCreated event. The solution to this situation where you want all PageCreated events to be handled by workflows is to either register a custom service for the communication with the instance, this custom service would know that the instance wants all page created events and could take care of delivering them to the instance (for example, by an internal queue).
Another solution is to have a workflow definition that should be started automatically when a page is created.
- maxSimultaneousWorkflows. There is a maxSimultaneousWorkflows setting in web.config on the service DefaultWorkflowSchedulerService in the section workflowRuntime that states the maximum number of threads that can be used by workflow instances. The reason to set a maximum value for this is that unprohibited, workflow instances could consume too many threads thus keeping them from serving ordinary web requests. Do note that this states how many workflow instances can be executing at the same time not how many workflow instances there can be in a system.
Since a workflow instance is unloaded from memory when it is idled this is no limitation on how many instances you can have in your system but rather it limits the number that can be loaded and executed at the same time. If the maximum number is reached and another workflow is scheduled for execution, that instance will wait until another instances is unloaded before it gets executed.
- Thread.Sleep. Do not use Thread.Sleep within workflow instances to make the workflow instance pause. Instead use the activity DelayActivity to achieve this. The reason for this is that if DelayActivity is used the workflow instance will be unloaded when it is paused and the thread will be put back in the thread pool meaning another waiting instance can be scheduled to execute.
Services
Custom services can be registered with the runtime, this can preferably be achieved by registering the service on the configuration element externalServices in web.config. The custom service could for example be a service to be used with communication with custom event activities. Another usage of services is to register a service to handle events from the workflow system, for example, to listen to the WorkflowCompleted or WorkflowTerminated events of a particular workflow type and then do some cleanup. Yet another usage is to use the constructor of the service to register custom types used in workflows with object store. Since the services will be instantiated at runtime startup this is a convenient way to setup event listening (compared to writing an HttpModule).
The requirement for a custom service is that they have a public default constructor (the event listening can be setup in this constructor). Services that are used to communicate with custom event activities must implement an interface with the attribute ExternalDataExchange.
Validating start parameters
Startparameters that are set by UI (that is through usercontrol specified by WorkflowPluginAttribute) can be validated by ordinary validator controls on the usercontrol. A way to validate all startparamters (that is both those set by UI or by API passed, for example, as parameter to StartInstance method) is to add en eventlistner to IInstanceHandler.InstanceStarting (which can be done, for example, in a service as described in above section). In this handler you could then look through the start parameters and validate them. If the parameters are not valid you can on the event argument passed with InstanceStarting set the property Cancel to true and set a description in CancelReason property. Then the instance will not be started and the caller to StartInstance will get an exception of type WorkflowInstanceException with the value passed as CancelReason.
See also
Last updated: Feb 23, 2015