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;
}
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));
}
}
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.
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:
Name each state activity by clicking the activity in the designer and changing the (Name) property in the Properties window.
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.
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 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:

This is the end state and it does not do anything.
The final workflow looks as follows:

The following shows code for the relevant sections:
// 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();
}
}
}
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());
}
}