Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A .NET State Machine Toolkit - Part III

0.00/5 (No votes)
26 Oct 2006 5  
Using code generation with the .NET state machine toolkit.

Contents

Introduction

This is the third and final part in my series of articles about my .NET State Machine Toolkit. In Part I, I introduced the classes that make up the toolkit and demonstrated how to create a simple, flat state machine. In Part II, I discussed some of the advanced features of the toolkit and demonstrated how to create a hierarchical state machine. In this part, we will look at how to use code generation to create state machines.

Note: based on feedback, since originally submitting my article from Ramon Smits, I've vastly improved the toolkit's XML support. It now uses XML serialization directly instead of relying on a DataSet to read and write state machines as XML data. In addition, the XML schema has been greatly simplified. Many thanks to Ramon for his helpful suggestions and providing code to demonstrate his ideas.

top

The StateMachineBuilder class

Code generation is accomplished through the StateMachineBuilder class. This class follows the Builder design pattern. With this design pattern, an object is constructed in steps using a builder object. After all the necessary steps have been taken, the builder is instructed to build the object, usually by calling a Build method. After which the built object can be retrieved and used. This pattern helps break down the complex construction of an object into discrete steps. It also enables you to use the same construction process repeatedly to get different representations. With the StateMachineBuilder class, you build a CodeDom CodeNamespace object. The namespace contains an abstract class representing the state machine. This class serves as a base class for the class that you will write.

top

A Recursive State Machine Table

Originally, the StateMachineBuilder class used classes from ADO.NET for representing state machine data. Simple DataTables represented states, events, guards, etc. More complex DataTables represented state transitions, substate/superstate relationships, and so on. One-to-many relationships were established between the simple tables and the more complex tables. Basically, it was an in-memory relational database for representing state machines. Pretty nifty, or so I thought...

There were problems with this approach. The main one was that I couldn't enforce all of the rules for declaring a hierarchical state machine through data constraints alone. It was possible to enter illegal combinations of values in the tables. For example, you could declare a state to be a substate of one state and a superstate to that same state. Since a state cannot be a substate of one state and a superstate to the same state, this was nonsense. I was trying to make a relational database do the job of a compiler, and it wasn't working. In addition, the XML generated by the DataSet was overly verbose. A better approach was needed.

Instead of using a large number of DataTables to create a relational database, the StateMachineBuilder class now uses four custom classes for keeping track of states, events, guards, actions, transitions, etc. The classes are:

  • StateRow
  • StateRowCollection
  • TransitionRow
  • TransitionRowCollection

The StateRow class represents a single state. The StateRowCollection class represents a collection of StateRows. You can think of the StateRowCollection as a table of states. The TransitionRow and TransitionRowCollection classes together represent a state's transitions.

Let's look at the StateRow class first. It has the following properties:

  • Name
  • InitialState
  • HistoryType
  • Substates
  • Transitions

The Name property is the name of the state. The InitialState property is the state's initial state. If the state does not have any substates, this property is ignored when the state machine is built. The HistoryType property is the state's history type, obviously. This property is also ignored if the state does not have any substates. These three properties can be thought of as three columns in the state table.

The Substates property is interesting. It represents a StateRowCollection object. So if we can think of a collection of StateRows as belonging to a table, this property is a kind of table within a table. StateRows can be added to the Substates property, and in turn those StateRows can have StateRows added to their Substates property, and so on. This forms a tree like structure in which there is a top level of states, states that do not have a superstate, and branches descending from them representing their substates.

The Transitions property represents a TransitionRowCollection object. A state's transitions are added to this property. So each StateRow contains a table of its transitions.

Each of these classes have XML serialization attributes describing how they should be serialized as XML data. In addition, the StateRowCollection class and the TransitionRowCollection class can be data bound to a control such as the DataGrid. This makes it easy to create a GUI front end for creating state machines with the StateMachineBuilder class.

The StateMachineBuilder class has a States property representing a StateRowCollection object. It is the root of the state hierarchy. Once all of the states and their transitions have been added to the StateMachineBuilder, the state machine base class can be built. As stated earlier, the result is a CodeDom CodeNamespace object.

top

Generating Code

Let's look at some code that uses the StateMachineBuilder class to build the traffic light state machine described in Part II and display the results:

