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
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
Originally, the StateMachineBuilder
class used classes from ADO.NET for representing state machine data. Simple DataTable
s represented states, events, guards, etc. More complex DataTable
s 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 DataTable
s 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 StateRow
s. 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 StateRow
s as belonging to a table, this property is a kind of table within a table. StateRow
s can be added to the Substates
property, and in turn those StateRow
s can have StateRow
s 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
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 Transition
s is hidden away in the base class.
top
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:
="1.0" ="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
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
Be sure to read the dependencies section in Part I.
top
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
- 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