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:
State
class - describes a simple state (doesn't include any logic, which, of course, can be added later - all logic is described by actions):
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; }
}
Transition
/ Transitions - describes transition logic from one state to another and defines the action performed during the transition:
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;}
}
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;
}
public override int GetHashCode()
{
return GetHashCode(m_initialState, n_eventArgs);
}
public static int GetHashCode(State state, StateEventArgs sevent)
{
return (state.GetHashCode() << 8) + sevent.Id;
}
public class Transitions :
System.Collections.Generic.Dictionary <int, Transition>
{
public void Add(Transition transition)
{
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));
}
}
IStateManager
/ StateManager - manages states and the transitions logic:
public interface IStateManager : IDisposable
{
void ChangeState(object sender, StateEventArgs eventArgs);
bool CheckState(object sender, StateEventArgs eventArgs);
}
public abstract class StatesManager : IStateManager
{
public delegate void StateChangedEventHandler(object sender,
StateEventArgs eventArgs);
public event StateChangedEventHandler StateChanged;
public StatesManager()
{
m_activeState = BuildTransitionsTable();
}
public virtual void Dispose()
{
}
State m_activeState = null;
public State ActiveState
{
get { return m_activeState; }
}
Transitions m_transitions = new Transitions();
public Transitions Transitions
{
get { return m_transitions; }
}
protected abstract State BuildTransitionsTable();
public virtual void ChangeState(object sender, StateEventArgs eventArgs)
{
Transition transition = m_transitions[m_activeState, eventArgs];
m_activeState = transition.finalState;
if (StateChanged != null)
StateChanged(this, eventArgs);
if (transition.action != null)
transition.action(this, eventArgs);
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:
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.
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
:
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()
{
State stop_state = new State("Stop");
State play_state = new State("Play");
State pause_state = new State("Pause");
State previous_state = new State("Previous");
State next_state = new State("Next");
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);
Transitions.Clear();
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));
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);
Transitions.Add(new Transition(stop_state,
new StateEventArgs((int)StateEvents.Play),
play_state, play_action));
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.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.