using System;
using System.Data;
using System.IO;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.Xml.Serialization;
using Sanford.StateMachineToolkit;

namespace StateMachineBuilderDemo
{
    class Class1
    {
        [STAThread]
        static void Main(string[] args)
        {
            try
            {
                StateMachineBuilder builder = new StateMachineBuilder();

                builder.NamespaceName = "StateMachineDemo";
                builder.StateMachineName = "TrafficLightBase";
                builder.InitialState = "Off";

                builder.States.Add("Disposed");

                int index = builder.States.Add("Off");
                builder.States[index].Transitions.Add("TurnOn", null, "On");
                builder.States[index].Transitions.Add("Dispose", 
                                              null, "Disposed");

                index = builder.States.Add("On", "Red", HistoryType.Shallow);
                builder.States[index].Transitions.Add("TurnOff", null, "Off");
                builder.States[index].Transitions.Add("Dispose", 
                                              null, "Disposed");

                StateRowCollection substates = builder.States[index].Substates;

                index = substates.Add("Red");
                substates[index].Transitions.Add("TimerElapsed", null, "Green");

                index = substates.Add("Yellow");
                substates[index].Transitions.Add("TimerElapsed", null, "Red"); 

                index = substates.Add("Green");
                substates[index].Transitions.Add("TimerElapsed", null, "Yellow");
                
                builder.Build();
                
                StringWriter writer = new StringWriter();

                CodeDomProvider provider = new CSharpCodeProvider();
                ICodeGenerator generator = provider.CreateGenerator();
                CodeGeneratorOptions options = new CodeGeneratorOptions();

                options.BracingStyle = "C";

                generator.GenerateCodeFromNamespace(builder.Result, 
                                                  writer, options);

                writer.Close();

                Console.Read();
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.Read();
            }
        }
    }
}

Here is the generated code:

namespace StateMachineDemo
{


