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

A .NET State Machine Toolkit - Part II

Rate me:
Please Sign up or sign in to vote.
4.80/5 (33 votes)
25 Oct 2006CPOL7 min read 207.2K   1.7K   132   46
A detailed look at using the more advanced features of the .NET state machine toolkit.

Image 1

Contents

Introduction

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

A traffic light state machine

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:

Image 2

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

Implementing hierarchical state machines

Let's jump into the code. Here is the beginning of our TrafficLight state machine class:

C#
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 States have been added to the on State's Substates property. This makes, not surprisingly, the red, yellow, and green States substates of the on State.

top

Initial state

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.

Image 3

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

History type

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

Image 4

Shallow history is indicated by an H inside a circle. An asterisks is added to indicate a Deep history.

top

Transitions

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 Transitions added:

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

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

Wrapping things up

Here is our TrafficLight state machine in its entirety:

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

Dependencies

Be sure to read the dependencies section in Part I.

top

Conclusion

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

History

  • September 18th, 2005
    • First version completed.
  • 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

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

 
QuestionResetting a states History? Pin
racurrie3-Jan-06 12:08
racurrie3-Jan-06 12:08 
AnswerRe: Resetting a states History? Pin
Leslie Sanford3-Jan-06 12:45
Leslie Sanford3-Jan-06 12:45 
GeneralRe: Resetting a states History? Pin
racurrie3-Jan-06 12:57
racurrie3-Jan-06 12:57 
GeneralRe: Resetting a states History? Pin
Leslie Sanford3-Jan-06 13:02
Leslie Sanford3-Jan-06 13:02 
QuestionState/Transition On Demand? Pin
racurrie30-Dec-05 13:31
racurrie30-Dec-05 13:31 
AnswerRe: State/Transition On Demand? Pin
Leslie Sanford31-Dec-05 14:11
Leslie Sanford31-Dec-05 14:11 
GeneralTransitions, actions and guards Pin
Ramon Smits27-Sep-05 12:04
Ramon Smits27-Sep-05 12:04 
GeneralRe: Transitions, actions and guards Pin
Leslie Sanford27-Sep-05 14:43
Leslie Sanford27-Sep-05 14:43 
Ramon Smits wrote:
The "trans" has a guard, "incrementTrans" doesn't. If I would write the following code then the statemachine will probably always perform "incrementTrans" and never "trans".

Or does your code first evaluate the transitions with guard and then those that don't have any?


It evaluates the transitions in the order they were added. So, yeah, if you added the "incrementTrans" first, the counter would just countinue to be incremented. "trans" would never fire. So care has to be taken to make sure the transitions are added in the right order.
QuestionA question about substates Pin
Marc Clifton18-Sep-05 14:50
mvaMarc Clifton18-Sep-05 14:50 
AnswerRe: A question about substates Pin
Leslie Sanford18-Sep-05 15:16
Leslie Sanford18-Sep-05 15:16 
GeneralRe: A question about substates Pin
Marc Clifton18-Sep-05 15:31
mvaMarc Clifton18-Sep-05 15:31 
GeneralRe: A question about substates Pin
Sebastien Lorion18-Sep-05 19:31
Sebastien Lorion18-Sep-05 19:31 
GeneralRe: A question about substates Pin
Leslie Sanford19-Sep-05 2:01
Leslie Sanford19-Sep-05 2:01 
GeneralRe: A question about substates Pin
Sebastien Lorion19-Sep-05 10:55
Sebastien Lorion19-Sep-05 10:55 
GeneralRe: A question about substates Pin
Leslie Sanford19-Sep-05 11:31
Leslie Sanford19-Sep-05 11:31 
GeneralRe: A question about substates Pin
Sebastien Lorion19-Sep-05 11:39
Sebastien Lorion19-Sep-05 11:39 
GeneralRe: A question about substates Pin
Leslie Sanford19-Sep-05 11:52
Leslie Sanford19-Sep-05 11:52 

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.