|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Contents
IntroductionThis is the third and final part in my series of articles about my .NET State Machine Toolkit. In Part I, I introduced the classes that make up the toolkit and demonstrated how to create a simple, flat state machine. In Part II, I discussed some of the advanced features of the toolkit and demonstrated how to create a hierarchical state machine. In this part, we will look at how to use code generation to create state machines. Note: based on feedback, since originally submitting my article from Ramon Smits, I've vastly improved the toolkit's XML support. It now uses XML serialization directly instead of relying on a The StateMachineBuilder classCode generation is accomplished through the A Recursive State Machine TableOriginally, the There were problems with this approach. The main one was that I couldn't enforce all of the rules for declaring a hierarchical state machine through data constraints alone. It was possible to enter illegal combinations of values in the tables. For example, you could declare a state to be a substate of one state and a superstate to that same state. Since a state cannot be a substate of one state and a superstate to the same state, this was nonsense. I was trying to make a relational database do the job of a compiler, and it wasn't working. In addition, the XML generated by the Instead of using a large number of
The Let's look at the
The The The Each of these classes have XML serialization attributes describing how they should be serialized as XML data. In addition, the The Generating CodeLet's look at some code that uses the using System;
using System.Data;
using System.IO;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.Xml.Serialization;
using Sanford.StateMachineToolkit;
namespace StateMachineBuilderDemo
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
try
{
StateMachineBuilder builder = new StateMachineBuilder();
builder.NamespaceName = "StateMachineDemo";
builder.StateMachineName = "TrafficLightBase";
builder.InitialState = "Off";
builder.States.Add("Disposed");
int index = builder.States.Add("Off");
builder.States[index].Transitions.Add("TurnOn", null, "On");
builder.States[index].Transitions.Add("Dispose",
null, "Disposed");
index = builder.States.Add("On", "Red", HistoryType.Shallow);
builder.States[index].Transitions.Add("TurnOff", null, "Off");
builder.States[index].Transitions.Add("Dispose",
null, "Disposed");
StateRowCollection substates = builder.States[index].Substates;
index = substates.Add("Red");
substates[index].Transitions.Add("TimerElapsed", null, "Green");
index = substates.Add("Yellow");
substates[index].Transitions.Add("TimerElapsed", null, "Red");
index = substates.Add("Green");
substates[index].Transitions.Add("TimerElapsed", null, "Yellow");
builder.Build();
StringWriter writer = new StringWriter();
CodeDomProvider provider = new CSharpCodeProvider();
ICodeGenerator generator = provider.CreateGenerator();
CodeGeneratorOptions options = new CodeGeneratorOptions();
options.BracingStyle = "C";
generator.GenerateCodeFromNamespace(builder.Result,
writer, options);
writer.Close();
Console.Read();
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
Console.Read();
}
}
}
}
Here is the generated code: namespace StateMachineDemo
{
public abstract class TrafficLightBase :
Sanford.StateMachineToolkit.ActiveStateMachine
{
private Sanford.StateMachineToolkit.State stateDisposed;
private Sanford.StateMachineToolkit.State stateOff;
private Sanford.StateMachineToolkit.State stateOn;
private Sanford.StateMachineToolkit.State stateRed;
private Sanford.StateMachineToolkit.State stateYellow;
private Sanford.StateMachineToolkit.State stateGreen;
public TrafficLightBase()
{
this.Initialize();
}
private void Initialize()
{
this.InitializeStates();
this.InitializeGuards();
this.InitializeActions();
this.InitializeTransitions();
this.InitializeRelationships();
this.InitializeHistoryTypes();
this.InitializeInitialStates();
this.Initialize(this.stateOff);
}
private void InitializeStates()
{
Sanford.StateMachineToolkit.EntryHandler enDisposed =
new Sanford.StateMachineToolkit.EntryHandler(this.EntryDisposed);
Sanford.StateMachineToolkit.ExitHandler exDisposed =
new Sanford.StateMachineToolkit.ExitHandler(this.ExitDisposed);
this.stateDisposed = new Sanford.StateMachineToolkit.State(
((int)(StateID.Disposed)), enDisposed, exDisposed);
Sanford.StateMachineToolkit.EntryHandler enOff =
new Sanford.StateMachineToolkit.EntryHandler(this.EntryOff);
Sanford.StateMachineToolkit.ExitHandler exOff =
new Sanford.StateMachineToolkit.ExitHandler(this.ExitOff);
this.stateOff = new Sanford.StateMachineToolkit.State(
((int)(StateID.Off)), enOff, exOff);
Sanford.StateMachineToolkit.EntryHandler enOn =
new Sanford.StateMachineToolkit.EntryHandler(this.EntryOn);
Sanford.StateMachineToolkit.ExitHandler exOn =
new Sanford.StateMachineToolkit.ExitHandler(this.ExitOn);
this.stateOn = new Sanford.StateMachineToolkit.State(
((int)(StateID.On)), enOn, exOn);
Sanford.StateMachineToolkit.EntryHandler enRed =
new Sanford.StateMachineToolkit.EntryHandler(this.EntryRed);
Sanford.StateMachineToolkit.ExitHandler exRed =
new Sanford.StateMachineToolkit.ExitHandler(this.ExitRed);
this.stateRed = new Sanford.StateMachineToolkit.State(
((int)(StateID.Red)), enRed, exRed);
Sanford.StateMachineToolkit.EntryHandler enYellow =
new Sanford.StateMachineToolkit.EntryHandler(this.EntryYellow);
Sanford.StateMachineToolkit.ExitHandler exYellow =
new Sanford.StateMachineToolkit.ExitHandler(this.ExitYellow);
this.stateYellow = new Sanford.StateMachineToolkit.State(
((int)(StateID.Yellow)), enYellow, exYellow);
Sanford.StateMachineToolkit.EntryHandler enGreen =
new Sanford.StateMachineToolkit.EntryHandler(this.EntryGreen);
Sanford.StateMachineToolkit.ExitHandler exGreen =
new Sanford.StateMachineToolkit.ExitHandler(this.ExitGreen);
this.stateGreen = new Sanford.StateMachineToolkit.State(
((int)(StateID.Green)), enGreen, exGreen);
}
private void InitializeGuards()
{
}
private void InitializeActions()
{
}
private void InitializeTransitions()
{
Sanford.StateMachineToolkit.Transition trans;
trans = new Sanford.StateMachineToolkit.Transition(null,
this.stateYellow);
this.stateGreen.Transitions.Add(((int)(EventID.TimerElapsed)),
trans);
trans = new Sanford.StateMachineToolkit.Transition(null,
this.stateOn);
this.stateOff.Transitions.Add(((int)(EventID.TurnOn)), trans);
trans = new Sanford.StateMachineToolkit.Transition(null,
this.stateDisposed);
this.stateOff.Transitions.Add(((int)(EventID.Dispose)), trans);
trans = new Sanford.StateMachineToolkit.Transition(null,
this.stateOff);
this.stateOn.Transitions.Add(((int)(EventID.TurnOff)), trans);
trans = new Sanford.StateMachineToolkit.Transition(null,
this.stateDisposed);
this.stateOn.Transitions.Add(((int)(EventID.Dispose)), trans);
trans = new Sanford.StateMachineToolkit.Transition(null,
this.stateGreen);
this.stateRed.Transitions.Add(((int)(EventID.TimerElapsed)),
trans);
trans = new Sanford.StateMachineToolkit.Transition(null,
this.stateRed);
this.stateYellow.Transitions.Add(((int)(EventID.TimerElapsed)),
trans);
}
private void InitializeRelationships()
{
this.stateOn.Substates.Add(this.stateGreen);
this.stateOn.Substates.Add(this.stateRed);
this.stateOn.Substates.Add(this.stateYellow);
}
private void InitializeHistoryTypes()
{
this.stateDisposed.HistoryType =
Sanford.StateMachineToolkit.HistoryType.None;
this.stateGreen.HistoryType =
Sanford.StateMachineToolkit.HistoryType.None;
this.stateOff.HistoryType =
Sanford.StateMachineToolkit.HistoryType.None;
this.stateOn.HistoryType =
Sanford.StateMachineToolkit.HistoryType.Shallow;
this.stateRed.HistoryType =
Sanford.StateMachineToolkit.HistoryType.None;
this.stateYellow.HistoryType =
Sanford.StateMachineToolkit.HistoryType.None;
}
private void InitializeInitialStates()
{
this.stateOn.InitialState = this.stateRed;
}
protected virtual void EntryDisposed()
{
}
protected virtual void EntryOff()
{
}
protected virtual void EntryOn()
{
}
protected virtual void EntryRed()
{
}
protected virtual void EntryYellow()
{
}
protected virtual void EntryGreen()
{
}
protected virtual void ExitDisposed()
{
}
protected virtual void ExitOff()
{
}
protected virtual void ExitOn()
{
}
protected virtual void ExitRed()
{
}
protected virtual void ExitYellow()
{
}
protected virtual void ExitGreen()
{
}
public enum EventID
{
TurnOn,
Dispose,
TurnOff,
TimerElapsed,
}
public enum StateID
{
Disposed,
Off,
On,
Red,
Yellow,
Green,
}
}
}
Yes, the code is ugly and verbose. This is due in part to the fully qualified names CodeDom is using. However, this is a code you never have to touch or look at. The class generated is the base class from which you derive your own state machine class. The advantage of this approach is that if you need to change the state machine, such as adding an event, you can regenerate the code and your derived class is not touched, only the base class is regenerated. You may need to make some minor tweaks to your derived class depending on what changes you make, but your implementation is not overwritten. The entry and exit methods are made virtual with do-nothing implementations. This in effect makes them optional. In your derived class, if you need to add behavior for entry and/or exit actions, you can override the methods you need and implement the behavior. The guard and action methods, however, are abstract. You must override these. Here is the new using System;
using Sanford.Threading;
using Sanford.StateMachineToolkit;
namespace StateMachineDemo
{
public class TrafficLight : TrafficLightBase
{
private DelegateScheduler scheduler = new DelegateScheduler();
public TrafficLight()
{
}
#region Entry/Exit Methods
protected override void EntryOn()
{
scheduler.Start();
}
protected override void EntryOff()
{
scheduler.Stop();
scheduler.Clear();
}
protected override void EntryRed()
{
scheduler.Add(1, 5000, new SendTimerDelegate(SendTimerEvent));
}
protected override void EntryYellow()
{
scheduler.Add(1, 2000, new SendTimerDelegate(SendTimerEvent));
}
protected override void EntryGreen()
{
scheduler.Add(1, 5000, new SendTimerDelegate(SendTimerEvent));
}
protected override void EntryDisposed()
{
scheduler.Dispose();
Dispose(true);
}
#endregion
public override void Dispose()
{
#region Guard
if(IsDisposed)
{
return;
}
#endregion
Send((int)EventID.Dispose);
}
private delegate void SendTimerDelegate();
private void SendTimerEvent()
{
Send((int)EventID.TimerElapsed);
}
}
}
Compare this version with the version in Part II. All of the code for creating and initializing the Hierarchical State Machines in XMLThe The root element is
The
The States can be nested inside other states. A nested state is the substate of the state that contains it, and it in turn can have nested states. Thus substate/superstate relationships are represented directly in the XML state machine structure. State transitions are represented by the
The To serialize a state machine, you would first build it with the // ...
using System.Xml.Serialization;
// ...
builder.Build();
StringWriter writer = new StringWriter();
XmlSerializer serializer =
new XmlSerializer(typeof(StateMachineBuilder));
serializer.Serialize(writer, builder);
Console.WriteLine(writer.ToString());
writer.Close();
// ...
Here, we serialized the <?xml version="1.0" encoding="utf-16"?>
<stateMachine xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
namespace="StateMachineDemo" name="TrafficLightBase"
initialState="Off">
<state name="Disposed" historyType="None" />
<state name="Off" historyType="None">
<transition event="TurnOn" target="On" />
<transition event="Dispose" target="Disposed" />
</state>
<state name="On" initialState="Red" historyType="Shallow">
<state name="Red" historyType="None">
<transition event="TimerElapsed" target="Green" />
</state>
<state name="Yellow" historyType="None">
<transition event="TimerElapsed" target="Red" />
</state>
<state name="Green" historyType="None">
<transition event="TimerElapsed" target="Yellow" />
</state>
<transition event="TurnOff" target="Off" />
<transition event="Dispose" target="Disposed" />
</state>
</stateMachine>
As you can see, the XML schema is straightforward and simple enough so that you can even declare a state machine in XML by hand. The State Machine MakerIncluded with the demo project is a program that provides a nice GUI for using the The State Machine Maker has three text boxes for setting the state machine's namespace, name, and initial state respectively. The important thing to note here is that you will get an error if you forget to enter an initial state. Every state machine must have an initial state it enters into when it is first run. In addition, there is a After entering a top level state, you can add its substates by expanding its row and clicking the Substates link:
There you will be taken to its Substates table:
After adding the substates, you can navigate back to the State table by clicking on the navigation arrow:
A state's transitions are added the same way, only you click on the Transitions link. This takes you to the state's Transition table:
Once all of the states and their transitions have been added, you can build the state machine. An error message will be displayed if the build failed. For example, say that you forgot to enter a state's name:
If the build succeeded, you'll get a message letting you know:
After the build, you can save the results as C# or VB code:
When you save the results as code, it will save the results from the last build, not the last edit. In other words, be sure to remember to build the state machine immediately before saving it as code. You may make a change to the state machine after a build and forget this when saving it to code and wonder why your last edit isn't showing up. DependenciesBe sure to read the dependencies section in Part I. ConclusionWell, this wraps up the last article in the series. With the second version of the toolkit, I'm now comfortable with it overall. While the engine was something that I worked hard on and was satisfied with, aspects of the code generation process still felt rough around the edges to me. With some help from a fellow CP'ian, that is no longer the case. I now feel that support for code generation is up to the same level of quality as the rest of the toolkit. And I hope you find it useful. Thanks for your time. History
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||