    public abstract class TrafficLightBase : 
           Sanford.StateMachineToolkit.ActiveStateMachine
    {

        private Sanford.StateMachineToolkit.State stateDisposed;

        private Sanford.StateMachineToolkit.State stateOff;

        private Sanford.StateMachineToolkit.State stateOn;

        private Sanford.StateMachineToolkit.State stateRed;

        private Sanford.StateMachineToolkit.State stateYellow;

        private Sanford.StateMachineToolkit.State stateGreen;

        public TrafficLightBase()
        {
            this.Initialize();
        }

        private void Initialize()
        {
            this.InitializeStates();
            this.InitializeGuards();
            this.InitializeActions();
            this.InitializeTransitions();
            this.InitializeRelationships();
            this.InitializeHistoryTypes();
            this.InitializeInitialStates();
            this.Initialize(this.stateOff);
        }

        private void InitializeStates()
        {
            Sanford.StateMachineToolkit.EntryHandler enDisposed = 
              new Sanford.StateMachineToolkit.EntryHandler(this.EntryDisposed);
            Sanford.StateMachineToolkit.ExitHandler exDisposed = 
              new Sanford.StateMachineToolkit.ExitHandler(this.ExitDisposed);
            this.stateDisposed = new Sanford.StateMachineToolkit.State(
              ((int)(StateID.Disposed)), enDisposed, exDisposed);
            Sanford.StateMachineToolkit.EntryHandler enOff = 
              new Sanford.StateMachineToolkit.EntryHandler(this.EntryOff);
            Sanford.StateMachineToolkit.ExitHandler exOff = 
              new Sanford.StateMachineToolkit.ExitHandler(this.ExitOff);
            this.stateOff = new Sanford.StateMachineToolkit.State(
              ((int)(StateID.Off)), enOff, exOff);
            Sanford.StateMachineToolkit.EntryHandler enOn = 
              new Sanford.StateMachineToolkit.EntryHandler(this.EntryOn);
            Sanford.StateMachineToolkit.ExitHandler exOn = 
              new Sanford.StateMachineToolkit.ExitHandler(this.ExitOn);
            this.stateOn = new Sanford.StateMachineToolkit.State(
              ((int)(StateID.On)), enOn, exOn);
            Sanford.StateMachineToolkit.EntryHandler enRed = 
              new Sanford.StateMachineToolkit.EntryHandler(this.EntryRed);
            Sanford.StateMachineToolkit.ExitHandler exRed = 
              new Sanford.StateMachineToolkit.ExitHandler(this.ExitRed);
            this.stateRed = new Sanford.StateMachineToolkit.State(
              ((int)(StateID.Red)), enRed, exRed);
            Sanford.StateMachineToolkit.EntryHandler enYellow = 
              new Sanford.StateMachineToolkit.EntryHandler(this.EntryYellow);
            Sanford.StateMachineToolkit.ExitHandler exYellow = 
              new Sanford.StateMachineToolkit.ExitHandler(this.ExitYellow);
            this.stateYellow = new Sanford.StateMachineToolkit.State(
              ((int)(StateID.Yellow)), enYellow, exYellow);
            Sanford.StateMachineToolkit.EntryHandler enGreen = 
              new Sanford.StateMachineToolkit.EntryHandler(this.EntryGreen);
            Sanford.StateMachineToolkit.ExitHandler exGreen = 
              new Sanford.StateMachineToolkit.ExitHandler(this.ExitGreen);
            this.stateGreen = new Sanford.StateMachineToolkit.State(
              ((int)(StateID.Green)), enGreen, exGreen);
        }

        private void InitializeGuards()
        {
        }

        private void InitializeActions()
        {
        }

        private void InitializeTransitions()
        {
            Sanford.StateMachineToolkit.Transition trans;
            trans = new Sanford.StateMachineToolkit.Transition(null, 
                                                  this.stateYellow);
            this.stateGreen.Transitions.Add(((int)(EventID.TimerElapsed)), 
                                                                  trans);
            trans = new Sanford.StateMachineToolkit.Transition(null, 
                                                      this.stateOn);
            this.stateOff.Transitions.Add(((int)(EventID.TurnOn)), trans);
            trans = new Sanford.StateMachineToolkit.Transition(null, 
                                                this.stateDisposed);
            this.stateOff.Transitions.Add(((int)(EventID.Dispose)), trans);
            trans = new Sanford.StateMachineToolkit.Transition(null, 
                                                     this.stateOff);
            this.stateOn.Transitions.Add(((int)(EventID.TurnOff)), trans);
            trans = new Sanford.StateMachineToolkit.Transition(null, 
                                                this.stateDisposed);
            this.stateOn.Transitions.Add(((int)(EventID.Dispose)), trans);
            trans = new Sanford.StateMachineToolkit.Transition(null, 
                                                   this.stateGreen);
            this.stateRed.Transitions.Add(((int)(EventID.TimerElapsed)), 
                                                                 trans);
            trans = new Sanford.StateMachineToolkit.Transition(null, 
                                                     this.stateRed);
            this.stateYellow.Transitions.Add(((int)(EventID.TimerElapsed)), 
                                                                    trans);
        }

        private void InitializeRelationships()
        {
            this.stateOn.Substates.Add(this.stateGreen);
            this.stateOn.Substates.Add(this.stateRed);
            this.stateOn.Substates.Add(this.stateYellow);
        }

        private void InitializeHistoryTypes()
        {
            this.stateDisposed.HistoryType = 
              Sanford.StateMachineToolkit.HistoryType.None;
            this.stateGreen.HistoryType = 
              Sanford.StateMachineToolkit.HistoryType.None;
            this.stateOff.HistoryType = 
              Sanford.StateMachineToolkit.HistoryType.None;
            this.stateOn.HistoryType = 
              Sanford.StateMachineToolkit.HistoryType.Shallow;
            this.stateRed.HistoryType = 
              Sanford.StateMachineToolkit.HistoryType.None;
            this.stateYellow.HistoryType = 
              Sanford.StateMachineToolkit.HistoryType.None;
        }

        private void InitializeInitialStates()
        {
            this.stateOn.InitialState = this.stateRed;
        }

        protected virtual void EntryDisposed()
        {
        }

        protected virtual void EntryOff()
        {
        }

        protected virtual void EntryOn()
        {
        }

        protected virtual void EntryRed()
        {
        }

        protected virtual void EntryYellow()
        {
        }

        protected virtual void EntryGreen()
        {
        }

        protected virtual void ExitDisposed()
        {
        }

        protected virtual void ExitOff()
        {
        }

        protected virtual void ExitOn()
        {
        }

        protected virtual void ExitRed()
        {
        }

        protected virtual void ExitYellow()
        {
        }

        protected virtual void ExitGreen()
        {
        }

        public enum EventID
        {

            TurnOn,

            Dispose,

            TurnOff,

            TimerElapsed,
        }

        public enum StateID
        {

            Disposed,

            Off,

            On,

            Red,

            Yellow,

            Green,
        }
    }
}

