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

A Simple State Machine

Rate me:
Please Sign up or sign in to vote.
4.67/5 (28 votes)
23 Jan 2014CPOL3 min read 121.4K   4.6K   117   22
Create loose coupled States using a Finite State Automation (FSM) model.

Introduction

I'm not going to explain automation and the State Design Pattern theory - you can find tons of information and examples on the Internet. Here are a few links to the known resources:

In this article, I'd like to concentrate on one of many possible implementations of a State Machine (SM), which solves the problem of tight connection between states.

Background

In the classic implementation of a State Machine, each object, when triggered by a change of state, performs certain actions, and then either changes to the next state or "rolls back" to a previous one. The drawback of such an approach is a tight connection between the state objects, each of those must be aware of its "neighbors". A change in the State Machine logic might cause multiple changes in the state object.

One of the solutions is to build a State Machine which embeds automation logic and state changes into transitions, and the state objects won't be interconnected. Such a design allows changing automation logic without change in objects, thus simplifying the construction of complex State Machines.

Also, in an example below, you can find the so-called "automatic transitions" - transitions, where a certain state is intermediate and by the end of the transition must automatically go to another state. In this example, such a state is represented by "Next state", which automatically goes to "Play" and is described by the automatic transition "next2play_transition".

Using the code

Let's start by reviewing AbstractFSM.dll, which implements a basic model of a State Machine:

