【翻译】WF从入门到精通(第十四章):基于状态的工作流
上一篇:【翻译】WF从入门到精通(第十三章):打造自定义活动
学习完本章,你将掌握:
1.理解状态机的概念以及它怎样被模拟到工作流处理中的
2.创建基于状态的工作流
3.运用初始(initial)和终止(terminal)状态条件
4.使用代码进行状态的切换
在第四章“活动和工作流类型介绍”中,我阐述过你使用WF所能创建的工作流类型,在那里我提到过基于状态的工作流。基于状态的工作流模型被认为是有限自动机(finite state machine)。基于状态的工作流在工作流需要和外部事件进行许多交互的场合中大出风头。在事件触发并被工作流处理的时候,工作流能按要求进行状态的切换。
WF为创建基于状态的工作流提供了富余的开发体验,你迄今为止在本书中看到的许多东西都适用于基于状态的工作流。例如,当一个状态切换过来的时候,假如你想的话,你能去执行几个顺序活动,进行条件判定(使用规则或者代码),或者使用一个迭代活动结构来循环访问一些数据点。唯一真正的区别是活动怎样排队执行。在顺序或并行工作流中,它们以出现的顺序进行排队。但是在基于状态的工作流中,活动以状态切换进出来进行排队。事件通常驱动这些切换过程,但是这条规则不是通用的。让我们再看看状态机的概念并把这些概念和你能使用的WF活动结合起来去构建你的工作流。
状态机的概念
状态机的目的是构建你业务流程中的离散点,切换通过事件来控制。例如,把你的洗衣机接通电源,然后关门并按下启动按钮。按下启动按钮时初始化了一个状态机,它通过运行各种各样的清洁周期来清洗你待洗的衣物直到这些周期全部完成。
状态机有一个已知的起点和一个已知的终点。中间的状态应能通过预期事件的触发去进行控制,但机器总处于一个特定的状态。有时事件把状态机扔进无效的状态中,这种情形和在你的应用程序中维持未处理的异常的情形来说并没有什么不同,整个过程不是忽然停止就是完全崩溃。无论哪种情况,切换到无效状态都是要密切监视的,至少在数字电子系统(digital electronic systems)中是这样。
总的来说,第4章涵盖了涉及状态机的基本概念。可看看“状态活动”这一节快速复习一下。让我们从怎样设计活动转到在基于状态的工作流中怎样使用活动去吧。
使用状态活动
也许你不会太惊讶,在你的基于状态的工作流中State活动构建了一个状态。它是一个组合活动,但它局限于只接受特定类型的活动来作为它的子活动,它们是:EventDriven活动,StateInitialization活动,StateFinalization活动以及其它State活动。EventDriven活动等待(监听)那些将导致切换到另一个状态的事件,而在状态被切换进来和切换出去的时候,StateInitialization和StateFinalization是保证能分别去执行相应处理的活动。对于能拖拽第二个State活动到一个已存在的State活动中去可能看起来有些古怪,但其意图是提供一种在父状态机中嵌入子状态机的能力。
对于你的状态能容纳的那些有效活动的数目也有一个限制。只允许有唯一的一个StateInitialization和StateFinalization,你可以只有其中的一个,但每一个都不能超过一个。它们都不是必须的。
但是并没有说你不能只有一个或者更多的子EventDriven和State活动。事实上,一般都能找到多个EventDriven活动,因为每一个事件可能会导致切换到一个不同的状态。例如,一个“不批准(disapprove)”事件可能会切换到最终的状态(结束状态),而一个“批准(approve)”事件则可能切换到一个预定的状态并要求进行更多的审批。至于State活动,假如你要创建嵌入的基于状态的工作流的话,毫无疑问超过一个也应当是允许的。只有一个状态(切换)的基于状态的工作流构建成了一个简单的顺序工作流,因此在那种情况下你应当直接使用一个顺序工作流。在任何情况下,使用State活动只需从工具箱中拖拽它的一个实例到工作流视图设计器上,唯一的必要条件是工作流自身必须是基于状态的工作流而不是顺序工作流。然后确定你的状态活动应容纳些什么子活动,并按需要把它们拖拽进去,牢记你只能插入四种类型的活动。
使用SetState活动
假如你回忆一下我在第四章中介绍过的状态机示例的话,下图14-1将看起来很眼熟。确实,它是一个(被简化了的)自动售货机状态图。我认为把这样一个状态图制成一个真实的基于状态的WF工作流并使用一个用户界面来驱动它会是很有趣的一件事,考虑到我缺少艺术细胞,该用户界面会被构成成一个简陋的不含酒精饮料(“汽水”)的自动售货机。
图14-1 饮料机的状态图
考虑到没有用户互动,该饮料售货机应用程序的界面如图14-2所示。一瓶饮料的价格是$1.25。当你投入硬币的时候,左边的饮料图形按钮都处于非激活状态。但是,当你投入了足够的金额后,这些饮料按钮就能使用并且你可以做出选择。这个简化的模型不会处理如退款和更改之类的事情,但如果你愿意的话,你可随意修改该应用程序。
备注:为简便起见,我并没有使该示例应用程序国际化。它模拟的是只接受美国货币的自动售货机。但是,请记住这里的重点是工作流,而不是所使用的货币单位。
图14-2 饮料机处于初始状态时的用户界面
但是,你不能真正把硬币投到一个Windows Forms应用程序中,因此我提供了5¢,10¢和25¢三个按钮(注:符号¢代表美分)。很抱歉,只有这几种硬币。当你首次点击其中一个硬币按钮的时候,一个新的基于状态的工作流实例就被启动了,执行该工作流的状态图如图14-1所示。图14-3为你展示了饮料机在投入了几个硬币后的情况。基于状态的工作流随时跟踪接收到的硬币并把金额总计反馈给应用程序,该应用程序在一个模拟的液晶二极管显示屏上把它显示出来。
图14-3 投入了硬币的饮料机用户界面
当投入了足够的硬币时,工作流就通知应用程序现在用户可以选择饮料了,如图14-4所示。应用程序让位于用户界面左边的各个饮料按钮处于可用状态(enable)。
图14-4 允许选择饮料的饮料机用户界面
当点击了左边的某个饮料按钮后,即如图14-4中显示的变黑的按钮,一个标签(label)将呈现出来并显示“Soda!”,这是我模拟一瓶客户选中的饮料从机器中落出的一种方式。为重置整个过程,可点击“Reset”按钮。这不会影响到该工作流但是会重置用户界面上的按钮。图14-2显示了这种情形的用户界面,你可再一次启动所有的处理过程。
图14-5 选中了某瓶饮料后的饮料机用户界面
已经为你创建了大量的应用程序代码。假如你读完该SodaMachine示例的代码,你将发现我使用了CallExternalMethod活动(看看第8章中的“工作流数据传送”)以及HandleExternalEvent活动(看看第10章“事件活动”)。有大量的工具来为你的工作流和你的应用程序之间进行交互。剩下的工作就是创建该工作流自身,下面就是具体的做法。
创建一个基于状态的工作流
1.该SodaMachine应用程序再次为你提供了两个版本:完整版本和非完整版本。你需要下载本章源代码,打开SodaMachine文件夹中的解决方案。
2.当SodaMachine解决方案在Visual Studio中打开后,从Visual Studio的“生成”菜单中选择“生成解决方案”。解决方案中的项目包含各种各样的依赖,编译该解决方案生成了那些关联的项目所能引用的程序集。
3.在Visual Studio的解决方案资源管理器窗口中找到SodaFlow项目中的Workflow1.cs文件。然后在工作流视图设计器中打开该工作流准备编辑。
备注:我已经创建了这个基本的工作流项目,因为该应用使用的CallExternalMethod和HandleExternalEvent活动的相关技术你在第8章和第10章中已经看过。重复这些必须的步骤来创建这些常规活动没有任何必要,但是假如你从头开始创建工作流项目的话,你需要去做这些工作。
4.该工作流现由唯一的一个State活动组成。选中该stateActivity1活动,把它重命名为“StartState”。
5.当创建工作流时,Visual Studio会为你自动添加一个原始的State活动。但它也把这个活动作为起始(开始)活动。当你在前一步骤中重命名该活动后,工作流就会丢失这个行为。为把这个活动重新设置为起始活动,需要在工作流视图设计器的界面中点击除了State活动之外的其它任何地方,以便激活整个工作流的属性。在属性面板中,你会看到一个InitialStateName属性,把它从stateActivity1改为StartState。注意你可把这个值直接输入到该属性中也可从下拉列表框中选择StartState。
6.我们现在要把剩下的State活动拖拽到工作流视图设计器的界面上。和你记得的一样,当SetState工作的时候,可方便地指定目标状态。从Visual Studio的工具箱中拖拽一个State活动到设计器的界面上并把它放到StartState活动的旁边。把它的名称改为WaitCoinsState。
7.拖拽另一个State活动到工作流视图设计器的界面上,把它的名称改为WaitSelectionState。
8.拖拽最后的一个State活动到工作流视图设计器的界面上,把它的名称改为EndState。
9.就像你要重新指定开始状态一样,你也需要告知WF结束状态是什么。点击任何State活动外面的工作流视图设计器界面来激活该工作流的属性。指定CompletedStateName属性为EndState。然后Visual Studio会清空EndState的内容并改变它左上角的图标。和前面一样,你可直接输入EndState也可从下拉列表框中选择它。
10.放好了这些状态活动,我们现在就来添加些细节。从StartState开始,从工具箱中拖拽一个StateInitialization活动并把它放到StartState中。
11.双击你刚刚添加的这个stateInitialization1活动,这将进入到顺序工作流编辑器中。
12.从工具箱中拖拽一个Code活动到该StateInitialization活动中。指定它的ExecuteCode方法为ResetTotal。然后Visual Studio会为你添加对应的ResetTotal方法并为你切换到代码编辑视图下。此时我们不准备添加代码,还是回到工作流视图设计器上来吧。
13.接下来拖拽一个SetState活动到设计器界面上,把它放到你刚刚添加的Code活动的下面。
14.指定该SetState的TargetStateName属性为WaitCoinsState。
15.回到工作流视图设计器的状态编辑器视图中,点击Workflow1左上角的超链接风格的按钮。
状态编辑器现在会显示出StartState向WaitCoinsState的转变。
16.StartState现在就完成了。下一步我们将转到WaitCoinsState。首先拖拽一个EventDriven活动到设计器的界面上并把它放到WaitCoinsState中。在Visual Studio的属性面板中把它的Name属性修改为CoinInserted。
17.双击CoinInserted EventDriven活动使顺序工作流编辑器呈现出来。
18.现在从工具箱中拖拽一个CoinInserted自定义活动到该EventDriven活动的表面上。注意,假如你还没有编译整个解决方案的话,该CoinInserted事件是不会在工具箱中显示出来的。假如你漏过了第2步,你可能必须移除该EventDriven活动以便成功地进行编译。
19.在工作流视图设计器中选中该ExternalEventHandler coinInserted1活动,在属性面板中点击CoinValue属性以便激活浏览(...)按钮,然后点击该浏览按钮。这将打开“将‘CoinValue’绑定到活动的属性”对话框。点击“绑定到新成员”选项卡,在“新成员名称”中输入LastCoinDropped。此时选中的应该是“创建属性”,假如不是的话选中它,以便你创建的是一个新的依赖属性。然后点击“确定”。
20.现在我们需要做一个判断:用户刚刚投入了足够的金钱来使那些饮料按钮处于可用(enable)状态吗?为此,拖拽一个IfElse活动到工作流视图设计器界面上,把它放到CoinInserted EventDriven活动中,它的位置在coinInserted1的下面。
21.选中ifElseActivity1的左边分支,以便在属性面板中显示它的属性。对于它的Conditon属性,选择“代码条件”。然后展开Condition节点,然后在Condition子属性中输入TestTotal。在Visual Studio添加一个新的方法并为你切换到代码编辑视图后,重新返回到工作流视图设计器上来。
22.TestTotal将检测你最终投入到饮料机中的金额总计。(我们将在添加代码前完成该工作流在工作流视图设计器中的设计工作,因为有一些我们需要的属性还没有创建。)假如投入了足够的金钱,我们就需要转换到WaitSelectionState。因此,拖拽一个SetState到该IfElse活动(ifElseBranchActivity1)的左边分支上,指定它的TargetStateName为WaitSelectionState。
23.假如TestTotal判定了没有足够的金额来买饮料,该工作流需要传达当前投入到饮料机中的钱的总计。为此,从工具箱中拖拽一个UpdateTotal并把它放到该IfElse活动的右边分支中。UpdateTotal是我为本任务创建的一个自定义的CallExternalMethod活动。
24.UpdateTotal需要一个要去通信的总计值,因此选中它的total属性并点击浏览(...)按钮,这将再次打开一个绑定对话框。当绑定对话框打开后,选择“绑定到新成员”选项卡并在“新成员名称”中输入Total并确认选中的是“创建属性”选项。然后点击“确定”。
25.点击左上角的超文本风格的Workflow1按钮回到状态设计器视图。拖拽一个StateFinalization到工作流视图设计器的界面上,把它放到WaitCoinsState中。
26.双击你刚刚添加的stateFinalizationActivity1活动重新激活顺序设计器视图。
27。从工具箱中拖拽一个ReadyToDispense并把它放到stateFinalizationActivity1中。ReadyToDispense也是一个自定义的CallExternalMethod活动。
28.你刚刚添加的ReadyToDispense1活动将把最终的总计值返回给主应用程序。为做这些,它需要访问你在第14步中添加的Total属性。看看readyToDispense1的属性,点击finalTotal属性,然后点击在finalTotal中激活的浏览(...)按钮。点击浏览按钮打开绑定对话框,但是这次是“绑定到现有成员”。从列表中选择Total属性然后点击“确定”。
29.点击超文本风格的Workflow1按钮回到状态设计器视图上来。这里,从工具箱中选择EventDriven活动并把它放到设计器界面上的WaitSelectionState活动中。把它命名为ItemSelected。
30.双击ItemSelected EventDriven活动进入顺序设计器视图。
31.拖拽一个自定义ExternalEventHandler的活动ItemSelected,把它放进ItemSelected EventDriven活动中。
32.用户挑选了饮料后,主应用程序触发该ItemSelected事件。当该事件发生的时候,我们需要切换到EndState。为此,我们需要添加SetState活动。因此从工具箱中拖拽一个SetState并把它放到ItemSelected EventDriven活动中的itemSelected1的下面。指定它的TargetStateName为EndState。
33.点击超文本风格按钮Workflow1回到状态设计器视图上来。
34.从工作流视图设计器的角度来看,该工作流就完成了,但我们还要写一些代码。在Visual Studio的解决方案管理器中选择Workflow1.cs文件,然后在代码编辑模式下打开该文件准备进行编辑。
35.查看Workflow1.cs源文件,找到你在第12步所添加的ResetTotal方法。把下面的代码插入到ResetTotal方法中:
// Start with no total.
Total = 0.0m;
36.最后,找到你在第21步所添加的TestTotal方法。为该方法添加下面这些代码:
// Add the last coin dropped to the total and check
// to see if the total exceeds 1.25.
Total += LastCoinDropped;
e.Result = Total >= 1.25m;
37.编译整个解决方案。修正任何可能出现的编译错误。
现在你可按下F5或Ctrl+F5运行该应用程序。点击一个投币按钮,LCD上显示的总金额更新了吗?当你投入了足够的金钱时,你能挑选饮料吗?
备注:假如该应用程序由于InvalidOperationException异常崩溃的话,最可能的情况是由于引用在解决方案第一次完成编译后没有被完全更新。可简单地重新编译整个应用程序(重复第37步)并再次运行该应用程序,它应该能干净利落地运行。
本章源代码:里面包含本章的练习项目和完整代码
下一篇:WF从入门到精通(第十五章):工作流和事务