When creating a new workflow, you must make an important decision: Is the workflow we want to create an ordered workflow or a state machine workflow? WF provides two out of the box workflow execution types. To answer this question, we have to decide who is under control.
A sequential workflow is a predictable workflow. The execution path may be a branch, loop, or wait for an external event to occur, however, the final sequence workflow uses the activities, conditions, and the matching rules that we provide in the previous chapter. The workflow is controlled in the process.
A state machine workflow is an event-driven workflow. That is to say, a state machine workflow depends on additional events to drive workflow completion. We define the legal status of the workflow and the legal migration between these statuses. A workflow is always in one of the states and has to wait for the event to arrive before it is migrated to a new state. Generally, an important choice occurs outside the workflow. The state machine defines the structure to be followed, but the control belongs to the external world.
When we can encode most of the decisions in a workflow, we use the sequential workflow. When the decision is made outside the workflow, we need to use the state machine workflow. In this chapter, we will discuss in detail how a State Machine workflow works.
7.1 What is a state machine?
The state machine has been applied in computer science for a long time. You will see that they are particularly popular in response systems, such as software for video games and robots. The designer uses a state machine to model systems that use status, events, and migration.
State, indicating a situation or environment. In the following example, there is a state machine with two statuses: power on and power off. The state machine is always one of the two States.
Events are external stimuli. In the preceding example, we only have one type of event-the Click Event of the button. The state machine will respond to this event in the power on or power off state. Not all events must respond to the same event.
Transition transfers the state machine to the next state. Migration can only happen in the response to the event. Migration does not necessarily move the state machine to a new State-the migration can be loop back to the same State. When the state machine receives a button click event in the power off state, it will be migrated to the power on state. If the state machine receives a button click event in the power on state, it will be transferred to the power off state.
The concept of State Migration implies that some operations will occur before or after the migration. That is to say, the state machine not only stores the state, but also executes the code when the event arrives. In the above, when the state machine reaches a new state, it will control the flow of current by opening or closing the circuit.
7.2 WindowsState Machine in Workflow
The state machine above is quite simple, and most systems will need more advanced models. However, the concept (State, event, and migration) introduced in is the same as what we used to create a state machine in a Windows workflow.
In WF, State activity indicates a state in the state machine workflow. As the event arrives, the workflow will be migrated between State activities. The state machine workflow must specify an initial state, which is the initial state of the workflow. The state machine workflow can also specify a completion state (optional ). The workflow will end after it is migrated to the completed status.
Eventdriven activity indicates an event in the state machine. We put these activities in the State activity to indicate legal events in this state. In the eventdriven activity, we can place a series of activities to be executed when the event arrives. The last activity in the sequence is usually the setstate activity. The setstate activity specifies the next migration status.
7.3 our first state machine.
As described in Chapter 2nd, we can only use code to create workflows, use only XAML, or use code and XAML (code separation ). There is no difference in the state machine workflow. In this chapter, we will use the code separation method to create a workflow, although any of the Creation modes can work.
Our workflow will support BUG Tracking Applications. In details, as a bug is migrated from the open state to the closed state, we will track the software bug lifecycle. During the life cycle, bugs can also be in the assigned, resolved, or deferred status.
Why do we need to use a state machine to model a bug-fixing workflow? It is impossible to model the selected bug, and the bug must reach a completion state. Think about the decision required for each step in the life cycle of a bug. A new public bug requires some assessment. Is this bug repeated? Is this bug really a bug? Even if this bug is really a defect, not all of the defect will be directly transferred to a personal work pair column. We must evaluate the severity of the Bug based on the effective resources and project plans required to fix the bug. If we cannot put all the intelligence we need into the workflow, we will rely on external events to tell the workflow what decisions we have made.
7.3.1 Create a project
Just like creating most projects, select file | new project in the Visual Studio dialog box. As shown below, we will use the state machine workflow Console mode application. The project template will create a project with all the assembly we need to reference in WF programming.
The new project will include a default workflow in the file workflow1.cs. We can delete this file and add our own state machine workflow (with code separation), named bugworkflow. xoml (see below ).
The workflow designer will appear with our new state machine workflow (see below ). Currently, toolbox forms can be used, and are filled with activities in the basic activity library. However, at first we could only use a subset of activity types-these activity types are listed in the following bugflowinitalstate graph.
Before designing our state machine, we need code support. In particular, we need a service that provides events to drive workflows.
7.3.2 bugLife Cycle
The state machine will spend most of its time waiting for events from the local communication service to arrive. We have discussed the local communication service in Chapter 3rd. We need an interface to define the service contract. The interface defines some events that the service can trigger to provide data to the workflow. The interface also defines some methods that the workflow can call on the service. In the following example, our communication is unidirectional-we define only a few events.
[ExternalDataExchange]public interface IBugService{ event EventHandler<BugStateChangedEventArgs> BugOpened; event EventHandler<BugStateChangedEventArgs> BugResolved; event EventHandler<BugStateChangedEventArgs> BugClosed; event EventHandler<BugStateChangedEventArgs> BugDeferred; event EventHandler<BugStateChangedEventArgs> BugAssigned;}
The event parameters of these events require the services in the process to pass information that can be used by the workflow. For example, a piece of available information is a bug object carrying all the features of the Bug (title, description, assignment.
[Serializable]public class BugStateChangedEventArgs : ExternalDataEventArgs{ public BugStateChangedEventArgs(Guid instanceID, Bug bug):base(instanceID) { _bug = bug; WaitForIdle = true; } private Bug _bug; public Bug Bug { get { return _bug; } set { _bug = value; } }}
Services that implement the ibugservice interface will trigger an event when the bug status changes. For example, the Service may trigger events from the Smart Client application to respond to bugs in user operations in the UI. Selectively, the Service may receive updated bug information in a web service call and trigger Web Service events from ASP. NET. The core issue is that the workflow does not care about the trigger or the result of the event. The workflow only cares about the occurrence of an event.
We will use the local implementation of the Bug service interface and provide a simple method to trigger the event. Later in this chapter, we will use this service in console-mode programs to trigger events to the workflow.
public class BugService : IBugService{ public event EventHandler<BugStateChangedEventArgs> BugOpened; public void OpenBug(Guid id, Bug bug) { if (BugOpened != null) { BugOpened(null, new BugStateChangedEventArgs(id, bug)); } } //and so on …}
Now that we know the service contract that our workflow will use, we can continue to create our state machine.
7.3.3 state activity
State activity represents a state in the state machine workflow. Don't be surprised. State activity is the pillar of an event-driven workflow. In general, we can drag the State activity in the toolbox to the designer to start a workflow design. If we drag a State activity for every possible state of a software bug, we have the following designer View:
Note that the two images above use special icons in the upper left corner. The bugflowinitialstate graph has a green icon in the upper left corner, because it is the initial state of the workflow. Each state machine workflow must have an initial state, which is the state in which the workflow enters or begins. You can change the initial state by right-clicking another graph and selecting set as initial state in the context menu.
The bugclosedstate has a red icon in the upper left corner because it is in the finished state. When a workflow enters the completion state, it completes, but the completion state is optional. In many bug tracking systems, a bug can be re-opened from the closed state, but in our workflow, we set the closed state to the completed state. You can set the completion status by right-clicking a graph and selecting set as completed state in the context menu.
The next step is to define the events that the state machine will process in each State. We will use eventdriven activities to define these events.
7.3.3.1 eventdriven Activity
Eventdriven is one of the few events that we can drag out of the Toolbox and drag into the State activity. In the following, we drag eventdriven to the bugflowinitialstate. We also used the Properties window to change the name of the eventdriven activity to onbugopened.
Onbugopened represents how the state machine interacts with the bugopened event in its initial state. We cannot take advantage of this activity more in details at this level. We need to double-click onbugopened to go deep into the activity. This will take us to the detailed activity view, as shown in:
This detailed view displays a breadcrumb navigation control at the top of the designer. The intention of bread shows that we are editing the bugflowinitialstate activity in the bugflow workflow. In the middle of this view, it is the detailed view of the eventdriven activity in the onbugopened state.
In the Details View, we can see that the eventdriven activity is like an ordered activity, and it can save additional sub-activities. However, there are some constraints. The ieventactivity interface must be implemented for the first eventdriven activity. The base activity library contains three activities that meet this condition: delay activity, handleexternalevent activity, and webserviceinput activity. All of our events come from a local communication service, so we will use the handleexternalevent activity.
The following shows a handleexternalevent activity in the onbugopened activity. We changed the activity name to handlebugopenedevent and set interfacetype to reference the previously defined ibugservice interface. Finally, we select bugopened as the name of the event to be processed. We have completed all initialization, and we need to process events in our initialization workflow status.
So far, we can continue adding activities after the event handler. For example, we can add an activity to send notifications to the team members about this new bug. When we finish adding these processing activities, the last activity we want to execute will be the setstate activity, which will be mentioned later.
7.3.3.2 setstate Activity
The following event forces the state machine to migrate to the new State. We can use the setstate activity to model the migration. This activity can only appear within the state machine workflow. Setstate activity is relatively simple. This activity includes the targetstatename attribute pointing to the target State.
In the following example, we have added the setstate activity to onbugopened and set the targetstatename attribute to bugopenstate. The targetstatename Attribute Editor only includes valid status names in the available drop-down list.
Now we can click the bugflow link in the breadcrumb to view our state machine workflow. The designer identifies the setstate activity we Just configured and draws a line from the bugflowinitialstate graph to the bugopenstate (see the following ). The workflow designer provides a panoramic view of a bug workflow: It starts with the bugflowinitialstate, and is transferred to the bugopenstate when the next bugopened event notifies you of the formal generation of a new bug.
So far, we can continue to add eventdriven activities to our workflow. We need to cover all the events and migration during the bug lifecycle. One advantage of the state machine is that we control which event is legal under which specific State. For example, except for the initial status, we do not want any status to handle the bugopened event. We can also design our state machine so that a bug in the latency State will only process one bugassigned event. The following shows our state machine and has all the events and migration at the appropriate location.
Note that in the above, the bugclosedstate does not need to process any events. This status is complete, and the workflow will not process any additional events.
7.3.3.3 stateinitialization and statefinalization activities
We can drag two additional activities to the State activity: The stateinitialization activity and the statefinalization activity. A State activity can have a stateinitialization activity and a statefinalization activity.
These two activities will be executed in sequence. When the state machine is migrated to a status that includes Initialization activities, the stateinitialization activity runs. Instead, the statefinalization activity is executed as long as the state machine is migrated to a State that includes the terminated activity. With these two activities, we can execute preprocessing and post-processing in the state machine.
7.3.4 driver state machine
Starting a state machine workflow is no different from starting other workflows. First, create an instance of the workflowruntime class. We will need to host an externaldataexchangeservice at runtime, which will host the local communication services that implement the ibugservice interface in sequence. Chapter 3rd includes the local communication service and more details about externaldataexchangeservice.
ExternalDataExchangeService dataExchange;dataExchange = new ExternalDataExchangeService();workflowRuntime.AddService(dataExchange);BugService bugService = new BugService();dataExchange.AddService(bugService);WorkflowInstance instance;instance = workflowRuntime.CreateWorkflow( typeof(BugFlow));instance.Start();
The next part of the code in our program will call methods on our bug service. These methods trigger the events to be captured when the workflow is running. We have carefully arranged these events to all States of the workflow and completed them successfully.
Bug bug = new Bug();bug.Title = "Application crash while printing";bugService.OpenBug(instance.InstanceId, bug);bugService.DeferBug(instance.InstanceId, bug);bugService.AssignBug(instance.InstanceId, bug);bugService.ResolveBug(instance.InstanceId, bug);bugService.CloseBug(instance.InstanceId, bug);waitHandle.WaitOne();
One advantage of using a state machine is that if our application triggers an event that is not expected to be triggered by the current workflow status, the workflow triggers an exception. When the state machine is in its initial state, we should only trigger the bugopened event. When the state machine is in its assigned state, we should only trigger the bugresolved event. When the workflow is running, our applications follow the process described by the state machine. This provides an advantage-it ensures that incorrectly coded applications will not cause State migration, and such workflows are considered to be unavailable, therefore, enterprise-level processing will always follow the workflow code. However, it is important to note that any code that starts the unavailable event will not cause a compilation error-we will only see the error at runtime.
In real BUG Tracking Applications, it may take several weeks for a bug to reach the closed state. Fortunately, state machine workflows can use workflow services, like tracking and persistence (all described in Chapter 6th ). The persistence service can save the workflow status, detach the instances in the memory, and reload the instances when the events arrive several weeks later.
There are some unusual things about our instances. Our applications know the workflow status when the workflow triggers each event. Real applications may not know the hidden information of the workflow. Our application may not remember the status of bugs that existed for two months. In this case, it does not know the legal events to be triggered. Fortunately, WF makes such information available.
7.4 check the state machine
Think about the user interface we want to provide for the bug tracking service. We do not want to give users the opportunity to create exceptions. For example, when a bug is in a state that does not move to the closed state, we do not want to provide the close this bug button. Instead, we want the user interface to reflect the current status of the bug and only allow the user to perform valid operations. With the statemachineworkflowinstance class, we can do this.
7.4.1 statemachineworkflowinstance
The statemachineworkflowinstance class provides interfaces for us to manage and query state machine workflows. As shown in the following class diagram, this API includes the attributes that we can use to retrieve the current status name and find the valid migration of this status. This class also includes a method for setting the state machine. Although we usually want a bug to follow the workflow we designed in the state machine, we can use the setstate method to set the bug back to its initial state, or force the bug to be migrated to the off state (or any state in the middle ).
Let's modify the original example to call the following method. We will call this dumpworkflow method after calling the assignbug method of the Bug service, so the workflow should be in the assigned state.
private static void DumpStateMachine(WorkflowRuntime runtime, Guid instanceID){ StateMachineWorkflowInstance instance = new StateMachineWorkflowInstance(runtime, instanceID); Console.WriteLine("Workflow ID: {0}", instanceID); Console.WriteLine("Current State: {0}", instance.CurrentStateName); Console.WriteLine("Possible Transitions: {0}", instance.PossibleStateTransitions.Count); foreach (string name in instance.PossibleStateTransitions) { Console.WriteLine("\t{0}", name); }}
This code first uses the workflow runtime and workflow ID to retrieve workflow instance objects. Then, we print the name of the current state of the workflow, the number of legal migrations, and the name of the legal migration. The output is as follows:
We can use the above information to customize user interfaces. If the user opens this special bug in the application and the status of this bug is bugassignedstate, we 'd better provide a button to mark this bug as resolved (resolved ), or delay (defer ). These are the only legal migrations in the current status.
Another interesting attribute of the statemachineworkflowinstance class is the statehistory attribute. As you may have guessed, this attribute can give all States seen by a set of workflows. If you still remember our discussion about the tracking service in Chapter 6th, you may still remember that the tracking service is not a complete task to record the workflow execution history. If you guess the statehistory attribute will use the tracking service embedded in WF, congratulations!
7.4.2 state machine tracking
Chapter 2 provides all the details of configuration, initialization, usage tracking, and tracing information, so we will not include the same content here. To use the statehistory attribute, We must configure the workflow runtime to use the tracking service. If we try to use the statehistory attribute but do not use the tracking service in the appropriate location, we will be able to create only one invalidoperationexception.
Note: statehistory and tracking service
By the time this book was written, If we configured the tracking service in APP. config or web. config in a declarative manner, the statehistory attribute would not work. Instead, we must programmatically configure the tracking service with connection strings and pass the service to the workflow runtime.
If you want to list the status of a bug, you can use the class described in Chapter 6th, such as sqltrackingquery. We can also use the statemachineworkflowinstance class and statehistory attribute to complete all our work. Let's call the following method before closing the bug:
private static void DumpHistory(WorkflowRuntime runtime, Guid instanceID){ StateMachineWorkflowInstance instance = new StateMachineWorkflowInstance(runtime, instanceID); Console.WriteLine("State History:"); foreach (string name in instance.StateHistory) { Console.WriteLine("\t{0}", name); }}
This code outputs the following output: A list of States that can be viewed by a group of workflows, starting with recently accessed states.
Note: we can only use the statemachineworkflowinstance class when the workflow instance is still running. Once the workflow instance is complete, we must go back to the tracing service and use the Tracing Service Query to read the history of the state machine.
7.5 stacked state machine
Our first state machine is relatively simple, but it does represent the basic design of the convenient state machine. However, sometimes this direct method may be difficult to manage. Imagine if the workflow for Bug Tracking Management software requires that we allow users to close or assign a bug, regardless of the current status of the bug. We must add event-driven activities to each state for the assigned and closed events in the workflow (except for completed states ). It would be nice if we only needed a few States, but as the state machine grows, it may become boring and error-prone.
Fortunately, there is an easy solution here. The stacked state machine allows us to embed sub-states in the parent state. Sub-States are essentially inherited from their father's event-driven activities. If every state in our bug tracking workflow needs to handle bug close events with the same behavior, we only need to add an event-driven activity to the parent state, and add our bug status as the descendant of this father.
The result is that the state machine workflow itself is an instance of the statemachineworkflowinstance class, which is derived from the stateactivity class (see the following ).
If there is such information, all we need to do is to add event-driven activities for public events to our workflow, rather than to every State. In the following example, we remove event-driven activities for assigned events and bug closed events from independent bugs and place them in the parent workflow.
You will see that this step greatly reduces the complexity of our state machine. In fact, bugdefferedstate and bugresolvedstate activities do not have event-driven activities between them. They inherit the actions of the father and only process onbugassigned and onbugdeffered events. All other States also inherit these event-driven activities. The stacked state machine is very suitable for enterprise-level exceptions, such as when a customer cancels an order. If the customer cancels the workflow, the workflow must be stopped regardless of the current status.
Note: It is important to understand the stacked state machine that the setstate activity can only take the leaf state as the target-that is, the State without any substate.
Conclusion 7.6
In this chapter, we introduce the state machine in WF. A state machine consists of statuses, events, and migration. WF provides all the activities we need to model these components. A state machine is typically driven by services during local communication services, web services, or workflow running. Like tracking services and persistence services, state machines and sequential workflows work in the same way. Finally, the stacked state machine allows us to extract common event-driven activities and put them in the parent state.