Introduction
Being tasked to look into ways of updating old C++ software, I needed to implement a simple state engine in C#. Wanting to create a generic solution that I can use throughout the code, I looked into having the actual state table written in XML along with a generic engine to process the table using reflection.
Using the Code
The state table in XML simply defines a number of states and are identified by number. Each state has a function name and an associated action that is carried out when the function call is complete.
="1.0"="utf-8"
<StateTable>
<State Number="0">
<Function>CheckInputs</Function>
<Action Type="SLEEP">
<SleepTime>1000</SleepTime>
</Action>
</State>
<State Number="10">
<Function>FirstPressed</Function>
<Action Type="MOVETONEXT">
<SleepTime>100</SleepTime>
<NextState>0</NextState>
</Action>
</State>
<State Number="20">
<Function>SecondPressed</Function>
<Action Type="MOVETONEXT">
<SleepTime>300</SleepTime>
<NextState>0</NextState>
</Action>
</State>
<State Number="30">
<Function>BothPressed</Function>
<Action Type="EXIT" />
</State>
<State Number="40">
<Function>NonePressed</Function>
<Action Type="MOVETONEXT">
<SleepTime>1000</SleepTime>
<NextState>0</NextState>
</Action>
</State>
</StateTable>
The state engine locates the function by using reflection and attaches it to the state's event. This is achieved by first getting state's event type and then attaching a delegate to it by specifying the target class (this.engineMethodsClass
) instance and the name of the instance method (functionName
).
System.Reflection.EventInfo eventInfo = state.GetType().GetEvent("OnState");
System.Delegate eventDelegate = System.Delegate.CreateDelegate(
eventInfo.EventHandlerType, this.engineMethodsClass, functionName);
eventInfo.AddEventHandler(state, eventDelegate);
An action is made up of the following enumeration values:
SLEEP
- causes the engine to stay at the current state and to sleep for a period (in milliseconds) MOVETONEXT
- moves to the next state (indicated by the <NextState>
tag). A sleep time can also be added here. EXIT
- causes the engine to exit
To use the state engine, first create a variable in the class that handles the state engine.
private RQAssemblies.StateEngine.Engine engine = new
RQAssemblies.StateEngine.Engine();
In order to separate the events from the main application, you can specify the class where the events can be found. In this example, we will simply point to our main application. We can then load the state
table into the engine and instruct it to Run()
.
engine.EngineMethodsClass = this;
engine.LoadStateTable(@"..\..\BasicEngine.xml");
engine.Run();
The engine runs on its own thread for execution and therefore must be stopped when the application is terminated. Place the following code in the application's Close
event.
engine.Stop();
When the engine hits a state
, it invokes the attached event. In our example state
table, we need to define 5 events, where the prototype for the event is public void functionName(Action actionOnExit)
.
public void CheckInputs(RQAssemblies.StateEngine.Action actionOnExit)
{
if((this.input1.Checked == true) && (this.input2.Checked == false))
{
actionOnExit.Type =
RQAssemblies.StateEngine.Action.ActionType.MOVETONEXT;
actionOnExit.StateNumberToMoveTo = 10;
}
else if((this.input1.Checked == false) && (this.input2.Checked == true))
{
actionOnExit.Type =
RQAssemblies.StateEngine.Action.ActionType.MOVETONEXT;
actionOnExit.StateNumberToMoveTo = 20;
}
else if((this.input1.Checked == true) && (this.input2.Checked == true))
{
actionOnExit.Type =
RQAssemblies.StateEngine.Action.ActionType.MOVETONEXT;
actionOnExit.StateNumberToMoveTo = 30;
}
else if((this.input1.Checked == false) && (this.input2.Checked == false))
{
actionOnExit.Type =
RQAssemblies.StateEngine.Action.ActionType.MOVETONEXT;
actionOnExit.StateNumberToMoveTo = 40;
}
}
public void FirstPressed(RQAssemblies.StateEngine.Action actionOnExit)
{
this.result.Text = "First Pressed";
}
public void SecondPressed(RQAssemblies.StateEngine.Action actionOnExit)
{
this.result.Text = "Second Pressed";
}
public void BothPressed(RQAssemblies.StateEngine.Action actionOnExit)
{
this.result.Text = "Both Pressed";
}
public void NonePressed(RQAssemblies.StateEngine.Action actionOnExit)
{
this.result.Text = "None Pressed";
}
Within an event, we can modify the exiting action of the state by modifying the actionOnExit
parameter.
In our example state table, state 0
simply loops around and checks the states of 2 buttons. Depending on the button states, we modify our exit action condition in order to branch to a different part of the state
table.
Points of Interest
When I first started learning C#, I viewed reflection as a breath of fresh air from C++, but struggled to find a real-world/work-related application to try it out on. Now I've found one, I can see the doors that it has opened for me to significantly reduce my coding efforts in the future.
Conclusion
The implementation of the state engine is very basic and is here to simply show how you can leverage the benefits of XML and reflection in creating a real-time state engine. Clearly, this example isn't interfacing to a real-time system as I can't ZIP my barcode scanner along with this article. In the future, I intend to modify this code to be more flexible in terms of its linking between states and providing external input states and possibly 'Yes/No' style state handling.
But in the meantime, I hope you find something useful in this article and I look forward to reading your comments.
History
- Version 1.0 - 20th July, 2004 - Original
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.