Skip to main content

MVP-Submitted: The Power of Custom Workflow Activities (Part 2)

Maurice de Beijer, www.WindowsWorkflowFoundation.eu, www.TheProblemSolver.nl

Workflow Foundation (WF) is part of the .NET 3.0 framework alongside Windows Communication Foundation (WCF) and Windows Presentation Foundation (WPF). Where WCF and WPF enable new and improved ways of doing things we have been doing for a long time, the same can’t be said for Workflow Foundation. Using Workflow Foundation enables a developer to create a whole new style of application, the long running and event driven application.

In the previous article about developing custom Workflow Activities I examined the basics of activity development. This included why we needed to use custom Workflow Activities in the first place as well the basic communication inside of the Workflow runtime. I also took a look at the activity lifecycle and how to add properties to a Workflow Activity.

In this second article I would like to take a look into some of the more advanced aspects of Workflow Activity development. The most important of these could well be how to create event-driven activities. A lot of the power of Workflow Foundation comes from the fact that is able to react to all sorts of events so it is more than likely that our activities will need to be able to do the same.

Event-Driven Activities

All the activities above work just fine but they have one big limitation because they can only be used in the normal execution flow while executing a workflow. Basically the activity start and the workflow will wait however long it takes to read some data, even if it takes several years. Sometimes this might be perfectly acceptable behavior but there will be times that we want our workflow to wait a maximum amount of time, or possibly for some other event, and proceed along another execution path. While it would be perfectly feasible to add a timeout property and code some form of a timeout there is really no need for us to do so as this mechanism is already provided in Workflow Foundation through using a ListenActivity and adding a DelayActivity to another branch. The difference in this case is that the ListenActivity is the one that is executed instead of our custom activity and it waits to see which event is raised first before allowing that branch to continue.

In order to add our activity as the first child of a ListenActivity, or any other EventDrivenActivity for that matter, we need to implement the IEventActivity interface. The most important function of this IEventActivity interface is the Subscribe() function. This Subscribe() function is called for every first child activity in a ListenActivity and in this function you are required to create the queue’s required and initialize the event handlers. The ListenActivity also monitors the WorkflowQueue created, it’s name is part of the IEventActivity interface, and it will call the Execute() function of the activity with the first message in the queue. The normal event processing now occurs as part of the Execute() function and this returns ActivityExecutionStatus.Closed to indicate that the activity is done.

Public class ReadLineActivity
    Inherits Activity
    Implements IEventActivity
    Protected Overrides Function Execute( _
        ByVal executionContext As ActivityExecutionContext) _
        As ActivityExecutionStatus

        Dim wqs As WorkflowQueuingService
        Dim queue As WorkflowQueue
        wqs = executionContext.GetService(Of WorkflowQueuingService)()
        queue = wqs.GetWorkflowQueue(QueueName)
        Dim data As Object = queue.Dequeue()

        wqs.DeleteWorkflowQueue(QueueName)

        Return ActivityExecutionStatus.Closed
    End Function

    Public ReadOnly Property QueueName() As System.IComparable _
        Implements IEventActivity.QueueName
        Get
            Return"MyPrivateQueueName"
        End Get
    End Property

    Public Sub Subscribe(ByVal parentContext As ActivityExecutionContext, _
        ByVal parentEventHandler _
        As IActivityEventListener(Of QueueEventArgs)) _
        Implements IEventActivity.Subscribe

        Dim wqs As WorkflowQueuingService
        Dim queue As WorkflowQueue
        wqs = parentContext.GetService(Of WorkflowQueuingService)()
        queue = wqs.CreateWorkflowQueue(QueueName, True)
        queue.RegisterForQueueItemAvailable( _
            parentEventHandler, QualifiedName)

        Dim rls As ReadLineService
        rls = parentContext.GetService(Of ReadLineService)()
        rls.ReadLine("Please enter the input:", QueueName)
    End Sub

    Public Sub Unsubscribe(ByVal parentContext As ActivityExecutionContext, _
        ByVal parentEventHandler _
        As IActivityEventListener(Of QueueEventArgs)) _
        Implements IEventActivity.Unsubscribe

        Dim wqs As WorkflowQueuingService
        Dim queue As WorkflowQueue
        wqs = parentContext.GetService(Of WorkflowQueuingService)()
        queue = wqs.GetWorkflowQueue(QueueName)
        queue.UnregisterForQueueItemAvailable(parentEventHandler)
    End Sub