Yes, the code is ugly and verbose. This is due in part to the fully qualified names CodeDom is using. However, this is a code you never have to touch or look at. The class generated is the base class from which you derive your own state machine class. The advantage of this approach is that if you need to change the state machine, such as adding an event, you can regenerate the code and your derived class is not touched, only the base class is regenerated. You may need to make some minor tweaks to your derived class depending on what changes you make, but your implementation is not overwritten.

The entry and exit methods are made virtual with do-nothing implementations. This in effect makes them optional. In your derived class, if you need to add behavior for entry and/or exit actions, you can override the methods you need and implement the behavior. The guard and action methods, however, are abstract. You must override these.

Here is the new TrafficLight class. It is derived from the TrafficLightBase generated by the StateMachineBuilder:

using System;
using Sanford.Threading;
using Sanford.StateMachineToolkit;

namespace StateMachineDemo
{
    public class TrafficLight : TrafficLightBase
    {
        private DelegateScheduler scheduler = new DelegateScheduler();

        public TrafficLight()
        {
        }

        #region Entry/Exit Methods

        protected override void EntryOn()
        {
            scheduler.Start();
        }

        protected override void EntryOff()
        {
            scheduler.Stop();
            scheduler.Clear();
        }

        protected override void EntryRed()
        {
            scheduler.Add(1, 5000, new SendTimerDelegate(SendTimerEvent));
        }

        protected override void EntryYellow()
        {
            scheduler.Add(1, 2000, new SendTimerDelegate(SendTimerEvent));
        }

        protected override void EntryGreen()
        {
            scheduler.Add(1, 5000, new SendTimerDelegate(SendTimerEvent));
        }

        protected override void EntryDisposed()
        {
            scheduler.Dispose();

            Dispose(true);
        }

        #endregion

        public override void Dispose()
        {
            #region Guard

            if(IsDisposed)
            {
                return;
            }

            #endregion

            Send((int)EventID.Dispose);            
        }

        private delegate void SendTimerDelegate();

        private void SendTimerEvent()
        {
            Send((int)EventID.TimerElapsed);
        }
    }
}

Compare this version with the version in Part II. All of the code for creating and initializing the State objects as well as their Transitions is hidden away in the base class.

top

Hierarchical State Machines in XML

The StateMachineBuilder class can be serialized as XML data. This lets you save state machine values and retrieve them later. Before looking at an XML representation of the traffic light state machine, let's look at the XML structure the toolkit uses to represent hierarchical state machines. We will examine each element and their attributes.

The root element is stateMachine, and it has three attributes:

  • namespace
  • name
  • initialState

The namespace attribute is the name of the namespace in which the state machine class resides. The name attribute is the name of the state machine. And the initialState attribute is the initial state of the state machine. The value of the initialState attribute must be one of the top level states. A top level state is a state that does not have a superstate; it exists at the top of the state hierarchy. Not surprisingly, states are represented by the state element. It has three attributes:

  • name
  • initialState
  • historyType

The name attribute is the name of the state. The initialState attribute is the initial state of the state; if the state has any substates, the initialState attribute represents which of its substates is entered after it is entered. And the historyType attribute represents the state's history type. It can have one of three values, None, Shallow, and Deep. If a state does not have any substates, the initialState and the historyType attributes are ignored. Otherwise the initialState attribute is required. The historyType attribute is optional, and if it is not present, the state will default to a history type value of None.

States can be nested inside other states. A nested state is the substate of the state that contains it, and it in turn can have nested states. Thus substate/superstate relationships are represented directly in the XML state machine structure.

State transitions are represented by the transition element. Transitions are nested inside the states to which they belong. The transition element has four attributes:

  • event
  • guard
  • action
  • target

