Introduction
First, I must say that English is my foreign language, so please be patient with my first article in Code Project. When working in CTI (computer telephony integration), I often used the state machine design pattern. I wrote state machines in C++ in the past, now it's time for C#. There have been some articles about state machines in Code Project already, but seems none are implemented in Generic. In my experience, a state machine implementation will help more if:
- Declaration of state machine is compact which will help you look all the state machine logic easily, and then no graphic interface is needed. State Machine itself is easily understood.
- Can get event in the transition level, state level and state machine level
- Has type information on state or event. An Enum or a class is better than an int, you will not get an impossible event or state machine go to an impossible state.
- Yet not have to limit on which state or event type to use
Background
To achieve type information in state and event (3 and 4), I chose to implement the state machine in generic. To achieve a compact declaration (1), I used to overload the operator +
in C++. But in C#, operator overload is very limited. So I choose to use the params
keyword, use variable parameters. To get notify (2) there are delegates, so get notify is easy.
My implementation of state machine consists of:
- One or more states
- The starting state ID
With a state machine, message can put to it, and state machine will react on the input message. The message consists of:
- Message ID. Often this is an integer or enum type.
- Optional status. Because message ID often is just integer or enum, so when you need state machine to react differently on the same message ID, you'll need to use status as extra information.
- Optional parameter. Parameter is optional like status, but status will cause state machine to react differently, while parameter will not. if you want to pass more information then just message ID to state machine, and not affect state machine transition, this is the choice
The state consists of:
- The state ID
- One or more rules, which will change the state of state machine
The Rule consists of:
- the message ID, which triggers the rule
- The optional status. If used, the rule will trigger only when the trigger message has the same ID and same status. Same means use .NET
Equals
implementation
Using the code
I've just tried a small test, and I haven't used this code in real applications, so just use it at your own risk. The TestStateMachine
project uses NUnit
, you will need NUnit
to compile it.
Ok, just get some code. First look how to use the state machine:
public enum MyState
{
Off,
Free,
Working1,
Working2
}
public enum MyMessage
{
PowerOn,
PowerOff,
Start,
Stop,
}
StateMachine m_Machine;
private void button1_Click(object sender, EventArgs e)
{
State[] states = new State[]
{
new State(MyState.Off,
new Rule(MyMessage.PowerOn, MyState.Free)),
new State(MyState.Free, new StateHandler(OnEnterStopped),
new StateHandler(OnExitStopped),
new Rule(MyMessage.PowerOff, MyState.Off),
new Rule(MyMessage.Start, 1, MyState.Working1),
new Rule(MyMessage.Start, 2, MyState.Working2,
new RuleHandler(OnWork2))),
new State(MyState.Working1,
new Rule(MyMessage.PowerOff, MyState.Off),
new Rule(MyMessage.Stop, MyState.Free)),
new State(MyState.Working2,
new Rule(MyMessage.PowerOff, MyState.Off),
new Rule(MyMessage.Stop, MyState.Free))
};
m_Machine = new StateMachine(MyState.Off, states);
m_Machine.PutMessage(MyMessage.PowerOn);
if (MyState.Free != m_Machine.Current)
MessageBox.Show("wrong");
}
That's a compact declaration. MyState.Off
is the init state. States consist of all the state machine states. As you can see, state can have one or more rules. In the demo, I show the Rule level event (OnWork2
) and state level event (OnEnterStooped
, OnExitStopped
). The state machine level event does not appear. For the demo, there are lots of rules that handle the power off message, so you can handle this message in state machine level, and remove all of the rules for power off message. This can be done as follows:
m_Machine.HandleMessage += new MachineHandler(m_Machine_HandleMessage);
The handler is shown below:
void m_Machine_HandleMessage(Message message, out bool handled)
{
if (message.Id == MyMessage.PowerOff)
{
m_Machine.SetState(message, MyState.Off);
handled = true;
}
else
{
handled = false;
}
}
But wait, where is the generic? I'll show you. I wanted code to be compact, and I chose generic. But unfortunately, generic often makes code lengthy. In fact, I have added these using
statements to make the code compact:
using StateMachine = Sogrand.StateMachine.Machine<MyMessage,
MyState, int, int>;
using Message = Sogrand.StateMachine.Machine<MyMessage,
MyState, int, int>.Message;
using Rule = Sogrand.StateMachine.Machine<MyMessage,
MyState, int, int>.Rule;
using State = Sogrand.StateMachine.Machine<MyMessage,
MyState, int, int>.State;
using RuleHandler = Sogrand.StateMachine.Machine<MyMessage,
MyState, int, int>.RuleHandler;
using StateHandler = Sogrand.StateMachine.Machine<MyMessage,
MyState, int, int>.StateHandler;
using MachineHandler = Sogrand.StateMachine.Machine<MyMessage,
MyState, int, int>.MachineHandler;
The C# using
statement is like the typedef
in C/C++, but is generic (like C++ template). So, you will need to add this using
too. But if you really work with a state machine and your state machine is not trivial; these 7 extra lines will not seem so serious.
Attempt to generic component
I've tried to let the VS2005 IDE edit the state machine. If you look at the code, you'll find:
public partial class Machine<MessageType, StateType, ParamType>: Component
In the test project, I add a component unit, and change like this:
public partial class TestMachineEditor : Machine
{
public TestMachineEditor()
{
InitializeComponent();
}
public TestMachineEditor(IContainer container)
{
container.Add(this);
InitializeComponent();
}
}
public class Machine: Sogrand.StateMachine.Machine<MyMessage,
MyState, int, int>
{
}
It appears VS2005 can edit the state machine, and generate these codes:
private void InitializeComponent()
{
Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
TestStateMachine.MyState, int, int>.State state1
= new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
TestStateMachine.MyState, int, int>.State();
Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
TestStateMachine.MyState, int, int>.Rule rule1
= new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
TestStateMachine.MyState, int, int>.Rule();
Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
TestStateMachine.MyState, int, int>.State state2
= new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
TestStateMachine.MyState, int, int>.State();
state1.Id = TestStateMachine.MyState.Off;
rule1.Id = TestStateMachine.MyMessage.PowerOn;
rule1.NextState = TestStateMachine.MyState.Free;
rule1.Param = null;
state1.Rules = new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
TestStateMachine.MyState, int, int>.Rule[] {rule1};
state2.Id = TestStateMachine.MyState.Free;
state2.Rules = new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
TestStateMachine.MyState, int, int>.Rule[0];
this.States = new Sogrand.StateMachine.Machine<TestStateMachine.MyMessage,
TestStateMachine.MyState, int, int>.State[] {state1,state2};
}
Without my using
statements, the generated code is very long. But as long as the IDE generates these codes, it doesn't matter. But after save and compile, the designer report:
Object of type:
Sogrand.StateMachine.Machine`3+State
[TestStateMachine.MyMessage,TestStateMachine.MyState,System.Int32][]
cannot be converted to type:
Sogrand.StateMachine.Machine`3+State
[TestStateMachine.MyMessage,TestStateMachine.MyState,System.Int32][]
Points of Interest
To implement the generic state machine, I found the state, message, rule must be inner class of state machine class to share the state type and message type easily.
History
- 2007-7-27: first edition
- 2007-7-31: I have found that I mess up the parameter and status. It should be the status that affects the state machine transition, while parameter should not. I add the
ParamType
in state machine definition to make it more generic. Because the generic component attempt failed, I changed the state machine base class, it's not component now. And default constructor of state machine, state and rule is removed.
The two types look just the same. I think it is a VS2005 IDE limitation in generic. So my attempt to generic component failed.