End Class

Listing 1. An event driven activity

This double headed behavior seems to place us in a bit of a dilemma. Either we create an activity for the normal execution flow where the Execute() function is called first and an event handler is used to receive the event and close the activity. Or in the event driven case we need to create an activity where the Subscribe() function is called first and the Execute() function is really the event handler that closes the activity. Now we could create two activities all the time, one for event driven work and the other for regular use but that would be rather confusing to the workflow developer so we need a better solution. This better solution, by combining the two behaviors, can be achieved by implementing one additional interface, the IActivityEventListener<QueueEventArgs> interface, and tracking what the entry point for execution was. Listing 2 shows how to combine the two behaviors into a single ReadLineActivity.

Public Class ReadLineActivity
    Inherits Activity
    Implements IEventActivity, IActivityEventListener(Of QueueEventArgs)

    Public Property IsInEventActivityMode() As Boolean
        Get
            Return CBool(GetValue(IsInEventActivityModeProperty))
        End Get

        Set(ByVal value As Boolean)
            SetValue(IsInEventActivityModeProperty, value)
        End Set
    End Property

    Public Shared ReadOnly IsInEventActivityModeProperty _
        As DependencyProperty = _
        DependencyProperty.Register("IsInEventActivityMode", _
                          GetType(Boolean), _
                          GetType(ReadLineActivity))

    Protected Overrides Function Execute( _
        ByVal executionContext As ActivityExecutionContext) _
        As ActivityExecutionStatus

        Dim status As ActivityExecutionStatus = ExecutionStatus

        If IsInEventActivityMode Then
            ReceiveLine(executionContext)
            status = ActivityExecutionStatus.Closed
        Else
            InternalSubscribe(executionContext, Me, False)
            status = ActivityExecutionStatus.Executing
        End If

        Return status
    End Function

    Protected Overrides Function Cancel( _
        ByVal executionContext As ActivityExecutionContext) _
        As ActivityExecutionStatus


        InternalUnsubscribe(executionContext, Me)

        Dim wqs As WorkflowQueuingService
        wqs = executionContext.GetService(Of WorkflowQueuingService)()

        If wqs.Exists(QueueName) Then
            wqs.DeleteWorkflowQueue(QueueName)
        End If

        Return ActivityExecutionStatus.Closed
    End Function

    Public ReadOnly Property QueueName() As IComparable _
        Implements IEventActivity.QueueName
        Get
            Return"MyPrivateQueueName"
        End Get
    End Property

    Public Sub Subscribe( _
        ByVal parentContext As ActivityExecutionContext, _
        ByVal parentEventHandler _
        As IActivityEventListener(Of QueueEventArgs)) _

        Implements IEventActivity.Subscribe

        InternalSubscribe(parentContext, parentEventHandler, True)
    End Sub

    Public Sub Unsubscribe( _
        ByVal parentContext As ActivityExecutionContext, _
        ByVal parentEventHandler _
        As IActivityEventListener(Of QueueEventArgs)) _

        Implements IEventActivity.Unsubscribe

        InternalUnsubscribe(parentContext, parentEventHandler)
    End Sub

    Public Sub OnEvent(ByVal sender AsObject, _
        ByVal e As QueueEventArgs) _
        Implements IActivityEventListener(Of QueueEventArgs).OnEvent

        Dim executionContext As ActivityExecutionContext
        executionContext = CType(sender, ActivityExecutionContext)
        ReceiveLine(executionContext)
    End Sub

    Private Sub InternalSubscribe( _
        ByVal parentContext As ActivityExecutionContext, _
        ByVal parentEventHandler _
        As IActivityEventListener(Of QueueEventArgs), _

        ByVal inEventActivityMode As Boolean)
        Dim wqs As WorkflowQueuingService
        Dim queue As WorkflowQueue
        wqs = parentContext.GetService(Of WorkflowQueuingService)()
        queue = wqs.CreateWorkflowQueue(QueueName, True)
        queue.RegisterForQueueItemAvailable( _
            parentEventHandler, QualifiedName)

        IsInEventActivityMode = inEventActivityMode

        Dim rls As ReadLineService
        rls = parentContext.GetService(Of ReadLineService)()
        rls.ReadLine("Please enter the input:", QueueName)
    End Sub

    Private Sub InternalUnsubscribe( _
        ByVal parentContext As ActivityExecutionContext, _

        ByVal parentEventHandler As _
            IActivityEventListener(Of QueueEventArgs))

        Dim wqs As WorkflowQueuingService
        Dim queue As WorkflowQueue
        wqs = parentContext.GetService(Of WorkflowQueuingService)()
        queue = wqs.GetWorkflowQueue(QueueName)
        queue.UnregisterForQueueItemAvailable(parentEventHandler)
    End Sub

    Private Sub ReceiveLine( _
        ByVal executionContext As ActivityExecutionContext)


        Dim wqs As WorkflowQueuingService
        Dim queue As WorkflowQueue
        wqs = executionContext.GetService(Of WorkflowQueuingService)()
        queue = wqs.GetWorkflowQueue(QueueName)
        Dim data As Object = queue.Dequeue()

        wqs.DeleteWorkflowQueue(QueueName)

        executionContext.CloseActivity()
    End Sub
