65.9K
CodeProject 正在变化。 阅读更多。
Home

A .NET 状态机工具包 - 第三部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (40投票s)

2005年9月21日

CPOL

11分钟阅读

viewsIcon

253019

downloadIcon

1252

使用 .NET 状态机工具包的代码生成。

目录

引言

这是我的 .NET 状态机工具包系列文章的第三部分,也是最后一部分。在 第一部分 中,我介绍了构成该工具包的类,并演示了如何创建一个简单的扁平状态机。在 第二部分 中,我讨论了该工具包的一些高级功能,并演示了如何创建分层状态机。在这一部分,我们将探讨如何使用代码生成来创建状态机。

注意:根据反馈,自 Ramo Smits 最初提交我的文章以来,我对该工具包的 XML 支持进行了极大改进。它现在直接使用 XML 序列化,而不是依赖 DataSet 来读写状态机作为 XML 数据。此外,XML 架构已大大简化。非常感谢 Ramon 提出的宝贵建议并提供代码来演示他的想法。

顶部

StateMachineBuilder 类

代码生成是通过 StateMachineBuilder 类实现的。该类遵循 Builder 设计模式。使用这种设计模式,通过一个 builder 对象分步构造对象。在完成所有必要的步骤后,指示 builder 构建对象,通常通过调用 Build 方法。之后,就可以检索和使用构建好的对象了。这种模式有助于将对象复杂的构造分解为离散的步骤。它还使您能够使用相同的构造过程反复获得不同的表示形式。使用 StateMachineBuilder 类,您可以构建一个 CodeDom CodeNamespace 对象。该命名空间包含一个表示状态机的抽象类。该类作为您将要编写的类的基类。

顶部

递归状态机表

最初,StateMachineBuilder 类使用 ADO.NET 中的类来表示状态机数据。简单的 DataTable 代表状态、事件、守卫等。更复杂的 DataTable 代表状态转换、子状态/父状态关系等。简单表与复杂表之间建立了一对多关系。基本上,它是一个用于表示状态机的内存关系数据库。很巧妙,或者我当时是这么认为的……

这种方法存在问题。主要问题是,我无法仅通过数据约束来强制执行声明分层状态机的所有规则。有可能在表中输入非法的数值组合。例如,您可以声明一个状态是某个状态的子状态,同时也是该状态的父状态。由于一个状态不能既是另一个状态的子状态,又是同一状态的父状态,因此这是荒谬的。我试图让关系数据库完成编译器的任务,但它不起作用。此外,DataSet 生成的 XML 非常冗长。需要一种更好的方法。

StateMachineBuilder 类现在使用四个自定义类来跟踪状态、事件、守卫、操作、转换等,而不是使用大量 DataTable 来创建关系数据库。这些类是

  • StateRow
  • StateRowCollection
  • TransitionRow
  • TransitionRowCollection

StateRow 类代表单个状态。StateRowCollection 类代表 StateRow 对象的集合。您可以将 StateRowCollection 视为一个状态表。TransitionRowTransitionRowCollection 类共同表示状态的转换。

我们先来看 StateRow 类。它具有以下属性

  • 名称
  • InitialState
  • HistoryType
  • Substates
  • 过渡

Name 属性是状态的名称。InitialState 属性是状态的初始状态。如果状态没有任何子状态,则在构建状态机时会忽略此属性。HistoryType 属性是状态的历史类型,显而易见。如果状态没有任何子状态,此属性也会被忽略。这三个属性可以被视为状态表中的三列。

Substates 属性很有趣。它代表一个 StateRowCollection 对象。因此,如果我们认为 StateRow 的集合属于一个表,则此属性是一种表中的表。可以向 Substates 属性添加 StateRow,反过来,这些 StateRow 也可以在其 Substates 属性中添加 StateRow,依此类推。这就形成了一个树状结构,其中有一个顶层状态,即没有父状态的状态,以及从它们下降的分支代表它们的子状态。

Transitions 属性代表一个 TransitionRowCollection 对象。状态的转换被添加到此属性中。因此,每个 StateRow 都包含一个其转换的表。

这些类都有 XML 序列化属性,描述了它们应如何作为 XML 数据进行序列化。此外,StateRowCollection 类和 TransitionRowCollection 类可以绑定到 DataGrid 等控件。这使得使用 StateMachineBuilder 类轻松创建状态机 GUI 前端变得容易。

StateMachineBuilder 类有一个 States 属性,代表一个 StateRowCollection 对象。它是状态层次结构的根。一旦所有状态及其转换都已添加到 StateMachineBuilder 中,就可以构建状态机基类。如前所述,结果是一个 CodeDom CodeNamespace 对象。

顶部

生成代码

让我们看一些使用 StateMachineBuilder 类构建 交通灯 状态机的代码,该状态机在 第二部分 中进行了描述,并显示结果

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();
            }
        }
    }
}

生成的代码如下

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,
        }
    }
}

是的,代码又丑又冗长。这部分是由于 CodeDom 使用了完全限定名。但是,这是您永远不必触摸或查看的代码。生成的类是从中派生您自己的状态机类的基类。这种方法的优点是,如果您需要更改状态机,例如添加一个事件,您可以重新生成代码,而您的派生类不会受到影响,只有基类会重新生成。根据您所做的更改,您可能需要对派生类进行一些小的调整,但您的实现不会被覆盖。

