System Requirements
· Install the following prerequisites:
· Set the Target Framework - project > Properties > Application tab – set the project's target framework is .
NET Framework 4 Platform Update 1
· If you don’t install the prerequisites, you will see the following error stating Project Target Framework Is Not Installed - You need to install the Multi-Targeting Pack for Microsoft .NET Framework 4 Platform Update 1 (KB2495638)
· In your .csproj file note the following target framework version:
<TargetFrameworkVersion>v4.0.1</TargetFrameworkVersion>
· Note: Resharper doesn’t support v4.0.1 at this point in time – you will need to disable Resharper.
Constructing a Workflow State Machine
· Add a State Machine – D&D a StateMachine activity onto the Workflow Designer
· Add A State
o Drag a state onto the state machine. Give it a DisplayName
o Double Click on a State to open it. You will see 2 places where you can drop activities: Entry and Exit
o 
o You can also see any Transitions and their target states
· Configure a Transition
o Transitions usually involve an activity that waits for some event to occur.
o a Null Transitionis atransition with no triggering action.
o Give it a DisplayName - Typically, you name the transition something that describes the event that causes the transition.
· Add a Final State -
State machines end with a Final State (you may have more than one Final State). Drop a
FinalState activity.
· Navigation during design time
o When moving states, you can select the state and use the arrow keys to position the state where you want it.
o navigate to a state from a transition or from a transition to a state by clicking the Destination hyperlink
o breadcrumbs at top of designer allow drill down / up
· Add Workflow Arguments
o Click on Arguments to open the Arguments window - Click on Create Argument to add a new Argument - In the type drop down select Browse for Types… to open the type selector dialog. This allows you to specify In / Out arguments for the workflow.
o For example use Settings.Timeout to allow the host application to pass a timeout value that will be consistent throughout your workflow.
Beyond the Basics
o A Shared trigger has multiple possible state destinations as well as multiple Condition-Action panes
o To create a shared trigger, with the transition not selected, move the mouse cursor over the source circle. a tooltip prompts: "Drag a line to create a transition that shares the same trigger".
o Drag a line from the start circle of transition to the target state.
o Change the DisplayName of the transition as appropriate
o For the transition, Expand the shared trigger and set the appropriate Condition expression. Copy any pre-transition Action activities as needed.
o Drop a StateMachine activity into the Entry pane of a StateActivity
o A nested state machine must have a Final State. This state marks the point where the inner state machine is complete and control will return to the outer state machine
What 'State' are we in? Prognosis: Negative!
Not a good one, I'm afraid – Workflow 4 is not a programmatic-free development paradigm:
· Besides the obvious of needing to create a host,
· Any complex state machine looks like abstract art – can you follow the logic from the following diagram (from the context menu, select copy as diagram)? The workflow paradigm has clearly failed to capture the complexity of capturing logical flows in a conceptually meaningful way. Truth be told, I tire of this dated BizTalk-style orchestration paradigm – I think StreamInsight is a more favorable approach to application engines.
· The StateMachineActivity designer service is limited in size – I don’t seem to be able to drag and enlarge it! This means that there are only so many StateActivity objects I can visually squeeze in!
· We've created a state machine that when run automatically transitions from state to state – there is no StateActivity transition property to say "WAIT" – I need to acquaint myself with Bookmarks and custom activities – hardly designer friendly. It turns out I need to
o Create a TransitionTarget enum
o Create a TransitionResult POCO that contains a TransitionTarget enum property plus any other state I want to pass around
o Create a WaitForTransitionActivity that inherits NativeActivity<MyResult> and implements the following:
§ private BookmarkCallback with a callback
§ public TranstionTarget
§ override bool CanInduceIdle
§ override void Execute(NativeActivityContext)
o Place the activity in the trigger pane of a transition
o In properties, Optionally set the result of the activity to be stored in a global variable
o In properties, set the TransitionTarget
o Optionally set a transition condition expression that checks the result variable
· Which is not really very intuitive, is it? This should really be encapsulated in the framework.
· Functionality that was present in NET 3.51 Workflow Services is gone – I cant map my StateMachineActivity to a WCF contract, let alone have an application level protocol whereby different WCF operations are mapped to different transition trigger events.
· beware of infinite loops – there's no way to detect this at a glance without drilling down into your transitions!
· The designer metadata and logic are tied together: If you look at the XAML, its not as bad as XOML, but its still full of the following crap:
sap:VirtualizedContainerService.HintSize ="…" attributes
and
<sap:WorkflowViewStateService.ViewState>
<scg3:Dictionary x:TypeArguments="x:String, x:Object">
<av:Point x:Key="ShapeLocation">…</av:Point>
<av:Size x:Key="ShapeSize">…</av:Size>
<x:Boolean x:Key="IsPinned">False</x:Boolean>
</scg3:Dictionary>
</sap:WorkflowViewStateService.ViewState>
And
<sap:WorkflowViewStateService.ViewState>
<scg3:Dictionary x:TypeArguments="x:String, x:Object">
<av:PointCollection x:Key="ConnectorLocation">…</av:PointCollection>
</scg3:Dictionary>
</sap:WorkflowViewStateService.ViewState>
o If they had thought this out, they would have separated this out into different models like Entity Framework did with CSDL and RSDL.
· I cant query the current state!
· There is no easy way to define an IStateMachineActivity interface that demands certain states to be present.
Workflow – Infrastructure Needed!
· I have to say, the WF team really dropped the ball on this one! Maybe WF 5 will address this stinker? In the meantime, I need to encapsulate the deficiencies of WF 4 State Machine in my infrastructure, hiding aforementioned limitations and caveats.
Generic Infrastructure: Workflow Definitions & State Transition Bookmarks
· Obviously I want to be able to support different Workflow definitions à generic param 1: TWorkflow. This type must inherit from Activity and must be instantiate-able (hence new() ).
· I plan to use an enumeration to define the set of bookmarks that the State Machine and host application will use. It’s a good idea for these to match the StateActivity DisplayName values. However I don’t want to be tied to a specific set of states – hence generic param 2: TTransitionEnum. This will be an Enum, hence struct, IConvertible.
public class WorkflowHostManager<TWorkflow, TTransitionEnum>
where TWorkflow : Activity, new()
where TTransitionEnum: struct, IConvertible
· I'll come back to WorkflowHostManager soon.
Transition Result
· For my current needs, I don’t yet need to examine the results of what I do in each state, but I'm sure that will change. Hence I've created a result payload class that at the minimum contains the desired target state to use next.
· This will be the return type of my soon to be created WaitForTransitionActivity, as well as the type of a Workflow variable that I will specify in my Workflow definition. I can then use this variable in state transition conditions if I need to.
· I can always subclass this and stick in more properties as I need to.
public class TransitionResult<TTransitionEnum>
where TTransitionEnum: struct, IConvertible
{
public TTransitionEnum Data { get; set; }
}
WaitForTransitionActivity
· We will create a custom activity base class that will hide the details of the wait and Bookmark callback mechanisms:
public abstract class WaitForTransitionActivity<TTransitionEnum> : NativeActivity<TransitionResult<TTransitionEnum>>
where TTransitionEnum: struct, IConvertible
{}
· The activity will be used for transitioning to an state defined by any enum value – for any enum à maximum reusability. It contains a property specifying the enum value – so that future logic can do whatever on transition to a specific state.
public TTransitionEnum Transition { get; set; }
· A BookmarkCallback represents a method called when a Bookmark is resumed.
// The transition callback
private BookmarkCallback _transitionCallback;
private BookmarkCallback TransitionCallback
{
get
{
return _transitionCallback
?? (_transitionCallback = new BookmarkCallback(OnTransitionCallback));
}
}
· The NativeActivity.CanInduceIdle property specifies that the activity can cause the workflow to become idle.
protected override bool CanInduceIdle
{
get { return true; }
}
· The execute method override just induces the idle state by creating the bookmark:
protected override void Execute(NativeActivityContext context)
{
context.CreateBookmark(Transition.ToString(), TransitionCallback);
}
· The callback is boilerplate:
private void OnTransitionCallback(NativeActivityContext context, Bookmark bookmark, object value)
{
var transitionResultValue = value as TransitionResult<TTransitionEnum>;
if (value != null)
{
SetResult(context, transitionResultValue);
}
else
{
// Resumed with something else
throw new InvalidOperationException("You must resume Transition bookmarks with a result of type TransitionResult");
}
}
· SetResult is marked protected virtual – derived classes can push what they need into the result payload
protected virtual void SetResult(NativeActivityContext context, TransitionResult<TTransitionEnum> transitionResultValue)
{
Result.Set(context, transitionResultValue);
}
· We've now hidden all the details of bookmarking in state transition triggers. We just need to derive from this class and add it to the trigger pane of a transition, specifying its target state. We will see this soon.
Hiding the details of managing a Workflow
We need to be able to do the following with our workflow:
· Start an instance
· Wait for workflow state processing and transition processing to complete
· Track its progress
· Control state change via requests
· Disable transition requests whilst a transition is in place
· Leverage timeouts – some transitions should be timely
· Handle events raised by the Workflow runtime host and manage instance state
· Throw and Handle exceptions when an invalid bookmark request occurs
· Maintain a history of state traversals for testing purposes – so that we can verify that state transitions occur in the correct order
· Transition an instance to a completed state
Workflow Host Manager and State Transition History Store
· The WorkflowHostManager provides all this functionality in a generic manner that can work against any state machine workflow.
· Upon construction, we also specify which enum values to use for start state, complete state and cancel state.
public WorkflowHostManager(
IWorkflowStateHistoryStore<TTransitionEnum> historyStore,
TTransitionEnum startState,
TTransitionEnum completeState,
TTransitionEnum cancelState
)
· Leveraging generics, the WorkflowDefinition is instantiated in the constructor as an instance of the generic type TWorkflow.
· The constructor allows for injection of a state transition store - this serves 2 purposes:
o 1) the use of the Workflow just for state management, abstracting the actual actions of specific state transitions into the store;
o 2) facilitation of testing state transition order as the history of state transitions are stored;
public interface IWorkflowStateHistoryStore<TTransitionEnum>
where TTransitionEnum : struct, IConvertible
{
TrackingParticipant TrackingParticipant { get; }
void EnableTransition(TTransitionEnum transition);
void Notify(TTransitionEnum transition, object payload);
void Notify(TransitionResult<TTransitionEnum> transitionResult);
void StateChanged(string state);
}
· An implementation of the history store methods can be seen as follows. Note StateChanged method is used to add a record to the state change audit trail.
public void EnableTransition(PrintTransitionEnum transition)
{
if (transition == TargetTransition)
{
_waitHandle.Set();
}
}
public void Notify(PrintTransitionEnum transition, object payload)
{
_transitions.Add(transition);
}
public void Notify(TransitionResult<PrintTransitionEnum> readerResult)
{
TransitionResults.Add(readerResult);
}
public void StateChanged(string state)
{
_states.Add(state);
if (state == TargetState)
{
_waitHandle.Set();
}
}
Start an Instance
· The WorkflowDefinition is passed into a host – in this case a stock WorkflowApplication. (an input dictionary can optionally be provided too).
· The Idle and Completed event handlers are attached, both of which call _workflowBusy.Set();
· Tracking extensions are added (more on this later).
· The WorkflowHostManager maintains a ManualResetEvent – event threading is used to switch between the workflow execution and the host execution thread. This detail can be hidden from the end-developer by leveraging a fluent interface that returns the WorkflowHostManager after running the primed WorkflowApplication.
public WorkflowHostManager<TWorkflow, TTransitionEnum> StartWorkflow()
{
…
WorkflowHost = new WorkflowApplication(WorkflowDefinition, input)
{
Idle = OnWorkflowIdle,
Completed = OnWorkflowCompleted
};
AddTrackingExtensions();
// set workflow waithandle to nonsignaled, causing thread to block
_workflowBusy.Reset();
WorkflowHost.Run();
return this;
}
Hiding WaitHandle Details
· An Episode mechanism was used to determine what happened in the workflow by fluently supporting waiting for workflow threads to switch out on idle.
· A WaitForWorkflow method waits for idle – it blocks the current thread until the WaitHandle receives a signal - either from a timeout (if enabled) or from a bookmark.
public void WaitForWorkflow()
{
if (Timeout == TimeSpan.MaxValue)
{
_workflowBusy.WaitOne();
return;
}
if (!_workflowBusy.WaitOne(Timeout))
{
throw new TimeoutException(string.Format("Timeout waiting for the workflow to idle or complete"));
}
}
· The WaitForWorkflow call can be placed after any state transition operation request into the workflow. This greatly assists testability.
State change requests – Transition Management
· Using the enum of target states, it is possible to specify a generic method for transitioning to a specific state (providing there is a waiting bookmark for it – if not, bookmark resumption errors are managed)
· The workflow state history store is notified of which state it is to transition to. The TransitionResult<TTransitionEnum> functions as a generic payload at this point – this may be subclassed in future.
public WorkflowHostManager<TWorkflow, TTransitionEnum> TransitionToState(TTransitionEnum target)
{
var transitionResult = new TransitionResult<TTransitionEnum> { Data = target };
_workflowStateHistoryStore.Notify(transitionResult);
_workflowBusy.Reset();
var result = WorkflowHost.ResumeBookmark(transitionResult.Data.ToString(), transitionResult);
CheckForBookmarkResumptionErrors(result);
return this;
}
· Transitions are initialized from the Enum in the constructor – a dictionary of all state transitions are populated:
private void InitializeTransitions()
{
foreach (var transition in Enum.GetValues(typeof(TTransitionEnum)).Cast<TTransitionEnum>())
{
_allowedtransitions.Add(transition, false);
}
}
· All Transitions are disabled temporarily during state change (for concurrency reasons) and when the workflow is stopped:
private void DisableAllTransitions()
{
foreach (var transition in Enum.GetValues(typeof(TTransitionEnum)).Cast<TTransitionEnum>())
{
_allowedtransitions[transition] = false;
}
}
· A specific Transition is enabled in the WorkflowApplication Idle event handler when the WaitForTransitionActivity creates a bookmark, raising the event.
private void EnableTransition(TTransitionEnum transition)
{
_allowedtransitions[transition] = true;
_workflowStateHistoryStore.EnableTransition(transition);
}
Tracking
· The WorkflowHostManager inherits from TrackingParticipant – the base class for Workflow Extensions that interact with the workflow tracking infrastructure and access tracking records
public class WorkflowHostManager<TWorkflow, TTransitionEnum> : TrackingParticipant
· As a tracking participant , The Workflow host manager can override the Track method and based on the derived type of the TrackingRecord, take the appropriate action:
protected override void Track(TrackingRecord record, TimeSpan timeoutValue)
· is ActivityStateRecord – log it (e.g. using a TraceWriter)
· is StateMachineStateRecord – temporarily disable transitions, set current state and add the state name to the historical record:
// All transitions are temp disabled while the state change is in flux
DisableAllTransitions();
CurrentState = stateRecord.StateName;
_workflowStateHistoryStore.StateChanged(stateRecord.StateName);
· is BookmarkResumptionRecord - add the state name to the historical record:
_workflowStateHistoryStore.Notify(bookmarkResumptionRecord.BookmarkName.ToEnumString())
· is WorkflowInstanceRecord – add the start / completed state name to the historical record:
_workflowStateHistoryStore.Notify(_startState / _cancelState / _completeState
Bookmark Exceptions
· Workflow 4.0.1 includes a BookmarkResumptionResult enum.
· When an attempt is made to resume a bookmark for a transition, it may well be the case that the bookmark resumption was not successfully scheduled:
· Either because the bookmark could not be found or because the runtime has not yet created the bookmark.
private static void CheckForBookmarkResumptionErrors(BookmarkResumptionResult result)
{
switch (result)
{
case BookmarkResumptionResult.NotFound:
throw new BookmarkException(BookmarkResumptionResult.NotFound);
case BookmarkResumptionResult.NotReady:
throw new BookmarkException(BookmarkResumptionResult.NotReady);
}
}
· Under such circumstances, a custom BookmarkException is thrown:
public class BookmarkException : ApplicationException
{
public BookmarkResumptionResult BookmarkResumptionResult { get; private set; }
public BookmarkException(BookmarkResumptionResult bookmarkResumptionResult)
: base(bookmarkResumptionResult.ToString())
{
BookmarkResumptionResult = bookmarkResumptionResult;
}
}
Using the Infrastructure
Define States in an Enum
· Map enum values to states – avoid magic strings
Subclass WaitForTransitionActivity
· All details are hidden in the base class – the developer no longer has to concern themselves with bookmarks and callbacks
public sealed class WaitForPrintTransitionActivity : WaitForTransitionActivity<PrintTransitionEnum> {}
· Create a Variable to capture the result of a WaitForPrintTransitionActivity - Click Variables to open the variables pane. Create a new variable for the state. Declare variables at the smallest scope. Transitions are children of the source state; therefore, variables declared in the source state scope are available to expressions in transitions.
o Drop the WaitForTransitionActivity in the Trigger pane and set its properties.
§ DisplayName
§ Result – assignthe Activity result to a WF variable
§ In arguments – any public properties of the activity are exposed in the Properties view
o Optionally set a Condition - Triggers support an optional Condition expression that determines when the transition applies. Set the Condition expression to evaluate the TransitionResultVariable
o Optionally set a pre-transition action - Drop an activity in the transition Action pane
Testing
· Unit Tests for a StateMachine - use the
Microsoft.Activities.UnitTesting test framework (available at
http://wf.codeplex.com). This framework makes it easier to build unit tests for all kinds of workflow activities with any unit test framework.
· Construct a unit test TestMethod:
[TestMethod] public void WFTransitionToReadyState()
· Arrange: instantiate the WorkflowHostManager for the specific Workflow definition and states enum. Provide it with a history store and map its begin and end states.
var historyStore = new TestWorkflowStateHistoryStore();
var target = new WorkflowHostManager<SystemStateActivity, PrintTransitionEnum>(
historyStore,
PrintTransitionEnum.PowerOn, PrintTransitionEnum.Shutdown, PrintTransitionEnum.Shutdown);
· Act: Start the Workflow. Specify state transitions as appropriate. Note the fluent call to WaitForWorkflow() – this hides the details of async calls, by waiting for transition to idle state and back to the host thread. Just 2 methods are needed: TransitionToState and WaitForWorkflow – avoiding method blowout.
target.StartWorkflow().WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.PowerOn).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.AppStartup).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.On).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.Init).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.StandBy).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.Warming).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.Ready).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.P2Print).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.P2Print).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.Busy).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.Ready).WaitForWorkflow();
target.TransitionToState(PrintTransitionEnum.P2Print).WaitForWorkflow();
· Assert: Microsoft.Activities.UnitTesting.AssertState allows me to query a TrackingRecordsList to verify that my states were transitioned in order:
AssertState.OccursInOrder(
"SystemStateMachine", historyStore.Records,
PrintTransitionEnum.Off.ToString(),
PrintTransitionEnum.PowerOn.ToString(),
PrintTransitionEnum.AppStartup.ToString(),
PrintTransitionEnum.On.ToString(),
PrintTransitionEnum.Init.ToString(),
PrintTransitionEnum.StandBy.ToString(),
PrintTransitionEnum.Warming.ToString(),
PrintTransitionEnum.Ready.ToString(),
PrintTransitionEnum.P2Print.ToString(),
PrintTransitionEnum.Print.ToString(),
PrintTransitionEnum.Ready.ToString(),
PrintTransitionEnum.P2Print.ToString(),
PrintTransitionEnum.Print.ToString(),
PrintTransitionEnum.Ready.ToString(),
PrintTransitionEnum.Busy.ToString(),
PrintTransitionEnum.Ready.ToString(),
PrintTransitionEnum.P2Print.ToString(),
PrintTransitionEnum.Print.ToString(),
PrintTransitionEnum.Ready.ToString()
);
Conclusion
· By creating infrastructure around the Workflow 4.0.1 StateMachineActivity, I am able to overcome some of its limitations and hide the implementation details of state transition automation and expose testability.
· Its too bad that the framework doesn’t encapsulate this out of the box – the Microsoft Workflow API should strive to provide a more declarative programming model.