End Class

Listing 2. The ReadlineActivity with both the normal and the event driven behavior

If you take a good look at listing 2 you can see that it takes quite a bit more code to implement this dual approach. Luckily most of the code has nothing to do with the actual problem we are trying to solve and it would be quite feasible to create an abstract or mustinherit base activity with this generic code. While this base activity sounds like a perfect activity to be included in the base activity library from Microsoft this unfortunately is not the case.

Activities and Long Running Workflows

One of the major advantages of Workflow Foundation is the possibility to have workflows running for months, or even years if need be, and still be able to serialize the workflow state to disk. When the workflow state is serialized to disk all traces of the workflow can be completely removed from memory only to be recreated at a later moment when the workflow is ready to continue. This capability should not be taken lightly as it enables us to restart the process hosting the workflow runtime, the complete machine running the application or even move the entire application including all its state to another machine. After all it would be very hard to create workflow that executes over a time span of several months and expect the machine and hosting process to keep running all the time. Unfortunately this power isn’t completely free and we, as the developer, have to make certain that our code can survive these restarts. Most of the time when we are developing workflow activities this isn’t much of a problem, just as long as we stick to the guidelines everything will be serialized and de-serialized correctly. This doesn’t just include the value of properties but also any workflow queues and event binding we might have set up.

Key takeaway: Both the activities and runtime services must be able to handle a machine restart.

The main problem is usually in the area of runtime services we might have developed along with our workflow activities. While the workflow activities with their state will be serialized, the same is not true for any runtime services leaving the developer to handle any persistence requirements. In a lot of cases this isn’t any extra work as the runtime service is only a layer between an external system and the workflow without any real state. In these cases the external system will cause some sort of event to occur, for example a database update or an MSMQ message, and the runtime service, either actively or passively, waits for the event to occur and passes it along. But there are exceptions, suppose the runtime service needs to wait for a specific file to be dropped in a directory and pass the contents to a known workflow queue. During normal operations this could easily be done using a SystemFileWatcher class. However if the process hosting the workflow runtime is restarted the runtime service needs to create a new SystemFileWatcher and start waiting again. The workflow activity itself cannot notify the runtime service that it is waiting for a file because the workflow is persisted to disk and will not be reloaded until something happens to it. In a case like this the runtime service needs to have its own persistence store and maintain its state. And just adding a private persistence store to the runtime service isn’t enough, we also need to make sure it is exactly in sync with the normal workflow persistence service. After all if our application crashes for some unknown reason we want to make sure the workflow state and the related service state are exactly the same. This problem is not just related to a runtime service using its own persistence store but actually to every external state managed by a runtime service.

An example of a potential state management/synchronization issue in a runtime service can be seen when we look at what happens when we receive a message through an MSMQ queue. Suppose the runtime service reads the message, places this in a workflow queue, the workflow continues its execution in another thread and the runtime service deletes the message from the queue. Sounds perfectly reasonable but suppose the process hosting the workflow runtime crashes before the new workflow state is persisted. This process will now be restarted and when it does so it will use the workflow state as was persisted in the persistence store. This happens to be the state before the MSMQ message was received so the workflow starts waiting for this message to appear. However this message has been read and is gone, never to appear again. Wrapping the MSMQ message read-workflow message write action in a TransactionScope isn’t going to help either because we would need to include the workflow persistence in our transaction as well. While this is possible it is problematic because it makes using a workflow persistence service mandatory. The best solution to this problem is using the standard Workbatch service. The details of work batches are explained below but in this case we would use a Peek() function to read the message from the MSMQ queue. Next when we queue the data for the workflow we use the third and fourth parameter of the WorkflowInstance.EnqueueItem() function to specify the IPendingWork object, possibly the service itself, and the MSMQ LookupId as the data. The WorkBatchService now makes sure the IPendingWork object is called with the specified data as soon as the workflow state reaches its next persistence point. Finally the IPendingWork object can remove the message from the queue using the ReceiveByLookupId() function.