进入和退出方法被设为虚拟方法,带有空实现。这实际上使它们成为可选的。在派生类中,如果您需要为进入和/或退出操作添加行为,您可以覆盖所需的方法并实现行为。然而,守卫和操作方法是抽象的。您必须覆盖它们。

这是新的 TrafficLight 类。它派生自 StateMachineBuilder 生成的 TrafficLightBase

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);
        }
    }
}

将此版本与 第二部分 中的版本进行比较。所有用于创建和初始化 State 对象及其 Transition 的代码都隐藏在基类中。

顶部

XML 中的分层状态机

StateMachineBuilder 类可以作为 XML 数据进行序列化。这允许您保存状态机值并在以后检索它们。在查看交通灯状态机的 XML 表示形式之前,让我们看一下该工具包用于表示分层状态机的 XML 结构。我们将检查每个元素及其属性。

根元素是 stateMachine,它有三个属性

  • 命名空间
  • 名称
  • initialState

namespace 属性是状态机类所在的命名空间。name 属性是状态机的名称。initialState 属性是状态机的初始状态。initialState 属性的值必须是顶层状态之一。顶层状态是没有父状态的状态;它存在于状态层次结构的顶部。毫不奇怪,状态由 state 元素表示。它有三个属性

  • 名称
  • initialState
  • historyType

name 属性是状态的名称。initialState 属性是状态的初始状态;如果状态有任何子状态,initialState 属性代表进入该状态后要进入的子状态。historyType 属性代表状态的历史类型。它可以是三个值之一:NoneShallowDeep。如果状态没有子状态,则忽略 initialStatehistoryType 属性。否则,initialState 属性是必需的。historyType 属性是可选的,如果不存在,状态将默认为历史类型值 None

状态可以嵌套在其他状态内。嵌套状态是包含它的状态的子状态,而它本身也可以有嵌套状态。因此,子状态/父状态关系直接在 XML 状态机结构中表示。

状态转换由 transition 元素表示。转换嵌套在它们所属的状态内。transition 元素有四个属性

  • 事件
  • guard
  • action
  • target

event 属性表示触发转换的事件。guard 属性表示为确定转换是否应实际发生而进行评估的守卫。action 属性是在转换发生时应执行的操作。target 属性是转换的目标状态。除 event 属性外,所有属性都是可选的。所有转换都必须存在 event 属性。

要序列化状态机,首先需要像上面使用交通灯状态机一样用 StateMachineBuilder 构建它。然后使用 XmlSerializer 类序列化 builder

// ...




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();

// ...

在这里,我们将 StateMachineBuilder 序列化到一个 StringWriter 对象,以便我们可以将生成的 XML 显示到控制台。这是序列化交通灯状态机的结果

<?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>

正如您所见,XML 架构非常直接和简单,您甚至可以手动声明一个状态机。

顶部

状态机生成器

演示项目附带了一个程序,该程序提供了一个 nice GUI 用于使用 StateMachineBuilder 类。它易于使用。只需将值输入 DataGrid,构建状态机,然后将结果保存为 C# 或 VB 代码。如果您想保存状态机值以便以后进行编辑,可以将数据保存为 XML 文件。我将在下面解释如何使用状态机生成器应用程序。

状态机生成器有三个文本框,分别用于设置状态机的命名空间、名称和初始状态。需要注意的重要一点是,如果您忘记输入初始状态,将会收到错误。每个状态机在首次运行时都必须有一个初始状态。

此外,还有一个 DataGrid 控件,您可以在其中添加状态及其转换。DataGrid 绑定到 StateMachineBuilderStates 属性,因此对 DataGrid 的输入会自动添加到 StateMachineBuilder。最初,您将输入状态机的顶层状态;这些是没有父状态的状态。

输入顶层状态后,可以通过展开其行并单击 Substates 链接来添加其子状态

在那里,您将被带到其子状态表

添加子状态后,可以通过单击导航箭头返回到 State 表

状态的转换的添加方式相同,只是您单击 Transitions 链接。这会将您带到状态的 Transition 表

添加完所有状态及其转换后,您可以构建状态机。如果构建失败,将显示错误消息。例如,假设您忘记输入状态名称

如果构建成功,您将收到一条通知消息

构建完成后,您可以将结果保存为 C# 或 VB 代码

当您将结果保存为代码时,它将保存上一次构建的结果,而不是上一次编辑的结果。换句话说,在将状态机保存为代码之前,请务必记住立即构建状态机。您可能在构建后更改了状态机,并在保存为代码时忘记了这一点,然后会想为什么您最后的编辑没有显示出来。

顶部

依赖项

请务必阅读 第一部分 中的 依赖项 部分。

顶部

结论

好了,这篇系列文章到此结束。有了工具包的第二个版本,我整体上对此感到满意。虽然引擎是我努力工作并感到满意的部分,但代码生成过程的某些方面对我来说仍然显得粗糙。在一位 CP 成员的帮助下,现在情况已经不同了。我现在觉得代码生成支持的质量与工具包的其余部分相当。我希望您觉得它很有用。感谢您的时间。

顶部

历史

  • 2005年9月21日 - 完成第一个版本。
  • 2005年10月5日 - 完成第二个版本。文章大幅重写,并重新设计了 StateMachineBuilder 类。
  • 2005年10月25日 - 完成第三个版本。文章修订。
  • 2006年5月15日 - 完成 4.1 版本。文章修订。
  • 2006年10月21日 - 完成 5.0 版本。文章修订。

顶部

© . All rights reserved.