This is the second part in a series of articles about my .NET state machine toolkit. In this part, we will take a look at its more advanced features. Our main focus will be on creating hierarchical state machines. We will use a running example to demonstrate how to use the toolkit.
top
In Part I, we built a state machine representing a light switch. In this part, we will build a slightly more complex state machine representing a traffic light. The traffic light state machine will have an On and Off state indicating whether or not it is operational. In addition, when it is in the On state, it will have three substates, Red, Yellow and Green indicating which of the lights is currently lit:
When in the On state, the state machine will periodically change which of the lights is lit (from red to green to yellow and back to red). A real traffic light would be more complex, of course, but this simplified version will suffice for demonstration purposes.
top
Let's jump into the code. Here is the beginning of our TrafficLight
state machine class:
using System;
using Sanford.Threading;
using Sanford.StateMachineToolkit;
namespace TrafficLightDemo
{
public class TrafficLight : ActiveStateMachine
{
public enum EventID
{
Dispose,
TurnOn,
TurnOff,
TimerElapsed
}
public enum StateID
{
On,
Off,
Red,
Yellow,
Green,
Disposed
}
private State on, off, red, yellow, green, disposed;
private DelegateScheduler scheduler = new DelegateScheduler();
public TrafficLight()
{
on = new State((int)StateID.On, new EntryHandler(EntryOn));
off = new State((int)StateID.Off, new EntryHandler(EntryOff));
red = new State((int)StateID.Red, new EntryHandler(EntryRed));
yellow = new State((int)StateID.Yellow,
new EntryHandler(EntryYellow));
green = new State((int)StateID.Green,
new EntryHandler(EntryGreen));
disposed = new State((int)StateID.Disposed,
new EntryHandler(EntryDisposed));
on.Substates.Add(red);
on.Substates.Add(yellow);
on.Substates.Add(green);
on.InitialState = red;
on.HistoryType = HistoryType.Shallow;
Initialize(off);
}
#region Entry/Exit Methods
#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);
}
}
}
This is pretty much boilerplate code and is very similar to the LightSwitch
state machine class in Part I. There are a few differences, though. Note that the red
, yellow
, and green
State
s have been added to the on
State
's Substates
property. This makes, not surprisingly, the red
, yellow
, and green
State
s substates of the on
State
.
top
Superstates have an initial state. A superstate's initial state is one of its substates and is the state that is entered when the superstate is the target of a state transition. In practical terms what that means for our traffic light state machine is that since the On state is a superstate to the Red, Yellow, and Green substates, it needs an initial state, and that initial state will be one of its substates. We will make the Red substate the initial state to the On superstate. When the On state is first entered, it will in turn enter the Red state. Implementing this is simply a matter of assigning the substate to the superstate's InitialState
property. In this case, the red
State
is assigned to the on
State
's InitialState
property. Care must be taken because if the State
has not already been made a substate to the superstate, an exception will be thrown when it is made the superstate's initial state.
The image shows a hierarchical state machine. Substates are nested inside their superstates. A solid circle connected with a line and ending with an arrow points to the initial state.
top
Note in the TrafficLight
code that the on
's HistoryType
property has been set to Shallow
. Superstates can have a history type. The history type determines how deeply a superstate will remember which of its substates was active when it exits. There are three types of history, None
, Shallow
, and Deep
. With a history type of None
, the superstate will not remember which substate was last active and will always target its initial state when it is entered.
A Shallow
history type indicates that a superstate will remember which substate was active one level down. There can be more than one level of nesting with superstates and substates (see the above state chart). A Shallow
history type only goes one level down at which point the substates will target their initial states when reentered.
A Deep
history type indicates that a superstate will remember which substate was active all the way down to the bottom of the state hierarchy. When a superstate is entered, it in turn enters the last active substate which in turn enters its last active substate, and so on. In the case where there is only one level of substates, as is the case with our TrafficLight
, Shallow
and Deep
history types are equivalent.
A Shallow
history type for the on
State
for our TrafficLight
means in practical terms that it will remember which light was lit when it is turned off and turned back on. So if the green light was lit when the traffic light was turned off, it will remember when it is turned back on and light up the green light (target the Green state).
Shallow history is indicated by an H inside a circle. An asterisks is added to indicate a Deep
history.
top
If you look at the TrafficLight
code, you'll notice that no transitions have yet been added. Let's describe in more detail how the TrafficLight
should behave:
When the TrafficLight
is in the Off state, it will transition to the On state when it receives a TurnOn
event. And when the TrafficLight
is in the On state, it will transition to the Off state when it receives a TurnOff
event, regardless of which substate it is in. While in one of the On's substates, the state machine will respond to a TimerElapsed
event. When it receives a TimerElapsed
event while in one of the On's substates, it will transition to another state based on which substate it is currently in. For example, if it is in the Red substate, it will transition to the Green substate.
When one of the On's substates is entered, it will use the DelegateScheduler
object for scheduling the next timer event. The DelegateScheduler
class belongs to my Sanford.Threading
namespace. It was originally part of the State Machine Toolkit, but I have moved it to my new threading namespace along with the DelegateQueue
class. The DelegateScheduler
class allows you to scheduler delegate invocations. We use it here to signal when the lights should change.
We want the state machine to remain in the Red and Green substates longer than in the Yellow substate. So when entering the Red or Green substates, the timer event will be scheduled to last longer than it will be when entering the Yellow substate. This is to mimic of how a real traffic light behaves.
Here is the constructor with the Transition
s added:
public TrafficLight()
{
on = new State((int)StateID.On, new EntryHandler(EntryOn));
off = new State((int)StateID.Off, new EntryHandler(EntryOff));
red = new State((int)StateID.Red, new EntryHandler(EntryRed));
yellow = new State((int)StateID.Yellow, new EntryHandler(EntryYellow));
green = new State((int)StateID.Green, new EntryHandler(EntryGreen));
disposed = new State((int)StateID.Disposed,
new EntryHandler(EntryDisposed));
on.Substates.Add(red);
on.Substates.Add(yellow);
on.Substates.Add(green);
on.InitialState = red;
on.HistoryType = HistoryType.Shallow;
Transition trans = new Transition(off);
on.Transitions.Add((int)EventID.TurnOff, trans);
trans = new Transition(on);
off.Transitions.Add((int)EventID.TurnOn, trans);
trans = new Transition(green);
red.Transitions.Add((int)EventID.TimerElapsed, trans);
trans = new Transition(yellow);
green.Transitions.Add((int)EventID.TimerElapsed, trans);
trans = new Transition(red);
yellow.Transitions.Add((int)EventID.TimerElapsed, trans);
trans = new Transition(disposed);
off.Transitions.Add((int)EventID.Dispose, trans);
trans = new Transition(disposed);
on.Transitions.Add((int)EventID.Dispose, trans);
Initialize(off);
}
And here are the entry methods for the states. These are methods that will be invoked when their states are entered:
#region Entry/Exit Methods
private void EntryOn()
{
scheduler.Start();
}
private void EntryOff()
{
scheduler.Stop();
scheduler.Clear();
}
private void EntryRed()
{
scheduler.Add(1, 5000, new SendTimerDelegate(SendTimerEvent));
}
private void EntryYellow()
{
scheduler.Add(1, 2000, new SendTimerDelegate(SendTimerEvent));
}
private void EntryGreen()
{
scheduler.Add(1, 5000, new SendTimerDelegate(SendTimerEvent));
}
private void EntryDisposed()
{
scheduler.Dispose();
Dispose(true);
}
#endregion
Notice the entry method for the On state. It starts the DelegateScheduler
. Remember that the On state is a superstate. When it is entered, it in turn enters one of its substates. Which substate it enters is based on the current history setting. If the On state is being entered for the first time, it will enter its initial state. So after the On state's entry method is invoked, the state machine will in turn invoke one of the On state's substate's entry method. For example, if the Red state is entered next, it will invoke the entry method for the Red substate. If you look at that method, you see that when the Red substate is entered, it schedules a timing event with the DelegateScheduler
object. When this timing event expires, the delegate passed to the DelegateScheduler
will be invoked. Here, we pass it a delegate representing a method that simply sends an event to the state machine telling it that a timer event has elapsed. Also notice that when entering the Off state, the state machines stops the DelegateScheduler
. While in the Off state, there is no reason for it to be running. The outside world can tell when the TrafficLight
has changed states by listening for the TransitionCompleted
event.
(To give credit where credit is due, this use of my DelegateScheduler
class was at least partly inspired by a post to my State Machine Toolkit Part III message board by leeloo999.)
top
Here is our TrafficLight
state machine in its entirety:
using System;
using Sanford.Threading;
using Sanford.StateMachineToolkit;
namespace TrafficLightDemo
{
public class TrafficLight : StateMachine
{
public enum EventID
{
Dispose,
TurnOn,
TurnOff,
TimerElapsed
}
public enum StateID
{
On,
Off,
Red,
Yellow,
Green,
Disposed
}
private State on, off, red, yellow, green, disposed;
private DelegateScheduler scheduler = new DelegateScheduler();
public TrafficLight()
{
on = new State((int)StateID.On, new EntryHandler(EntryOn));
off = new State((int)StateID.Off, new EntryHandler(EntryOff));
red = new State((int)StateID.Red, new EntryHandler(EntryRed));
yellow = new State((int)StateID.Yellow,
new EntryHandler(EntryYellow));
green = new State((int)StateID.Green,
new EntryHandler(EntryGreen));
disposed = new State((int)StateID.Disposed,
new EntryHandler(EntryDisposed));
on.Substates.Add(red);
on.Substates.Add(yellow);
on.Substates.Add(green);
on.InitialState = red;
on.HistoryType = HistoryType.Shallow;
Transition trans = new Transition(off);
on.Transitions.Add((int)EventID.TurnOff, trans);
trans = new Transition(on);
off.Transitions.Add((int)EventID.TurnOn, trans);
trans = new Transition(green);
red.Transitions.Add((int)EventID.TimerElapsed, trans);
trans = new Transition(yellow);
green.Transitions.Add((int)EventID.TimerElapsed, trans);
trans = new Transition(red);
yellow.Transitions.Add((int)EventID.TimerElapsed, trans);
trans = new Transition(disposed);
off.Transitions.Add((int)EventID.Dispose, trans);
trans = new Transition(disposed);
on.Transitions.Add((int)EventID.Dispose, trans);
Initialize(off);
}
#region Entry/Exit Methods
private void EntryOn()
{
scheduler.Start();
}
private void EntryOff()
{
scheduler.Stop();
scheduler.Clear();
}
private void EntryRed()
{
scheduler.Add(1, 5000, new SendTimerDelegate(SendTimerEvent));
}
private void EntryYellow()
{
scheduler.Add(1, 2000, new SendTimerDelegate(SendTimerEvent));
}
private void EntryGreen()
{
scheduler.Add(1, 5000, new SendTimerDelegate(SendTimerEvent));
}
private 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);
}
}
}
top
Be sure to read the dependencies section in Part I.
top
In this article, we've taken a closer look at the advanced features of the .NET state machine toolkit. I hope you've found it interesting and helpful. In Part III, we will explore using code generation with the toolkit.
Thanks again for your time. Comments and suggestions are welcome as always.
top
- September 18th, 2005
- September 21st, 2005
- Minor edit to the Conclusion.
- October 6th, 2005
- Minor edit to the article; source code and demo project updated.
- October 25th, 2005
- Major revision to the article; source code and demo project updated.
- March 23rd, 2006
- Major revision to the article.
- May 15th, 2006
- Major revision to the article.
- October 21st, 2006
- Minor edits to the article; source code and demo project updated.
top