Basic State Workflow

Summary

Introduction

Communication between Workflows and Hosting environment


Workflows can interact with an external system (usually the code driving the workflows) through a messaging architecture that uses events and methods. The following diagram shows how WWF can communicate with external systems:


Windows Workflow Foundation uses a simple mechanism for hosts and objects to communicate with a workflow instance. The definition of the communications channel is an interface (attributed with ExternalDataExchange), and the interface implementation is a service class that is added to the runtime to facilitate communication. The HandleExternalEventActivity and CallExternalMethodActivity activities on the workflow instance interact with events and methods that are declared in a custom interface (attributed with ExternalDataExchange) and implemented in a custom class (aka local service). The HandleExternalEventActivity activity responds to a particular event that is raised by the host application and implemented by the local service. The CallExternalMethodActivity is used by a workflow to invoke a method on the local service.

In the example below, the service interface is IOrderingService and the service class is MainFrame.

To the service class, the workflow behaves much like any other class, and you communicate with the workflow by raising events and receiving method calls. To the workflow, the communication interface appears as a channel that contains inbound event sinks, and outbound operations method invocations. The following code example shows an example of how to define a local workflow communication interface:

[ExternalDataExchange]
public interface IOrderingService
{
    void ItemStatusUpdate(Guid orderId, string newStatus);
    event EventHandler<NewOrderEventArgs> NewOrder;
}

Service Class

A service class implements an interface that is used to define communications with a workflow. All events on the interface are one-way and are used by the host to inform the workflow of some action to take (recall that one-way events have no ref or out parameters and no return value). However, for outgoing requests from the workflow to its host, the methods on the interface can have ref and out parameters and a return value.

This effectively means that the communications model is many-to-one: many workflow instances, each of which may be carrying on many conversations, with this singleton service object. This also means that all outbound calls from all workflows, to a particular method foo on the host, are serviced by the same method on the same object. The following shows how the host invokes an event on a workflow instance

public class MainForm : Form, IOrderingService
{
    public event EventHandler<NewOrderEventArgs> NewOrder;

    private void submitButton_Click(object sender, EventArgs e)
    {
        ...

        // Raise the NewOrder event on the workflow instance identified by workflowInstance variable
        newOrderEvent(null, new NewOrderEventArgs(workflowInstance.InstanceId, item, quantity));
    }
}

Example

In this example, a Windows Forms application is used to submit orders. A state workflow is driven by the Forms application to monitor and process incoming orders. The workflow then informs the host once the order has been processed.

Workflow Construction

Although the workflow can be constructed using code, it is more convenient to used the visual designer. Note that you first need to download and install Visual Studio 2005 extensions for .NET Framework 3.0 (Windows Workflow Foundation). WWF projects can be found in the Add New Project dialog box under Workflows (in any .NET Language)

Create an empty Workflow project using the State Machine Workflow Console Application template:

After you create the project, open the workflow designer by double clicking on the workflow class (initially named Workflow1) and place three State activities from the Toolbox window:

After placing three State activities on the surface of the designer, you need to do two things before implementing the actual work flow:

  1. Name each state activity by clicking the activity in the designer and changing the (Name) property in the Properties window.

  2. Identify the initial state and the completed state activities. This is done by clicking the surface of the designer and changing the CompletedStateName and InitialStateName properties.

The effect of these two steps are shown in the two figures below:

(Naming the states)

(Identifying start and end states)

We are now ready to implement each of the three state activities.

Workflow States

WaitForOrder

The WaitForOrder states contains only one activity, the EventDrivenActivity activity which is used to handle events that are fired from hosts (or other activities). Within EventDrivenActivity activity will be contained other activities that will be used to listen to the event and process it. In fact, the first child of EventDrivenActivity should be an activity that derives from IEventActivity. All subsequent children of EventDrivenActivity can be activities of any type. The figure on the left shows the WaitForOrder state after an EventDrivenActivity activity named eventDrivenActivity1 was dropped is it. The middle figure shows the EventDrivenActivity activity after it has been expanded (by double clicking it), and the figure on the right shows the EventDrivenActivity activity after it has been implemented:

    

As expected, first activity within EventDrivenActivity is HandleExternalEventActivity which is an IEventActicity-derived activity. HandleExternalEventActivity blocks and waits until an event is fired. To identify which event this activity should wait for, set the InterfaceType property to the interface that is annotated with ExternalDataExchangeAttribute, (IOrderingService) and set the EventName property to the event declared inside that interface. Note that this can be visually done from VS.NET Properties window. Note how EventName and InterfaceType have both been set:



The next activity to follow HandleExternalEventActivity is CallExternalMethodActivity activity which allows a workflow to call a method on the host. In this example, CallExternalMethodActivity is used to tell the host that an order has been received (processing not yet started.) As expected, properties for this activity must be initialized with the ExternalDataExchangeAttribute-annotated interface (IOrderingService) and with the name of a method within this interface that this activity can use to call into the host. Again, this can be very easily done by highlighting this activity and setting the appropriate properties in the Porperties window. Note how InterfaceType and MethodName have both been set:



