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

A Generic State Machine and Attempt to Generic Component

Rate me:
Please Sign up or sign in to vote.
4.80/5 (6 votes)
25 Jul 20074 min read 43.1K   380   42   1
A Generic State Machine and Attempt to Generic Component

Screenshot - GenericStateMachine.gif

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:

  1. 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.
  2. Can get event in the transition level, state level and state machine level
  3. 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.
  4. 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:

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

C#
m_Machine.HandleMessage += new MachineHandler(m_Machine_HandleMessage);

The handler is shown below:

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

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

C#
public partial class Machine<MessageType, StateType, ParamType>: Component

In the test project, I add a component unit, and change like this:

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

C#
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();
    //
    // TestMachineEditor
    //
    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:

C#
Sogrand.StateMachine.Machine`3+State
[TestStateMachine.MyMessage,TestStateMachine.MyState,System.Int32][] 

cannot be converted to type:

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Generalfound minor mistake after the editor's work, but can't edit it myself now Pin
article6-Aug-07 19:11
article6-Aug-07 19:11 

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.