State machines have always fascinated me. There is a clockwork precision to their inner workings that appeals to me on an aesthetic level. They are also an invaluable programming tool. In building libraries and applications, I have returned to them again and again. The .NET State Machine Toolkit grew out of my interest in state machines as well as my need for a small framework to create them.
This is the first of three articles about my .NET State Machine Toolkit. This article will cover the classes that make up the core of the toolkit as well as how to create a simple, flat state machine. Part II will cover creating hierarchical state machines as well as some of the more advanced features. Part III will cover code generation as well as creating state machines with XML. Special thanks to Marc Clifton for suggesting how I could break up my article into several parts. I had been struggling with this, and his suggestion made things clear. Thanks, Marc!
top
A state machine is a model of how something behaves in response to events. It models behavior by making its responses appropriate to the state that it is currently in. How a state machine responds to an event is called a transition. A transition describes what happens when a state machine receives an event based on its current state. Usually, but not always, the way a state machine responds to an event is to take some sort of action and change its state. A state machine will sometimes test a condition to make sure it is true before performing a transition. This is called a guard.
- A state machine is a model of behavior composed of states, events, guards, actions, and transitions.
- A state is a unique condition in which a state machine can exist during its lifetime.
- An event is something that happens to a state machine.
- A transition describes how a state machine behaves in response to an event based on its current state.
- A guard is a condition that must be true before a state machine will perform a transition.
- An action is what a state machine performs during a transition.
This description of state machines introduces several abstract concepts quickly, and is not meant to be formal or complete. It is just a starting point. We will explore what state machines are, more through example than definition.
top
We will look at a very simple state machine, a light switch. It has only two states: on and off. When the light switch is in the off state and receives an event turning it on, it transitions to the on state. When the light switch is in the on state and receives an event turning it off, it transitions to the off state. This is about as simple as it gets for state machines.
The above state chart diagram illustrates the light switch state machine. States are represented by rounded rectangles. Transitions are represented by arrowed curved lines connecting the states. The arrows indicate the direction of the transition, and the lines are labeled with the name of the event that triggered the transition.
When a state machine is created, it begins its life in one of its states. This state is called the initial state. A solid circle connected by an arrowed line points to the initial state. In the case of our light switch state machine, the initial state is the off state.
State charts can include other details as well. For example, each transition can be labeled with an action that describes what action the state machine performs during the transition. A transition can be labeled with a guard as well. For a more in depth look at state charts, go here.
top
With that brief introduction to state machines, we will now look at the .NET State Machine Toolkit. It is made up of a small number of classes described below. In addition to these classes are a number of classes used for code generation, which I will cover in Part III. I will describe the role of each class as well as some important points about how they behave.
top
The StateMachine
class is the abstract base class for all state machine classes. You do not derive your state machine classes from this class but rather from one of its derived classes, either the ActiveStateMachine
class or the PassiveStateMachine
class. When I talk about the StateMachine
class in the rest of the article, I'm describing functionality and behavior common to both the ActiveStateMachine
and PassiveStateMachine
classes.
Sending an event to a StateMachine
is done by using its Send
method. This places the event and its data, if any, at the end of the queue. Later, the StateMachine
will dequeue the event and dispatch it to its current State
. Additionally, there is a SendPriority
method that places the event at the head of the queue so that it will be handled before other events already in the queue. This method has protected access so that only StateMachine
derived classes can use it. It is useful when a StateMachine
needs to send an event to itself and needs that event to be dealt with before other events.
A StateMachine
raises the TransitionCompleted
event when it has finished firing a transition. The TransitionCompletedEventArgs
class accompanies the event. It has the following properties:
StateID
- an integer value representing the ID of the current state, the target state of the transition. In the case of internal transitions, this value will not change from the previous transition.
EventID
- an integer value representing the ID of the event that triggered the transition.
ActionResult
- an object that represents the result of the action(s) associated with the transition. This property essentially allows transitions to return a value. This property can be null
if no value was set by the transition's action(s).
Error
- an Exception
object representing an exception thrown by one of the transition's actions. This property will be null
if no exception was thrown.
A client that uses a StateMachine
can listen for the TransitionCompleted
event to be fired after sending a StateMachine
an event. It can then examine the results and respond how ever it chooses. If an exception is thrown from an action, it is caught and passed along, after the transition completes through the TransitionCompleted
event.
top
The ActiveStateMachine
class uses the Active Object design pattern. What this means is that an ActiveStateMachine
object runs in its own thread. Internally, ActiveStateMachine
s use DelegateQueue
objects for handling and dispatching events. You derive your state machines from this class when you want them to be active objects.
The ActiveStateMachine
class implements the IDisposable
interface. Since it represents an active object, it needs to be disposed of at some point to shut its thread down. I made the Dispose
method virtual
so that derived ActiveStateMachine
classes can override it. Typically, a derived ActiveStateMachine
will override the Dispose
method, and when it is called, will send an event to itself using the SendPriority
method telling it to dispose of itself. In other words, disposing of an ActiveStateMachine
is treated like an event. How your state machine handles the disposing event depends on its current state. However, at some point, your state machine will need to call the ActiveStateMachine
's Dispose(bool disposing)
base class method, passing it a true
value. This lets the base class dispose of its DelegateQueue
object, thus shutting down the thread in which it is running.
top
Unlike the ActiveStateMachine
class, the PassiveStateMachine
class does not run in its own thread. Sometimes using an active object is overkill. In those cases, it is appropriate to derive your state machine from the PassiveStateMachine
class.
Because the PassiveStateMachine
is, well, passive, it has to be prodded to fire its transitions. You do this by calling its Execute
method. After sending a PassiveStateMachine
derived class one or more events, you then call Execute
. The state machine responds by dequeueing all of the events in its event queue, dispatching them one right after the other.
top
The State
class represents a state a StateMachine
can be in during its lifecycle. A State
can be a substate and/or superstate to other State
s.
When a State
receives an event, it checks to see if it has any Transition
s for that event. If it does, it iterates through all of the Transition
s for that event until one of them fires. If no Transition
s were found, the State
passes the event up to its superstate, if it has one; the process is repeated at the superstate level. This process can continue indefinitely until either a Transition
fires or the top of the state hierarchy is reached.
After processing an event, the State
returns the results to the Dispatch
method where the State
originally received the event. The results indicate whether or not a Transition
fired, and if so, the resulting State
of the Transition
. It also indicates whether or not an exception occurred during the Transition
's action (if one was performed). State machines use this information to update their current State
, if necessary.
top
The SubstateCollection
class represents a collection of substates. Each State
has a Substates
property of the SubstateCollection
type. Substates are added and removed to a State
via this property.
Substates are not represented by their own class. The State
class performs double duty, playing the role of substates and superstates when necessary. Whether or not a State
is a substate depends on whether or not it has been added to another State
's Substates
collection. And whether or not a State
is a superstate depends on whether or not any State
s have been added to its Substates
collection.
There are some restrictions on which State
s can be added as substates to another State
. The most obvious one is that a State
cannot be added to its own Substates
collection; a State
cannot be a substate to itself. Also, a State
can only be the direct substate of one other State
; you cannot add a State
to the Substates
collection of more than one State
.
top
The Transition
class represents a state transition. It can have a delegate representing a guard method which it will use to determine whether or not it should fire. It can also have one or more delegates representing action methods that it will execute when it fires. And, it can have a target State
that is the target of the Transition
.
top
The TransitionCollection
represents a collection of Transition
s. Each State
object has its own TransitionCollection
for holding its Transition
s.
When a Transition
is added to a State
's TransitionCollection
, it is registered with an event ID. This event ID is a value identifying an event a State
can receive. When a State
receives an event, it uses the event's ID to check to see if it has any Transition
s for that event (as described above).
top
Let's use the toolkit to build the light switch state machine described above. It will have two states: on
and off
. And two events: TurnOn
and TurnOff
First, we create a class that is derived from the PassiveStateMachine
class:
using System;
using Sanford.StateMachineToolkit;
namespace LightSwitchDemo
{
public class LightSwitch : PassiveStateMachine
{
public LightSwitch()
{
}
#region Entry/Exit Methods
#endregion
#region Action Methods
#endregion
}
}
This is the skeleton for our PassiveStateMachine
derived class. Notice that we created regions to mark off each of the method types we will be using. This is strictly to help the code be more readable.
Events are represented in the toolkit as integers. The values of the integers serve as IDs for the events. It is easiest to represent event IDs with an enumeration.
States are represented by the State
class. Each state has its own State
object. In addition, each state has its own ID. Like the event IDs, state IDs are integer values and are best represented by an enumeration. So, the next step is to create enumerations to represent the event and state IDs and add State
objects:
using System;
using Sanford.StateMachineToolkit;
namespace LightSwitchDemo
{
public class LightSwitch : StateMachine
{
public enum EventID
{
TurnOn,
TurnOff
}
public enum StateID
{
On,
Off
}
private State on;
private State off;
We made the enumerations public so that clients listening to the TransitionCompleted
event will have access to event and state ID values in order to identify the event and state associated with the transition.
top
Before going any further, let's add all of the methods for our state machine:
using System;
using Sanford.StateMachineToolkit;
namespace LightSwitchDemo
{
public class LightSwitch : StateMachine
{
private enum EventID
{
TurnOn,
TurnOff
}
public enum StateID
{
On,
Off
}
private State on;
private State off;
private State disposed;
public LightSwitch()
{
}
#region Entry/Exit Methods
private void EnterOn()
{
Console.WriteLine("Entering On state.");
}
private void ExitOn()
{
Console.WriteLine("Exiting On state.");
}
private void EnterOff()
{
Console.WriteLine("Entering Off state.");
}
private void ExitOff()
{
Console.WriteLine("Exiting Off state.");
}
#endregion
#region Action Methods
private void TurnOn(object[] args)
{
Console.WriteLine("Light switch turned on.");
ActionResult = "Turned on the light switch.";
}
private void TurnOff(object[] args)
{
Console.WriteLine("Light switch turned off.");
ActionResult = "Turned off the light switch.";
}
#endregion
}
}
In previous versions of the toolkit, I described using "Facade" methods to serve as light wrappers for sending events to the StateMachine
. With this version of the toolkit, I've made the Send
method of the StateMachine
class public
instead of protected
. What this means is that the facade methods are not strictly necessary; events can be sent to the StateMachine
directly using the Send
method. However, you can still write facade methods if you choose; they help hide some of the machinery for sending events. It is a matter of style.
The Entry and Exit methods are optional. Entry methods are called by State
s when they are entered, and Exit methods are called when they are exited. Here, we have Entry and Exit methods for both the on
and off
states and an Entry method for the disposed
state. As a matter of convention, we use the name Enter or Exit, with the name of the state it belongs to as a suffix. Notice that they do not take any parameters. Also, it is important to note that throwing an exception from an entry or exit method is illegal and will lead to undefined behavior.
Next, we have the Action methods. These methods represent the actions that are performed during transitions. Notice that they take an object array as their only parameter. This array represents the arguments passed to the StateMachine
's Send
method. The number of event data elements can vary from zero to many. In the case of our light switch state machine, no additional arguments are passed with the event. If something were to go wrong in our action methods, we could throw an exception. This is the only place in a StateMachine
where exceptions can be thrown. As described before, any exceptions thrown from an action are caught by the StateMachine
and passed along to the client via the TransitionCompleted
event.
In addition to the methods described above, you can have Guard methods. These are methods Transition
s use to determine whether or not they should fire. Our light switch state machine does not need any guards, so we have not added any.
Before we leave the StateMachine
's methods, let's look at how they are invoked, and how a StateMachine
typically handles an event:
In the case of ActiveStateMachine
derived classes:
- An event is sent to the
StateMachine
via its Send
method.
- The state machine enqueues its
Dispatch
method along with the event and any of its accompanying arguments, to its DelegateQueue
.
- At some point in the future, the
DelegateQueue
dequeues the Dispatch
method and invokes it, passing it the event's arguments.
And in the case of PassiveStateMachine
derived classes:
- An event is sent to the
StateMachine
via its Send
method.
- The state machine enqueues the event and any of its accompanying arguments to its event queue.
- When the
Execute
method is called, an event is dequeued. It is passed along with its arguments to the Dispatch
method.
At this point, the steps for both the passive and active state machines are the same:
- The
Dispatch
method dispatches the event to the StateMachine
's current State
.
- The
State
checks to see if it has any Transition
s for the event. If it does, the Transition
s are evaluated in the order in which they were added to the State
until one of them fires. It is during this process that the Guard methods are called, if any have been added to the Transition
s.
- If a
Transition
fires in response to an event and the Transition
has a target State
, the Exit methods are called. More than one State
may be exited. This depends on the path of the current State
to the target State
.
- If the
Transition
has an action, it is performed at this point.
- The Entry methods are called. Like the Exit methods, more than one may be called.
- If a
Transition
is fired, the TransitionCompleted
event is raised.
Passive state machines will continue dispatching events until their event queue is empty.
top
Next, let's create the State
s in the constructor:
public LightSwitch()
{
off = new State((int)StateID.Off,
new EntryHandler(EnterOff),
new ExitHandler(ExitOff));
on = new State((int)StateID.On,
new EntryHandler(EnterOn),
new ExitHandler(ExitOn));
}
Each State
is initialized with its ID. In addition, the State
s are initialized with delegates to their Entry and Exit methods. Again, Entry and Exit methods are optional. You may choose not to use them, in which case you would only pass the state's ID to the State
's constructor.
In previous versions of the toolkit, State
s needed to know the number of events they will receive. The reason for this is that their TransitionCollection
used an ArrayList
for storing Transition
s. Thus, event IDs had to be consecutive values from zero to one less than the total number of events. And the TransitionCollection
needed to know the number of events before hand. I've found this to be a brittle requirement. So I've switched over to using a hash table for storing transitions. This leaves you free to use any values for the event IDs you want. The number of events or their IDs do not have to be know by the State
s beforehand.
Now, let's set up the state transitions so that when the state machine is in the on
state and receives a TurnOff
event, it transitions into the off
state, and when the state machine is in the off
state and receives a TurnOn
event, it transitions into the on
state:
public LightSwitch()
{
off = new State((int)StateID.Off, 3,
new EntryHandler(EnterOff),
new ExitHandler(ExitOff));
on = new State((int)StateID.On, 3,
new EntryHandler(EnterOn),
new ExitHandler(ExitOn));
Transition trans;
trans = new Transition(on);
trans.Actions.Add(new ActionHandler(TurnOn));
off.Transitions.Add((int)EventID.TurnOn, trans);
trans = new Transition(off);
trans.Actions.Add(new ActionHandler(TurnOff));
on.Transitions.Add((int)EventID.TurnOff, trans);
Initialize(off);
}
When we created a Transition
, we passed it a State
object representing the target of the Transition
. After creating a Transition
, it is added to a State
's Transitions
property.
Before leaving the constructor, we initialized the StateMachine
with the initial state. This is an important step and one that's easy to forget. If you forget, you will get an InvalidOperationException
when the StateMachine
receives an event. We initialized the StateMachine
so that it will initially be in the off
state. This is done here in the constructor, but it can be done at a later time. The important thing to remember is that it must be done before the StateMachine
receives its first event.
top
We are now ready to write a simple driver program to demonstrate our LightSwitch
state machine:
using System;
using System.Threading;
using Sanford.StateMachineToolkit;
namespace LightSwitchDemo
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
LightSwitch ls = new LightSwitch();
ls.TransitionCompleted +=
new TransitionCompletedEventHandler(HandleTransitionCompleted);
ls.Send((int)LightSwitch.EventID.TurnOn);
ls.Send((int)LightSwitch.EventID.TurnOff);
ls.Send((int)LightSwitch.EventID.TurnOn);
ls.Send((int)LightSwitch.EventID.TurnOff);
ls.Execute();
Console.Read();
}
private static void HandleTransitionCompleted(object sender,
TransitionCompletedEventArgs e)
{
Console.WriteLine("Transition Completed:");
Console.WriteLine("\tState ID: {0}",
((LightSwitch.StateID)(e.StateID)).ToString());
Console.WriteLine("\tEvent ID: {0}",
((LightSwitch.EventID)(e.EventID)).ToString());
if(e.Error != null)
{
Console.WriteLine("\tException: {0}", e.Error.Message);
}
else
{
Console.WriteLine("\tException: No exception was thrown.");
}
if(e.ActionResult != null)
{
Console.WriteLine("\tAction Result: {0}",
e.ActionResult.ToString());
}
else
{
Console.WriteLine("\tAction Result: No action result.");
}
}
}
}
With the results when run:
Entering Off state.
Entering Off state.
Exiting Off state.
Light switch turned on.
Entering On state.
Transition Completed:
State ID: On
Event ID: TurnOn
Exception: No exception was thrown.
Action Result: Turned on the light switch.
Exiting On state.
Light switch turned off.
Entering Off state.
Transition Completed:
State ID: Off
Event ID: TurnOff
Exception: No exception was thrown.
Action Result: Turned off the light switch.
Exiting Off state.
Light switch turned on.
Entering On state.
Transition Completed:
State ID: On
Event ID: TurnOn
Exception: No exception was thrown.
Action Result: Turned on the light switch.
Exiting On state.
Light switch turned off.
Entering Off state.
Transition Completed:
State ID: Off
Event ID: TurnOff
Exception: No exception was thrown.
Action Result: Turned off the light switch.
Notice that the Off
state is entered first. This is because when the StateMachine
is initialized with its initial State
, it automatically enters it.
top
When you download the demo projects, which includes the source code for the toolkit, you'll find that it won't build out of the box. This is because I've deleted the bin and obj folders from each project to make the zip file smaller. These folders contain the assemblies the projects depend on to build. For this reason, you'll need to go to my website to download the assemblies the State Machine Toolkit depends on. Then you'll need to add them to each project in the solution manually.
The State Machine Toolkit depends on two other of my namespaces, the Sanford.Threading
namespace and the Sanford.Collections
namespace. The toolkit depends directly on the Sanford.Threading
namespace by using its DelegateQueue
class. ActiveStateMachine
s use this class as an event queue. The DelegateQueue
class previously belonged to the toolkit itself, but I decided that it was better suited in a different namespace; several of my other namespaces use it without needing other features of the toolkit.
The dependency on the Sanford.Collections
namespace is indirect. The DelegateQueue
class uses the Deque
class from Sanford.Collections
. So in order to use the DelegateQueue
class, the toolkit must not only reference the Sanford.Threading
assembly but also the Sanford.Collections
assembly. You can get these assemblies here.
top
As I said at the beginning, I find state machines appealing and fascinating. This toolkit has been incredibly satisfying to write. It has continued to evolve, and I hope that this latest version will be the easiest version to use yet. If you have found this article interesting and the toolkit looks useful to you, please take a look at Part II and Part III.
Take care, and as always, comments and suggestions are welcome.
top
- 29th August, 2005
- 5th October, 2005
- second version completed, article rewritten, and source code updated.
- 25th October, 2005
- third version completed, major article revision, and updated source code.
- 22nd March, 2006
- fourth version completed, major article revision, and updated source code.
- 15th May 2006
- Version 4.1 completed, major article revision, and updated source code.
- 20th October 2006
- Version 5.0 completed, major article revision, and updated source code.
- 29th March 2007
top