Click here to Skip to main content
15,867,834 members
Articles / Programming Languages / C#
Article

A .NET State Machine Toolkit - Part I

Rate me:
Please Sign up or sign in to vote.
4.80/5 (69 votes)
29 Mar 2007CPOL18 min read 408.6K   2.5K   290   115
An introduction to the .NET State Machine Toolkit.

Contents

Introduction

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

What are state machines?

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

A light switch state machine

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

The .NET State Machine Toolkit

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

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

The ActiveStateMachine class uses the Active Object design pattern. What this means is that an ActiveStateMachine object runs in its own thread. Internally, ActiveStateMachines 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

The PassiveStateMachine class

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

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 States.

When a State receives an event, it checks to see if it has any Transitions for that event. If it does, it iterates through all of the Transitions for that event until one of them fires. If no Transitions 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

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 States have been added to its Substates collection.

There are some restrictions on which States 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

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 class

The TransitionCollection represents a collection of Transitions. Each State object has its own TransitionCollection for holding its Transitions.

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 Transitions for that event (as described above).

top

Implementing the light switch state machine

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:

C#
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:

C#
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

State machine methods

Before going any further, let's add all of the methods for our state machine:

C#
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 States 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 Transitions 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 Transitions for the event. If it does, the Transitions 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 Transitions.
  • 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

Creating states and transitions

Next, let's create the States in the constructor:

C#
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 States 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, States needed to know the number of events they will receive. The reason for this is that their TransitionCollection used an ArrayList for storing Transitions. 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 States 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:

C#
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

LightSwitch demo

We are now ready to write a simple driver program to demonstrate our LightSwitch state machine:

C#
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

Dependencies

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. ActiveStateMachines 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

Conclusion

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

History

  • 29th August, 2005
    • first version completed.
  • 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
    • Updated source code.

top

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Aside from dabbling in BASIC on his old Atari 1040ST years ago, Leslie's programming experience didn't really begin until he discovered the Internet in the late 90s. There he found a treasure trove of information about two of his favorite interests: MIDI and sound synthesis.

After spending a good deal of time calculating formulas he found on the Internet for creating new sounds by hand, he decided that an easier way would be to program the computer to do the work for him. This led him to learn C. He discovered that beyond using programming as a tool for synthesizing sound, he loved programming in and of itself.

Eventually he taught himself C++ and C#, and along the way he immersed himself in the ideas of object oriented programming. Like many of us, he gotten bitten by the design patterns bug and a copy of GOF is never far from his hands.

Now his primary interest is in creating a complete MIDI toolkit using the C# language. He hopes to create something that will become an indispensable tool for those wanting to write MIDI applications for the .NET framework.

Besides programming, his other interests are photography and playing his Les Paul guitars.

Comments and Discussions

 
GeneralRe: Extending the toolkit Pin
malikdeveloper3-Aug-06 6:04
malikdeveloper3-Aug-06 6:04 
QuestionRetrieving parameters of "Send" method Pin
perlom24-Jul-06 6:15
perlom24-Jul-06 6:15 
AnswerRe: Retrieving parameters of "Send" method Pin
Leslie Sanford24-Jul-06 6:30
Leslie Sanford24-Jul-06 6:30 
GeneralRe: Retrieving parameters of "Send" method Pin
perlom24-Jul-06 9:07
perlom24-Jul-06 9:07 
GeneralQuestion on Modelling Pin
DY_public2-Jun-06 16:54
DY_public2-Jun-06 16:54 
GeneralRe: Question on Modelling Pin
Leslie Sanford2-Jun-06 19:14
Leslie Sanford2-Jun-06 19:14 
GeneralRe: Question on Modelling Pin
DY_public3-Jun-06 1:40
DY_public3-Jun-06 1:40 
QuestionMoving to .NET v2.0? Pin
Leslie Sanford24-May-06 21:37
Leslie Sanford24-May-06 21:37 
AnswerRe: Moving to .NET v2.0? Pin
dave.dolan28-Nov-06 17:55
dave.dolan28-Nov-06 17:55 
AnswerRe: Moving to .NET v2.0? Pin
Schachman14-Dec-06 10:22
Schachman14-Dec-06 10:22 
QuestionHow to use WaitForCompletion Pin
Ajornet6-May-06 10:43
Ajornet6-May-06 10:43 
AnswerRe: How to use WaitForCompletion Pin
Leslie Sanford22-May-06 20:18
Leslie Sanford22-May-06 20:18 
GeneralRe: How to use WaitForCompletion Pin
Ajornet22-May-06 21:06
Ajornet22-May-06 21:06 
GeneralRe: How to use WaitForCompletion [modified] Pin
Leslie Sanford22-May-06 21:16
Leslie Sanford22-May-06 21:16 
QuestionWhat about SCXML? Pin
Visar Elmazi30-Apr-06 1:55
Visar Elmazi30-Apr-06 1:55 
GeneralLSCollections Pin
tommyg184-Apr-06 4:01
tommyg184-Apr-06 4:01 
GeneralRe: LSCollections Pin
Leslie Sanford4-Apr-06 6:01
Leslie Sanford4-Apr-06 6:01 
GeneralRe: LSCollections Pin
alitozan26-Jun-06 3:18
alitozan26-Jun-06 3:18 
GeneralRe: LSCollections Pin
Leslie Sanford26-Jun-06 4:42
Leslie Sanford26-Jun-06 4:42 
GeneralDelegateque and Compact Framework Pin
dvescovi23-Mar-06 5:32
dvescovi23-Mar-06 5:32 
GeneralRe: Delegateque and Compact Framework Pin
Leslie Sanford13-Jun-06 12:07
Leslie Sanford13-Jun-06 12:07 
GeneralRe: Delegateque and Compact Framework Pin
dvescovi13-Jun-06 15:12
dvescovi13-Jun-06 15:12 
GeneralRe: Delegateque and Compact Framework Pin
Leslie Sanford13-Jun-06 16:13
Leslie Sanford13-Jun-06 16:13 
GeneralRe: Delegateque and Compact Framework Pin
KonTom27-Jun-06 21:45
KonTom27-Jun-06 21:45 
GeneralRe: Delegateque and Compact Framework Pin
Leslie Sanford27-Jun-06 21:49
Leslie Sanford27-Jun-06 21:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.