Finally, the SetStateActivity is used to transition from this state to the ProcessOrder state. Recall that a state machine moves from one state to another through various events. This transition between states is done by the SetStateActivity. Again, use the Properties window to initialize SetStateActivity with the target state name:

ProcessOrder

ProcessOrder state contains only one activity called initOpenOrderActivity - a StateInitializationActivity activity to initialize order processing. StateInitializationActivity is usually used (as in this example) as a container of other child activities that are executed when the state is transitioned to, i.e., when the state is initialized. Unlike the EventDrivenActivity used in the WaitForOrder state, StateInitializationActivity does not have to respond to events. It is always run when entering a new state. In this example, StateInitializationActivity contains 2 child activities of type CallExternalMethodActivity to call into the host and inform it about order progress - the first CallExternalMethodActivity informs the host that the order is being processed, while the second one informs the host that the order has been processed.

Properties for each of the above child activities are shown below. For the first two child activities, the MethodInvoking property is used to set status accordingly before calling the ItemStatusUpdate on the host:

OrderComplete

This is the end state and it does not do anything.

The final workflow looks as follows:

Code

The following shows code for the relevant sections:

Workflow service

// This interface represents the communication channel between the host and
// the workflow. his interface is implemented by the main form

[ExternalDataExchange]
public interface IOrderingService
{
    // This method is used by the workflow to send status messages back to the client
   
void ItemStatusUpdate(Guid guidID, string strNewStatus);

    // This event is fired by the host to initiate the the WaitForOrder activity.
    // 'NewOrderEventArgs' is an event args class used to pass data from host to
    // workflow
    event EventHandler<NewOrderEventArgs> NewOrder;
}

// NewOrderEventArgs is used to pass information from workflow to host. Note
// the following:
// 1. NewOrderEventArgs derives from ExternalDataEventArgs. Because IOrderingService
// is marked with ExternalDataExchangeAttribute, it must use an EventArgs type that
// derives from ExternalDataEventArgs in order to allow that event to be handled in
// workflow with a HandleExternalEventActivity
//
// 2. An event class that inherits from ExternalDataEventArgs needs to implement a
// constructor that uses the :base(instanceId) constructor.
//
// 3. NewOrderEventArgs class needs to be marked as Serializable
[Serializable]
public class NewOrderEventArgs : ExternalDataEventArgs
{
    // Data members
    private Guid orderItemId;
    private string orderItem;
    private int orderQuantity;

    // Constructor. Note that itemID is passed to the base constructor
    public NewOrderEventArgs(Guid instanceID, string item, int quantity) : base(instanceID)
    {
        this.orderItemId = instanceID;
        this.orderItem = item;
        this.orderQuantity = quantity;
    }

    // Properties
    public Guid ItemId
    {
        get { return orderItemId; }
        set { orderItemId = value; }
    }

    public string Item
    {
        get { return orderItem; }
        set { orderItem = value; }
    }

    public int Quantity
    {
        get { return orderQuantity; }
        set { orderQuantity = value; }
    }
}

// Workflow code-behind file
public sealed partial class BasicStateMachine: StateMachineWorkflowActivity
{
    #region Data members
   
private Guid orderId;
    private string orderItem;
    private int orderQuantity;
    private string orderItemStatus;
    private NewOrderEventArgs argsOrderDetails;
    #endregion

    #region Constructor
    public BasicStateMachine()
    {
        // InitializeComponent contains all code to create and initialize workflow components
        InitializeComponent();
    }
    #endregion

    #region Properties
   
public Guid Id
    {
        get { return orderId; }
        set { orderId = value; }
    }

    public string ItemStatus
    {
        get { return orderItemStatus; }
        set { orderItemStatus = value; }
    }

    public string Item
    {
        get { return orderItem; }
        set { orderItem = value; }
    }

    public NewOrderEventArgs NewOrderArgs
    {
        get { return argsOrderDetails; }
        set { argsOrderDetails = value; }
    }
    #endregion

    #region
    #endregion

    #region WaitForOrder state - MethodInvoking property

   
// This method is called before the updatestatusOrderReceived activity
    // (a CallExternalMethodActivity activity) is executed in order to cache
    // order inforamtion
   
private void OrderReceived(object sender, EventArgs e)
    {
        orderId = argsOrderDetails.ItemId;
        orderItem = argsOrderDetails.Item;
        orderQuantity = argsOrderDetails.Quantity;
        orderItemStatus = "Received order";
    }
    #endregion

    #region ProcessOrder state - MethodInvoking property

    private void OrderPrcessing(object sender, EventArgs e)
    {
        // Just update order states
       
orderItemStatus = "Processing order";
    }

    private void FinalizeOrder(object sender, EventArgs e)
    {
        orderItemStatus = "Order processed";
    }
    #endregion
}

// Workflow main class
class Program
{
    static void Main(string[] args)
    {
        using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
        {
            AutoResetEvent waitHandle = new AutoResetEvent(false);
            workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e) {waitHandle.Set();};
            workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
            {
                Console.WriteLine(e.Exception.Message);
                waitHandle.Set();
            };

            WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(BasicStateMachineWorkflow.BasicStateMachine));
            instance.Start();