Key takeaway: Use the WorkBatchService to ensure the consistency between the runtime service and the workflow persistence service.

This leaves us with the unanswered question of when the IPendingWork object will be called with the specified data. This call occurs whenever a persistence point is reached. The easiest way of thinking about a persistence point is considering when the current state of a workflow becomes fixed and cannot revert to a previous state, no matter what happens to the parent process hosing the workflow. There are five possible distinct persistence points during the lifetime of a workflow:

  1. Before the workflow completed event.
    This means that the workflow has finished its work successfully.
  2. Before the workflow terminated event.
    This means that the workflow has finished in an unsuccessful manner, possibly due to an unhandled exception.
  3. When the workflow runtime is equipped with a workflow persistence service and either the Unload() or TryUnload() function is called.
    This means the current state of the workflow is safely persisted to disk.
  4. When the workflow becomes idle and a SqlWorkflowPersistenceService is loaded with the property UnloadOnIdle set to true.
    This means the workflow will be persisted to disk every time it becomes idle.
  5. When an activity which is decorated with the PersistOnClose attribute reaches the closed state.

The first four options are really quite self explanatory. In the case of the first two the workflow is finished, and is thereby removed from the workflow persistence service when present. The next two options, calling the Unload() or TryUnload() or using a SqlWorkflowPersistenceService with the UnloadOnIde option, means that the workflow statues will be persisted on disk at that time ensuring that any restart will occur from that point. The last option, the PersistOnClose attribute, is the most interesting to look at. You might have noticed that you are required to add a workflow persistence service when you use either the TransactionScopeActivity or the CompensatableTransactionScopeActivity in a workflow. Both these activities ensure that all contained work is executed inside of a TransactionScope. The reason for this persistence requirement is that both are decorated with the PersistOnClose attribute which ensures the workflow runtime will save the workflow state as soon as the activity is done. The reasoning behind this is that no matter what happens the workflow will never restart before the transactional activity and redo the complete transaction.

Key takeaway: All activities causing some external state to be altered should be decorated with the PersistOnClose attribute.

Besides using the WorkflowInstance.EnqueueItem() function it is also possible to add work items to the queue using the WorkflowEnvironment.WorkBatch.Add() function. This function can be called any time a workflow activity is being executed while still on the same thread. The work item will be executed at the next persistence point of that workflow.

Validating Activities in the Workflow Designer

You might have noticed that when you create a new workflow and drag a CodeActivity on it a red icon with an exclamation mark appears. Not only does this red exclamation mark appear but when we try to compile the solution it shows up as an error. This compile error indicates that the ExecuteCode property has not been set yet. An example of this error can be seen in picture 1. The cause of both the compiler error and the red exclamation mark is an ActivityValidator that is attached to the CodeActivity using an ActivityValidatorAttribute.

Activity Validator

Figure 1. An invalid CodeActivity

Validating our own custom activities is quite easy to do as listing 2 demonstrates. The first thing we need to do is create a new class derived from the ActivityValidator class. In this class we can override either the Validate() function or the ValidateProperties() function. In these functions we must create a ValidationErrorCollection, add all the current errors to it and return this collection. The designer will use the information returned to display an exclamation mark if there are any errors found. Hooking the ActivityValidator to the activity class to be validated is easy, all that needs to be done is decorate the activity class with the ActivityValidatorAttribute specifying the ActivityValidator class to be used.

<ActivityValidator(GetType(ReadLineActivityValidator))> _
Public Class ReadLineActivity
    Inherits Activity

    PublicProperty Question() AsString
        Get
           ReturnCStr(GetValue(QuestionProperty))
        EndGet
        Set(ByVal value AsString)
            SetValue(QuestionProperty, value)
        EndSet
    EndProperty

    Public SharedReadOnly QuestionPropertyAs DependencyProperty = _

                          DependencyProperty.Register("Question", _
                          GetType(String), _
                          GetType
(ReadLineActivity))
End Class

