Introduction
This article discusses how to create hierarchical statemachines based in XML with C# code-behind.
Purpose
Statemachines allow us to represent a problem in a concrete and manageable form that generally has a common solution. The Statemachine code allows one to write a hierarchical based statemachine using XML and generate a corresponding C# code file representing the statemachine with all the semantics needed to execute the statemachine.
Creating a Statemachine
The majority of code to use the statemachine is simply writing it in XML:
<StateMachines name="<NamespaceName>" xmlns="StateMachines">
<StateMachine name="<ClassName>">
<States>
<State name="<StateName>"/>
</States>
<Events>
<Event name="<EventName>"/>
</Events>
<Transitions>
<Transition from="<StateName>"
to="<StateName>" trigger="<EventName>"/>
</Transitions>
</StateMachine>
</StateMachines>
A State
element can be another StateMachine
with the exact same format. The correct XML formatting is provided through a schema, but valid references are not checked. The following C# code is generated from the XML:
using StateMachines;
namespace StateMachines.NamespaceName
{
public partial class ClassName :
StateMachine<ClassName.States, ClassName.Events, ClassName.Transitions>
{
public enum States
{
StateName
}
public enum Events
{
EventName
}
public enum Transitions
{
EventName__StateName_StateName
}
public ClassName()
{
_States[(int)States.StateName] =
(this.New(this, States.StateName));
InitialState = _States[(int)States.StateName];
_currentState = InitialState;
_Transitions
[(int)Transitions.EventName__StateName_StateName] =
new Transition<States, Events, Transitions>
(States.StateName, States.StateName, Events.EventName);
_Transitions[(int)
Transitions.EventName__StateName_StateName].Conditions =
new Condition(() => { return true; });
}
public new void Fire(ClassName.Events Event, params object[] args)
{
switch(Event)
{
case Events.EventName:
if (_Transitions[(int)Transitions.EventName__
StateName_StateName].Eval())
{
CurrentState =
_States[(int)States.StateName];
}
break;
}
}
}
}
To use the code, you simply have to instantiate the statemachine, add handlers to its events, then fire them. To generate the statemachine, you must add the file to the StateMachine.tt parser.
States
States are objects representing the states in a statemachine. They cannot be instantiated by anything but the StateMachine and have low weight. A statemachine itself is a state.
Events
Standard OnEnter
and OnLeave
events exist for all states.
Conditionals
Each transition can have an optional set of conditions that must be passed for the transition to take place. This is to reduce the complexity to set up complex conditions. A transition may have a hierarchical conditional block that will be evaluated before a transition takes place.
<Transitions>
<Transition from="<StateName>" to="<StateName>" trigger="<EventName>">
<Conditions op="<OperationType>">
<Condition name="<ConditionName" op="<OperationType>">
</Conditions>
</Transition>
</Transitions>
<Conditions>
adds a block of conditions and maybe nested. A <Condition>
references a method in the class by "ConditionName
", and op
is the operation to combine the conditions.
Adding Functionality
In the sample code, there is an example of a statemachine of a mouse interacting with objects. The basic code of this machine is:
void B_MouseEnter(object sender, MouseEventArgs e)
{
switch (mouseSM.CurrentState._State)
{
case MouseSM.States.Waiting :
mouseSM.Waiting.Fire(Waiting.Events.IntoObject, sender);
mouseSM.Waiting.ForceCurrentState(Waiting.States.OnObject);
mouseSM.LeftButton.ForceCurrentState
(LeftButton.States.OnObject);
mouseSM.RightButton.ForceCurrentState
(RightButton.States.OnObject);
break;
case MouseSM.States.LeftButton :
mouseSM.LeftButton.Fire
(LeftButton.Events.IntoObject, sender);
mouseSM.Waiting.ForceCurrentState(Waiting.States.OnObject);
mouseSM.LeftButton.ForceCurrentState
(LeftButton.States.OnObject);
mouseSM.RightButton.ForceCurrentState
(RightButton.States.OnObject);
break;
case MouseSM.States.RightButton :
mouseSM.RightButton.Fire
(RightButton.Events.IntoObject, sender);
mouseSM.Waiting.ForceCurrentState(Waiting.States.OnObject);
mouseSM.LeftButton.ForceCurrentState
(LeftButton.States.OnObject);
mouseSM.RightButton.ForceCurrentState
(RightButton.States.OnObject);
break;
default: break;
}
CurrentState.Text = mouseSM.ActiveState.ToString();
}
The handler here simply fires the appropriate event off when an object (a Button
in this case) is interacted with. ForceCurrentState
is used in this case to make all the top level states follow each other when they are over a button or not. This is so we enter the correct substate. This could be avoided by using a common class of events and rewriting the statemachine. In this case, we actually have three statemachines working simultaneously. That is being on or off a button, and that of the mouse button being down or not. A third statemachine can be used to manage the possible combinational outcomes of those two. This could also be implemented as one statemachine and having states such as LeftButtonOnObject
and LeftButtonOffObject
.
We also hook up OnEvent
s and OnLeave
s in the sample code without console output.
Conclusion
The usefulness of this code is to reduce all the boilerplate code required in implementing the statemachine along with the type safety of C#. The major point of posting this code is to provide a foundation for your own statemachine code projects. While T4 is used, it was designed in such a way that it is not required and the code generation can easily be migrated to its own executable. One could add a visual frontend to generate the statemachines.
The files required in the project are:
- StateMachine.cs
- StateMachineGen.tt
- StateMachine.xsd
- T4Utility.tt
Things To Do
Add the ability to use a common set of events, transitions, states, and conditions. This may make the statemachine overly complex but can reduce complexity in some cases significantly.
History
- v0.5 - 02/19/2011 - First release.
- v0.55 - 02/21/2011 - Added local state fields for easy state access. Updated passing data to event handlers.