AbstractFSM_demo

  1. State class - describes a simple state (doesn't include any logic, which, of course, can be added later - all logic is described by actions):
  2. C#
    public class State
    {
       private String m_sState = null;
       public State(string sSate)  {m_sState = sState;} 
       protected virtual void ChangeState( object sender, StateEventArgs eventArgs) {}
       public override string ToString() { return m_sState; }
    }
  3. Transition / Transitions - describes transition logic from one state to another and defines the action performed during the transition:
  4. C#
    //actions that are performed when state changed
    public delegate void StateAction(object sender, StateEventArgs eventArgs);
    public class Transition
    {
       private State m_initialState;
       private State initialState
       {
           get {return m_initialState;}
       } 
       
       private State m_finalState;
       private State finalState
       {
           get {return m_finalState;}
       } 
       private StateAction m_state_action;
       public StateAction action
       {
           get {return m_state_action;}
       }
       private bool m_autoMode = false;
       public bool AutoMode
       {
          get {return m_autoMode;}
       } 
       private Transition m_autoTransition = null;
       public Transition AutoTransition
       {
          get {return m_autoTransition;}
       }
       //Constructors
       public Transition(State initialState, StateEventArgs sevent, 
                         State finalState, StateAction action)
       {
          m_initialState = initialState;
          m_eventArgs = sevent;
          m_finalState = finalState;
          m_state_action = action;
       }
       
       public Transition(State initialState, StateEventArgs sevent, 
                         State finalState, StateAction action,
                         bool AutoMode, Transition autoTransition)
                : this (initialState, sevent, finalState, action)  
       {
          m_autoMode = autoMode;
          m_autoTransition = autoTransition;
       }  
       //get a unique transition key
       public override int GetHashCode()
       {
          return GetHashCode(m_initialState, n_eventArgs);
       } 
       public static int GetHashCode(State state, StateEventArgs sevent)
       {
           return (state.GetHashCode() << 8) + sevent.Id;
       }
    
    
       /// <summary>Represents a collection of transition objects.</summary>
        public class Transitions : 
               System.Collections.Generic.Dictionary <int, Transition>   
        {
            /// <summary>Adds the specified transition to the collection.</summary> 
            /// <param name="transition">Transition object</param>      
            /// <see cref="System.Collections.Generic.Dictionary {int, Transition}"/>
            /// <exception cref="System.ArgumentNullException">Key is null</exception>
            /// <exception cref="System.ArgumentException">
            ///      An transition with the same key already exists.</exception>
            public void Add(Transition transition)
            {
                // The Add method throws an exception
                // if the new key is already in the dictionary.
                try
                {
                    base.Add(transition.GetHashCode(), transition);
                }
                catch (ArgumentException)
                {
                    throw new ArgumentException(
                            "A transition with the key (Initials state " + 
                            transition.initialState + ", Event " + 
                            transition.eventArgs + ") already exists.");
                }  
            }
            //
            public Transition this[State state, StateEventArgs sevent]
            {
                get
                {
                    try
                    {
                        return this[Transition.GetHashCode(state, sevent)];
                    }
                    catch(KeyNotFoundException)
                    {
                        throw new KeyNotFoundException(
                                  "The given transition was not found.");
                    }
                }
                set
                {
                    this[Transition.GetHashCode(state, sevent)] = value;
                }
            }
            //
            public bool Remove(State state, StateEventArgs sevent)
            {
                return base.Remove(Transition.GetHashCode(state, sevent));   
            }
        }
  5. IStateManager / StateManager - manages states and the transitions logic:
  6. C#
    public interface IStateManager : IDisposable 
        {
            void ChangeState(object sender, StateEventArgs eventArgs);
            bool CheckState(object sender, StateEventArgs eventArgs);
        }
        public abstract class StatesManager : IStateManager
        {
            // Declare the delegate (if using non-generic pattern).
            public delegate void StateChangedEventHandler(object sender, 
                                 StateEventArgs eventArgs);
            // Declare the event.
            public event StateChangedEventHandler StateChanged;
    
            public StatesManager()
            {
                //build transitions and set an initial state
                m_activeState = BuildTransitionsTable(); 
            }
    
            public virtual void Dispose()
            {
                //virtual method
            }
            State m_activeState = null;
            public State ActiveState
            {
                get { return m_activeState; }
            }
    
            Transitions m_transitions = new Transitions();
            public Transitions Transitions
            {
                get { return m_transitions; }
            }
            //returns initial state
            protected abstract State BuildTransitionsTable();
            //
            public virtual void ChangeState(object sender, StateEventArgs eventArgs)
            {
                Transition transition = m_transitions[m_activeState, eventArgs];
                m_activeState = transition.finalState;
                //raise 'StateChanged' event
                 if (StateChanged != null)
                     StateChanged(this, eventArgs); 
                if (transition.action != null)
                    transition.action(this, eventArgs);
                //if the transitional is automatic - automatically go to the next state:
                if (transition.AutoMode == true && transition.AutoTransition != null)
                {
                    m_activeState = transition.AutoTransition.initialState;
                    ChangeState(sender, transition.AutoTransition.eventArgs);    
                }
            }
            public virtual bool CheckState(object sender, StateEventArgs eventArgs)
            {
                return m_transitions.ContainsKey(
                       Transition.GetHashCode(m_activeState, eventArgs));
            } 
        }

Now, let's start constructing a concrete State Manager. I will call it 'MediaPlayerStateManager' - the basic class, which creates and controls State Machine. I'd like to draw your attention to the BuildTransitionsTable() function which creates the automation logic. Let's review an example of automation logic definition using a dummy Media Player, which has Stop, Pause, Play, Previous, and Next buttons. Let's say, we need to describe a transition from the Pause state to the Play state. To do this, we will first construct a delegate OnPlay, which will be executed in the case of a state change, and describe the transition:

C#
Transitions.Add(new Transition(pause_state, 
    new StateEventArgs((int)StateEvents.Play), play_state, play_action));

Here is what this command says (in plane English): when the Play event occurs while in the Pause state, execute the OnPlay action and go to the Play state. There are also the so-called "automatic transitions", which describe the intermediate states. In this example, such a state can be represented by the Next state – when the "Next" button is pressed, the State Machine will first go to the Next state and then to the Play state.

C#
Transitions.Add(new Transition(play_state, new StateEventArgs((int)StateEvents.Next), 
            next_state, next_action, true, next2play_transition));

Shown below is the full code of MediaPlayerStateManager:

C#
class MediaPlayerStateManager : StatesManager
{

    private frmTest m_playWindow = null;
    public MediaPlayerStateManager(frmTest playWindow)
        : base()
    {
        m_playWindow = playWindow;
        ChangeState(this, new StateEventArgs((int)StateEvents.Stop));
    }
    protected override State BuildTransitionsTable()
    {
        //create states
        State stop_state = new State("Stop");   //stop pressed
        State play_state = new State("Play");   //play pressed
        State pause_state = new State("Pause"); //pause pressed
        State previous_state = new State("Previous"); //prev. song selected
        State next_state = new State("Next");   //next song selected
        //actions
        StateAction stop_action = new StateAction(OnStop);
        StateAction play_action = new StateAction(OnPlay);
        StateAction pause_action = new StateAction(OnPause);
        StateAction previous_action = new StateAction(OnPrevious);
        StateAction next_action = new StateAction(OnNext);
        //
        //clear transitions
        Transitions.Clear();
        //////////////////   build transitions table  ///////////////////////
        //pause state
        Transitions.Add(new Transition(pause_state, 
                        new StateEventArgs((int)StateEvents.Play), 
                        play_state, play_action));
        Transitions.Add(new Transition(pause_state, 
                        new StateEventArgs((int)StateEvents.Stop), 
                        stop_state, stop_action));
        //previous state
        Transition prev2play_transition = new Transition(previous_state, 
                   new StateEventArgs((int)StateEvents.Play), 
                   play_state, play_action);
        Transitions.Add(prev2play_transition);
        Transition next2play_transition = new Transition(next_state, 
                   new StateEventArgs((int)StateEvents.Play), 
                   play_state, play_action);
        Transitions.Add(next2play_transition);
        //stop state
        Transitions.Add(new Transition(stop_state, 
                        new StateEventArgs((int)StateEvents.Play), 
                        play_state, play_action));
        //play state
        Transitions.Add(new Transition(play_state, 
                        new StateEventArgs((int)StateEvents.Stop), 
                        stop_state, stop_action));
        Transitions.Add(new Transition(play_state, 
                        new StateEventArgs((int)StateEvents.Pause), 
                        pause_state, pause_action));
        Transitions.Add(new Transition(play_state, 
                        new StateEventArgs((int)StateEvents.Previos), 
                        previous_state, previous_action, true, 
                        prev2play_transition));
        Transitions.Add(new Transition(play_state, 
                        new StateEventArgs((int)StateEvents.Next), 
                        next_state, next_action, true, next2play_transition));

        return play_state;
    }

    public override void ChangeState(object sender, StateEventArgs eventArgs)
    {
        try
        {
            Transition transition = Transitions[ActiveState, eventArgs];
            m_playWindow.lstStates.Items.Insert(
               0, m_playWindow.lstStates.Items.Count.ToString("000") +
               " - State '" + transition.initialState.ToString() +
               "' was changed to a new state '" + 
               transition.finalState.ToString() +
               "' by event " + 
               Enum.GetName(typeof(StateEvents), eventArgs.Id));
            base.ChangeState(sender, eventArgs);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, ex.Source, MessageBoxButtons.OK, 
                            MessageBoxIcon.Error);
        }
    }
    private void OnStop(object sender, StateEventArgs sevent)
    {
        //m_playWindow.btnStop.Enabled = false;  
        m_playWindow.txtStatus.Text = "Stopped";
    }
    private void OnPlay(object sender, StateEventArgs sevent)
    {
        m_playWindow.txtStatus.Text = "Playing song '" + 
                     m_playWindow.lstSongs.SelectedItem.ToString();
    }
    private void OnPause(object sender, StateEventArgs sevent)
    {

        m_playWindow.txtStatus.Text = "Paused";
    }
    private void OnPrevious(object sender, StateEventArgs sevent)
    {
        m_playWindow.lstSongs.SelectedIndex -= 1;
        if (m_playWindow.lstSongs.SelectedIndex < 0)
            m_playWindow.lstSongs.SelectedIndex = 0;
    }
    private void OnNext(object sender, StateEventArgs sevent)
    {
        if (m_playWindow.lstSongs.SelectedIndex + 1 >= m_playWindow.lstSongs.Items.Count)
            m_playWindow.lstSongs.SelectedIndex = m_playWindow.lstSongs.Items.Count - 1;
        else
            m_playWindow.lstSongs.SelectedIndex += 1;
    }
}

After constructing the automation logic, all that's left is to add concrete action handlers, which will be executed automatically during state transition. Such an approach goes against the classic State design pattern, where an action is performed by a state object itself, but it's more appropriate to this State Machine model (if required, actions can be moved to the state objects).

This is my very first article, so criticisms and comments are welcome.

That's all! I hope that I won't be the only one who can benefit from my work. Special thanks go to all authors who have published excellent articles on State Machines here.

License

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


Written By
Software Developer (Senior)
Canada Canada
Has been engaging in software development for over 15 years. Has deliberately chosen this path and has no compunction about it at all. Has a nasty habit of sharing his 'out-of-office' code hoping to get some positive feedback. Smile | :)