            waitHandle.WaitOne();
        }
    }
}

Host

public partial class MainForm : Form, IOrderingService
{
    #region Data members
   
// Order tracking variables
   
private Dictionary<string, List<string>> orderHistory;
    private string[] inventoryItems = { "Apple", "Orange", "Banana", "Pear", "Watermelon", "Grapes" };

    // Workflow variables
    // WorkflowRuntime exposes functionality required by a host application and services
    // to configure and control the workflow runtime engine and to be notified of changes
    // to both the workflow runtime engine and any of its workflow instances
   
private WorkflowRuntime workflowRuntime = null;

    // ExternalDataExchangeService represents a service that must be added to the workflow
    // runtime engine for local services communications to be enabled. Local service
    // implementations are required to be added to the ExternalDataExchangeService for these
    // services to be properly initialized and registered. Recall that a local service
    // implementation is a class that implements an interface that is decorated with
    // ExternalDataExchangeAttribute. In this project, the local service is implemented
    // by class OrderingService
   
private ExternalDataExchangeService exchangeService = null;
    #endregion

    #region Constructors
   
public MainForm()
    {
        InitializeComponent();

        // Initialize inventory list
       
InitializeInventory();

        // Initialize workflow
       
StartWorkFlow();
    }
    #endregion

    #region Event handlers
   
private void submitButton_Click(object sender, EventArgs e)
    {
        // Get user inputs
       
string item = itemsList.SelectedItem.ToString();
        int quantity = (int)itemQuantity.Value;

        // Use the workflow runtime engine to create a workflow whose type is
        // BasicStateMachine. The WorkflowInstance class exposes methods and properties
        // that can be used to control the execution of a workflow instance. A host or a
        // service can instruct the workflow runtime engine to perform actions on a
        // workflow instance by calling the appropriate methods that are contained in
        // the WorkflowInstance class
       
WorkflowInstance wf = workflowRuntime.CreateWorkflow(typeof(BasicStateMachine));

        // Add order history information
       
string strKey = wf.InstanceId.ToString();
        List<string> lstOrder = new List<string>();
        lstOrder.Add("Item: " + item + "; Quantity:" + quantity);
        orderHistory[strKey] = lstOrder;

        // Start execution of the wf workflow instance. For 'BasicStateMachine',
        // it enters the WaitForOrder state and waits for the NewOrder event
       
wf.Start();

            // Kickstart the workflow instance by firing the NewOrder event
       
NewOrder(null, new NewOrderEventArgs(wf.InstanceId, item, quantity));
    }

    private void ordersIdList_SelectedIndexChanged(object sender, EventArgs e)
    {
        orderStatus.Text = GetOrderHistory(ordersIdList.SelectedItem.ToString());
    }
    #endregion

    #region Helpers
   
private string GetOrderHistory(string orderId)
    {
        // Retrieve the order status
        StringBuilder itemHistory = new StringBuilder();

        foreach (string status in orderHistory[orderId])
        {
            itemHistory.Append(status);
            itemHistory.Append(Environment.NewLine);
        }
        return itemHistory.ToString();
    }

    private void InitializeInventory()
    {
        // Initialize the inventory items list
        foreach (string item in inventoryItems)
        {
            itemsList.Items.Add(item);
        }
        itemsList.SelectedIndex = 0;

        // Initialize the order history collection
        orderHistory = new Dictionary<string, List<string>>();
    }

    private void StartWorkFlow()
    {
        // Start the workflow runtime
        workflowRuntime = new WorkflowRuntime();
        exchangeService = new ExternalDataExchangeService();
        workflowRuntime.AddService(exchangeService);
        exchangeService.AddService(this);
        workflowRuntime.StartRuntime();
    }
    #endregion
   
    #region IOrderingService Members

   
private delegate void ItemStatusUpdateDelegate(Guid orderId, string newStatus);

   
// This method is used by the workflow to send status messages back to the client
    public void ItemStatusUpdate(Guid guidID, string strNewStatus)
    {
        if (ordersIdList.InvokeRequired == true)
        {
            ItemStatusUpdateDelegate statusUpdate =
            new ItemStatusUpdateDelegate(ItemStatusUpdate);
            object[] args = new object[2] { guidID, strNewStatus };
            Invoke(statusUpdate, args);
        }
        else
        {
            // Add order to status collection if it doesn't exist
            if (orderHistory.ContainsKey(guidID.ToString()))
            {
                orderHistory[guidID.ToString()].Add(DateTime.Now + " - " + strNewStatus);

                // Update order status data UI info
                if (ordersIdList.Items.Contains(guidID.ToString()) == false)
                    ordersIdList.Items.Add(guidID.ToString());

                ordersIdList.SelectedItem = guidID.ToString();
                orderStatus.Text = GetOrderHistory(guidID.ToString());
            }
        }
    }

    // This event is fired by the host to initiate the the WaitForOrder activity.
    public event EventHandler<NewOrderEventArgs> NewOrder;
    #endregion
}

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.Run(new MainForm());
    }
}