PublicClass
ReadLineActivityValidator
    Inherits ActivityValidator

    PublicOverridesFunction ValidateProperties( _
        ByVal manager As ValidationManager, _
        ByVal obj AsObject) _
        As ValidationErrorCollection

        Dim errors AsNew ValidationErrorCollection()
        Dim activity As ReadLineActivity = TryCast(obj, ReadLineActivity)

        If activity IsNotNothing _
           AndAlso activity.Parent IsNotNothing _
           AndAlso activity.Question =""Then

           Dim err AsNew ValidationError( _
               "The question must be specified", 0)
            err.PropertyName = "Question"
            errors.Add(err)
        End If

        errors.AddRange(MyBase.ValidateProperties(manager, obj))

        Return errors
    End Function
End Class

Listing 3: An activity with its validator

Displaying an Activity in the Workflow Designer

We get a lot of control to fine tune the way an activity looks and feels inside of the workflow designer. In this example I will show just the very basics as a more complete coverage of the ActivityDesigner possibilities would at least be an article by itself. This short sample can be found in listing 4.

Designer

Figure 2. An activity with a custom designer

Imports System.Reflection

<Designer(GetType(ReadLineActivityDesigner))> _
Public Class ReadLineActivity
    Inherits Activity

    PublicProperty Question() AsString
        Get
            Return CStr
(GetValue(QuestionProperty))
        End Get
        Set(ByVal value AsString)
            SetValue(QuestionProperty, value)
        End Set
    End Property

    Public Shared ReadOnly QuestionProperty As DependencyProperty = _
                          DependencyProperty.Register("Question", _
                          GetType(String), _
                          GetType(ReadLineActivity))
End Class

Public Class
ReadLineActivityDesigner
    Inherits ActivityDesigner

    Protected Overrides Function OnLayoutSize( _
        ByVal e As ActivityDesignerLayoutEventArgs) _
        As Size

        Dim size As Size = MyBase.OnLayoutSize(e)
        size.Height += 50
        size.Width += 20
        Return size
    End Function

    Protected Overrides Sub OnPaint( _
        ByVal e As ActivityDesignerPaintEventArgs)

        Dim readLineActivity As ReadLineActivity
        readLineActivity = CType(Me.Activity,
            WorkflowConsoleApplication3.ReadLineActivity)

        Dim mi As MethodInfo
        mi = GetType(ActivityDesignerPaint). _
             GetMethod("DrawDesignerBackground", _
             BindingFlags.Static Or BindingFlags.NonPublic)
        mi.Invoke(Nothing, NewObject() {e.Graphics, Me})

        Dim rect As Rectangle = e.ClipRectangle
        rect.Offset(10, 0)

        Dim text As String
        text = String.Format("{0}{2}The question is:{2}{1}", _
                   readLineActivity.QualifiedName, _
                   readLineActivity.Question, _
                   vbCrLf)
        ActivityDesignerPaint.DrawText(e.Graphics, _
            e.DesignerTheme.Font, _
            text, _
            rect, _
            StringAlignment.Near, _
            e.AmbientTheme.TextQuality, _
            e.DesignerTheme.ForegroundBrush)
    End Sub
End Class

Listing 4: An activity with its own custom display in the workflow designer

The most interesting part of the code is the usage of the ActivityDesignerPaint class. This class is used quite a bit by the standard activities to paint their parts in the designer. Unfortunately there are quite a few members of the ActivityDesignerPaint class marked as friend (internal in C#) so we cannot call them from our code. Unfortunately the DrawDesignerBackground() function used to display the activity background is one of them. One way of getting the background is calling the base class OnPaint() function which does this. Unfortunately it also draws an icon and caption, something I didn’t want in this case. Naturally I didn’t want to duplicate this functionality either so the workaround is to use reflection to get a reference to the MethodInfo object and invoke this instead. This maybe not the fastest executing option but one that certainly is easy to implement.

Conclusion

Workflow Foundation is a powerful addition to a software developer’s toolkit. Using workflow foundation often requires building your own custom activity classes, something that is not hard to do as long as the basic rules are followed. Although, as this article demonstrates, there are quite a few of these rules to follow, fortunately they aren’t very complex to implement.

About Maurice de Beijer

Maurice de Beijer is an independent software consultant. He has been awarded the yearly Microsoft Most Valuable Professional award since 2005. Besides developing software Maurice also runs the Visual Basic section of the Software Developer Network, the largest Dutch .NET user group. Maurice can be reached through his websites, either www.WindowsWorkflowFoundation.eu or www.TheProblemSolver.nl, or by emailing him at Maurice@TheProblemSolver.nl.