The event attribute represents the event that triggered the transition. The guard attribute represents the guard that is evaluated to determine whether or not the transition should actually take place. The action attribute is the action that should be performed if the transition takes place. And the target attribute is the state target of the transition. All of the attributes are optional except for the event attribute. It must be present in all transitions.

To serialize a state machine, you would first build it with the StateMachineBuilder as we did above with the traffic light state machine. Then serialize the builder with the XmlSerializer class:

// ...




using System.Xml.Serialization;

// ...




builder.Build();

StringWriter writer = new StringWriter();
XmlSerializer serializer = 
   new XmlSerializer(typeof(StateMachineBuilder));
serializer.Serialize(writer, builder);
Console.WriteLine(writer.ToString());
writer.Close();

// ...

Here, we serialized the StateMachineBuilder to a StringWriter object so that we can display the resulting XML to the Console. This is the result of serializing the traffic light state machine:

<?xml version="1.0" encoding="utf-16"?>
<stateMachine xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      namespace="StateMachineDemo" name="TrafficLightBase" 
      initialState="Off">
  <state name="Disposed" historyType="None" />
  <state name="Off" historyType="None">
    <transition event="TurnOn" target="On" />
    <transition event="Dispose" target="Disposed" />
  </state>
  <state name="On" initialState="Red" historyType="Shallow">
    <state name="Red" historyType="None">
      <transition event="TimerElapsed" target="Green" />
    </state>
    <state name="Yellow" historyType="None">
      <transition event="TimerElapsed" target="Red" />
    </state>
    <state name="Green" historyType="None">
      <transition event="TimerElapsed" target="Yellow" />
    </state>
    <transition event="TurnOff" target="Off" />
    <transition event="Dispose" target="Disposed" />
  </state>
</stateMachine>

As you can see, the XML schema is straightforward and simple enough so that you can even declare a state machine in XML by hand.

top

The State Machine Maker

Included with the demo project is a program that provides a nice GUI for using the StateMachineBuilder class. It's easy to use. Simply enter the values into the DataGrid, build the state machine, and save the results as either C# or VB code. If you want to save the state machine values for editing later, you can save the data as an XML file. I'll explain how to use the State Machine Maker application below.

The State Machine Maker has three text boxes for setting the state machine's namespace, name, and initial state respectively. The important thing to note here is that you will get an error if you forget to enter an initial state. Every state machine must have an initial state it enters into when it is first run.

In addition, there is a DataGrid control where you add states and their transitions. The DataGrid is data bound to the StateMachineBuilder's States property so that entries made to the DataGrid are added to the StateMachineBuilder automatically. Initially, you will enter the top level states for the state machines; these are states that do not have a superstate.

After entering a top level state, you can add its substates by expanding its row and clicking the Substates link:

There you will be taken to its Substates table:

After adding the substates, you can navigate back to the State table by clicking on the navigation arrow:

A state's transitions are added the same way, only you click on the Transitions link. This takes you to the state's Transition table:

Once all of the states and their transitions have been added, you can build the state machine. An error message will be displayed if the build failed. For example, say that you forgot to enter a state's name:

If the build succeeded, you'll get a message letting you know:

After the build, you can save the results as C# or VB code:

When you save the results as code, it will save the results from the last build, not the last edit. In other words, be sure to remember to build the state machine immediately before saving it as code. You may make a change to the state machine after a build and forget this when saving it to code and wonder why your last edit isn't showing up.

top

Dependencies

Be sure to read the dependencies section in Part I.

top

Conclusion

Well, this wraps up the last article in the series. With the second version of the toolkit, I'm now comfortable with it overall. While the engine was something that I worked hard on and was satisfied with, aspects of the code generation process still felt rough around the edges to me. With some help from a fellow CP'ian, that is no longer the case. I now feel that support for code generation is up to the same level of quality as the rest of the toolkit. And I hope you find it useful. Thanks for your time.

top

History

  • September 21, 2005 - First version completed.
  • October 5, 2005 - Second version completed. Major rewrite of the article and reworking of the StateMachineBuilder class.
  • October 25, 2005 - Third version completed. Article revised.
  • May 15, 2006 - Version 4.1 completed. Article revised.
  • October 21, 2006 - Version 5 completed. Article revised.

top

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here