Comments and Discussions

 
SuggestionJust read about State Machines... Pin
Sander Rossel24-Jun-14 5:48
professionalSander Rossel24-Jun-14 5:48 
GeneralRe: Just read about State Machines... Pin
morgi_a22-Apr-16 4:19
morgi_a22-Apr-16 4:19 
GeneralRe: Just read about State Machines... Pin
Sander Rossel22-Apr-16 11:54
professionalSander Rossel22-Apr-16 11:54 
GeneralMy vote of 5 Pin
yuzaihuan5-Feb-14 15:48
yuzaihuan5-Feb-14 15:48 
GeneralRe: My vote of 5 Pin
morgi_a24-Feb-14 5:35
morgi_a24-Feb-14 5:35 
Questionremark about hashcode Pin
irjigeiler28-Jan-14 2:33
irjigeiler28-Jan-14 2:33 
AnswerRe: remark about hashcode Pin
morgi_a24-Feb-14 5:58
morgi_a24-Feb-14 5:58 
GeneralRe: remark about hashcode Pin
Member 1175310828-Nov-16 21:23
Member 1175310828-Nov-16 21:23 
Did you figure out, how to workaround this?
QuestionHow to implement guards? Pin
tom3118-Jul-13 18:57
tom3118-Jul-13 18:57 
AnswerRe: How to implement guards? Pin
morgi_a23-Jan-14 4:33
morgi_a23-Jan-14 4:33 
AnswerRe: How to implement guards? Pin
Sacha Barber23-Jan-14 6:10
Sacha Barber23-Jan-14 6:10 
QuestionArticle typo Pin
kevin.goodman22-Apr-13 23:59
kevin.goodman22-Apr-13 23:59 
AnswerRe: Article typo Pin
morgi_a21-May-13 7:48
morgi_a21-May-13 7:48 
QuestionSmall bug... Pin
Harvey Manfrenjensenden14-Mar-13 4:14
Harvey Manfrenjensenden14-Mar-13 4:14 
AnswerRe: Small bug... Pin
morgi_a21-May-13 7:47
morgi_a21-May-13 7:47 
GeneralMy vote of 5 Pin
Sunil P V15-Jan-13 0:42
Sunil P V15-Jan-13 0:42 
GeneralRe: My vote of 5 Pin
morgi_a19-Feb-13 5:16
morgi_a19-Feb-13 5:16 
QuestionWhy this line? Pin
Member 143609711-Aug-10 19:27
Member 143609711-Aug-10 19:27 
GeneralNicely done :) Pin
LastZolex1-Aug-10 7:44
LastZolex1-Aug-10 7:44 
GeneralThanx! Pin
Yooakim2-Nov-09 18:58
Yooakim2-Nov-09 18:58 
GeneralVery clearly written article. Kudos! Pin
Keith Rule29-Oct-09 6:43
professionalKeith Rule29-Oct-09 6:43 
GeneralStyle Pin
Terence Wallace29-Oct-09 4:14
Terence Wallace29-Oct-09 4:14 

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.