DCSIMG
July 2008 - Posts - Wortzel's blog

Wortzel's blog

.Net (2.0, 3.0, 3.5), C#, Asp.net, Com+, GIS(ESRI Software), Management, Analysis & Design, Life, Trips, And more...

July 2008 - Posts

How to define state machine in Windows Workflow Foundation (WF)

Windows Workflow Foundation is a powerful framework to develop a workflow component in your application. This framework has two workflow types: Sequential workflow (pre-defined flow) and State Machine (event driven flow). In this post I’ll explain and show you how to define a workflow from the later kind.

Each state machine consists of two major components: state (unique configuration) and Transitions (between the states, based on events or input). Each state machine should have initialize state and completion state.

For example let see a state machine of bug(You can download the entire example from here):

clip_image002

After we defined the state machine diagram we can implement it easily.

The first thing we should do is to open a state machine workflow console application:

clip_image004

In order to separate between the workflow design and the implementation we’ll create an interface of the state machine events. In the next step we’ll associate between the interface events to the workflow events. The ExternalDataExchange is necessary for exposing this interface to the workflow state machine.

[ExternalDataExchange]
public interface IBugService
{
    event EventHandler<BugEventArgs> BugOpened;
    event EventHandler<BugEventArgs> BugRejected;
    event EventHandler<BugEventArgs> BugSolved; 
    event EventHandler<BugEventArgs> BugClosed;
}
 

As you probably notice I used a custom EventArgs to pass some relevant arguments as parameters to the event handler. This custom EventArgs should inherit from the ExternalDataEventAargs.

[Serializable]
Public class BugEventArgs : ExternalDataEventArgs
{
    public BugEventArgs(Guid instanceId, Bug bug)
      :base(instanceId)
    { 
        Bug = bug;
        WaitForIdle = true;
    }

    public Bug Bug { get; set; }
}

A very important issue is the WaitForIdle property. You must set the value of this property to “True” (believe me it took me some time to figure it out), it tells to the workflow runtime to raise the event only when the workflow became idle. In case that this property set to “False” and you try to have quick transition between states, you probably will get this unclearly error message:

“Event "BugRejected" on interface type "Wortzel.WF.SimpleStateMachine.IBugService" for instance id "50943c87-348d-4a88-9ada-d440c4989eca" cannot be delivered.” (With inner exception : "Queue 'Message Properties Interface Type:Wortzel.WF.SimpleStateMachine.IBugService Method Name:BugRejected CorrelationValues: is not enabled.")

The service class which going to be the outer API for our state machine, will implement the IServiceBug interface. In addition, this class will contain the entire raise event methods:

[Serializable]
public class BugService:IBugService
{
    public void RaiseBugOpenedEvent(Guid instanceId, Bug bug)
    {
        if (BugOpened != null)
        {
            BugOpened(null, new BugEventArgs(instanceId, bug));
        }
    }
    public void RaiseBugRejectedEvent(Guid instanceId, Bug bug)
    {
        if (BugRejected != null)
        {
            BugRejected(null, new BugEventArgs(instanceId, bug));
        }
    }
    public void RaiseBugSolvedEvent(Guid instanceId, Bug bug)
    {
        if (BugSolved != null)
        {
            BugSolved(null, new BugEventArgs(instanceId, bug));
        }
    }
    public void RaiseBugClosedEvent(Guid instanceId, Bug bug)
    {
        if (BugClosed != null)
        {
            BugClosed(null, new BugEventArgs(instanceId, bug));
        }
    }
  
    #region IBugService Members
    public event EventHandler<BugEventArgs> BugOpened;
    public event EventHandler<BugEventArgs> BugRejected;
    public event EventHandler<BugEventArgs> BugSolved;
    public event EventHandler<BugEventArgs> BugClosed;
    #endregion
}

And now we’ll create the workflow itself. We’ll create an event-driven state machine, each event represents a single transition in our state machine. One of the drawbacks in this model is in case we want to share a single event across multiple states. For example, if we have 2 states (BugSlovedState and BugRejectedState state) which both of them need to define a transition to the BugClosedState state we must define two different events (OnBugClosed1 and OnBugClosed2). I understand the needs to define two events but it’s still annoying…

clip_image006

Each state has two activities: HandleExternalEventActivity and SetStateActivity. The former associates the state machine and the event (in the IBugService interface) and the later set the next state after this event fired.

clip_image008

The last thing we have to do is to activate this state machine. In order to do it we need to write some code which creates the workflow runtime, associates the BugService with our workflow and then creates a workflow instance and starts it.

static void Main(string[] args)
{
    using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
    {
        ExternalDataExchangeService dataExchangeService = new
        ExternalDataExchangeService();
        workflowRuntime.AddService(dataExchangeService);
        BugService bugService = new BugService();
        dataExchangeService.AddService(bugService);
        WorkflowInstance instance = 
            workflowRuntime.CreateWorkflow(typeof(BugWorkflow));
        instance.Start();
        Bug bug = new Bug() { BugId = 1, AssignTo = "Someone" };
        bugService.RaiseBugOpenedEvent(instance.InstanceId, bug);
        bugService.RaiseBugRejectedEvent(instance.InstanceId, bug);
        bugService.RaiseBugClosedEvent(instance.InstanceId, bug);
    }
}

And that it all, we created a state machine workflow. If you want, you can add a tracking service (SqlTrackingQuery) which can help you to determine the entire flow for a specific state machine instance.

When I used it I had a wired behavior: when I printed the current state to the console after I changed several states one after another, I got an inconsistent data (the state name doesn’t always update). My guess that is caused due to racing problem in the state machine engine (only when I gave it much time between the states transition it shows me the right data). Is it bug or feature?

 

Posted Tuesday, July 15, 2008 12:49 AM by Avi Wortzel | 4 comment(s)