学习完本章,你将掌握:
1.知道怎样使用Sequence活动
2.知道怎样使用Code活动
3.知道在工作流中怎样抛出异常并对其进行处理
4.知道如何在代码中暂停和终止你的工作流实例
在本章,我们将正式引入前面已经看到过的一组活动:Sequence活动和Code活动。但我相信,适当的错误处理对于精心设计和运行良好的软件是至关重要的,所以我们将会研究如何使用工作流中的活动抛出异常、捕获异常、甚至暂停和终止你的工作流。我们就从Sequence活动开始吧。
使用顺序活动对象
实际上,说我们已见过Sequence活动并不完全正确。我们创建工作流应用程序时实际上使用的是SequentialWorkflow活动,但大体的意思是一样的:这个活动包含其它依次要执行的活动。这一点可和使用parallel活动的并行执行相对比,在第11章(“Parallel活动”)中我们将看到parallel活动。
当你以特定的顺序执行任务时,你必须依次完成这些任务,这点通常是必须的。
Sequence活动是一个组合活动,我们在第四章(“活动和工作流类型介绍”)中已经简要讨论过。它包含其它活动,这些活动一定要按次序执行。你可在父Sequence活动内放入包含parallel活动在内的其它组合活动。但子活动要依次地,一个接一个地执行,即使这些子活动本身包含的并行执行流也如此。
我们就来使用Sequence活动创建一个简单的工作流。我们将再次使用Code活动,关于它的更详细的细节将在下一节“使用Code活动”进行讨论。为对特定的工作流活动的行为进行了解,我们将回到基于控制台的应用程序中。对于基于控制台的应用程序,通常你需要书写的代码更少,因为你不用对用户界面进行处理。(但随着本书的进展,我们也会创建其它的图形化的测试案例。)
创建一个使用了Sequence活动的工作流
1.下载本章的源代码,本例的最终版本在“Sequencer Completed”目录下,可使用Visual Studio 2008打开并直接查看它的运行结果。“Sequencer”目录下则为练习版本,我们将从该版本开始本例的学习,首先使用Visual Studio 2008打开该解决方案。
2.在我们的解决方案中添加一个顺序工作流库的项目,项目名称为“SequencerFlow”。
3.从工具箱中拖拽一个Sequence活动到Visual Studio的工作流视图设计器上。
4.然后,从工具箱中拖拽一个Code活动到我们刚添加的Sequence活动中。
5.在该活动的ExecuteCode属性中输入“DoTaskOne”,然后按下回车键。
6.Visual Studio会自动把我们带到代码编辑状态。定位到Visual Studio刚刚添加的DoTaskOne方法,然后再该方法内输入下面的代码:
Console.WriteLine("Executing Task One...");
7.反复执行步骤4、5、6两次,添加方法“DoTaskTwo”和“DoTaskThree”,然后在这些方法中修改Console.WriteLine输出的内容(“One”依次改为“Two”、“Three”)。该工作流的视图设计器现如下图所示:
8.回到主应用程序,打开Program.cs文件,定位到Main方法上。在该方法中,找到下面的代码:
Console.WriteLine("Waiting for workflow completion.");
9.在你找到的这行代码下添加下面的代码:
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof(SequencerFlow.Workflow1));
instance.Start();
10.当然,我们需要在主应用程序项目中引用该SequencerFlow工作流库。
11.编译该应用程序,纠正任何出现的错误。按下F5或Ctrl+F5运行该应用程序。设置一个断点或从命令提示符下运行该程序,这样你就能看到输出结果,结果如下:
正如你从步骤11看到的,该任务和我们所期望的顺序依次被执行。需记住两方面:Sequence活动是一个组合活动(其它活动的容器),其次就是它容纳的活动以顺序依次执行的。
使用Code活动
迄今为止,在本书中我们经常使用的另一个活动是Code活动。Code活动就是要让你的工作流执行你所提供的自定义代码。在下一章我们将看到,还有一种方法可调用外部方法。
当你把一个Code活动放入你的工作流中时,它的ExecuteCode属性会被设置为工作流运行时将调用的方法的名称。
实际上,当你在刚刚完成的Sequencer应用程序设置ExecuteCode属性时,假如你仔细看看Visual Studio为你插入的代码,它虽不是一个被调用的方法,但也差不多,它其实是一个事件处理,下面是我们插入代码后的DoTaskOne方法:
private void DoTaskOne(object sender, EventArgs e)
{
Console.WriteLine("Executing Task One");
}
正如你看到的,当工作流运行时在执行你的Code活动时,它会触发一个事件,该事件的名称就是你在ExecuteCode属性中设置的值。我们将在本书的剩余部分中好好利用这个Code活动。
使用Throw活动
在本书中很早以前就提到过该活动,但我还没有真正加深这个基本的工作流处理模型的概念。因此,我们需能对真实世界中的各种各样的情况进行建模,这其中就包含我们需要抛出一个异常的情况。假设有些事情在前进的道路上并不平坦,我们的软件并不能为防止抛出异常而处理任何其它任何情况。假如我们乐意的话,我们可使用C#中的throw关键字来抛出一个异常,但在工作流中,我们使用一个特别的活动来做这些事,并使用一个特别的活动来处理这些异常,这些我们将在下节看到。假如我们使用C#中的throw关键字的话,工作流运行时会“swallows(淹没)”该异常,并不会给出通知信息。
这种现象的原因是Throw活动。当工作流运行时遭遇Throw活动时,假如没有相关的失败处理操作,工作流运行时将触发WorkflowTerminated事件。但请记住,届时,工作流实例会被终止,工作流运行时会被停止。在这时任何更正异常状况的任何尝试都已为时已晚,我们仅能做的是重启工作流并开始一个新的工作流实例。假如我们想在终止前更早地处理异常,我们需要使用Throw和FaultHandler活动组合。
备注:推荐的练习使用了Throw和FaultHandler组合而不是单一的Throw。使用Throw活动本身等同于在传统应用程序代码中使用没有进行异常处理的C#的throw关键字。在本节,我们将单独使用Throw来看看会发生什么。在下一节,我们将使用Throw和FaultHandler活动组合来看看他们怎样协同工作。
回到我们关注的Throw活动,当你拖拽一个Throw活动到设计器上后,你可找到两个需进行设置的属性。首先是FaultType属性,该属性告知Throw活动将抛出什么类型的异常;另一个是Fault属性,假如此时Throw活动抛出的异常不为空,它就指示该Throw活动引发的异常对象。
FaultType属性不必做大量的解释,它简单地告知工作流实例将抛出的异常类型。我们没有指明的异常由工作流运行时进行处理或者被忽略(假如你想处理的话,也可在以后进行处理)。
但Fault属性的背后有什么密码呢?假如设置了该属性的话,它才真正会是Throw活动所使用异常。假如该属性为空的话,Throw活动仍旧抛出一个FaultType指定的类型的异常,但它是一个新的异常,该异常没有既定的消息(记住,Message属性为我们提供了一些除它本身的异常类型之外的关于错误的一些描述)。
假如你想让Throw活动抛出一个带有详细Message的异常,你需要使用new操作符创建该异常的一个实例并把它指定到你绑定的Throw活动的相同属性上。
我再以略微不同的语言来表达上述这些。Throw活动,更具体地说,它的Fault属性会和你的工作流中所选择的活动(包括root活动)中的具有同一种异常类型的一个属性绑定到一起。就是说,假如你有一个抛出类型异常为NullReferenceException的Throw活动,你就必须在你的工作流中的一些活动上提供一个类型为NullReferenceException的属性以让Throw活动使用。然后让Throw活动绑定这些活动的属性,以便它能使用你用new操作符产生的同一个异常。
在这里,我们会写一些代码来进行试验。我们就开始创建一个使用了Throw活动的小工作流来看看它是怎样工作的。
创建一个使用了Throw活动的工作流
1.在下载的本章的源代码内有两个名称为“ErrorThrower Completed”和“ErrorThrower”的文件夹,“ErrorThrower Completed”文件夹内为本例的完整代码,“ErrorThrower”文件夹内为本例的练习项目。我们现在就使用Visual Studio打开“ErrorThrower”文件夹内的项目。
2.打开ErrorThrower后,向该解决方案添加一个顺序工作流库的项目,名称为ErrorFlow。
3.从工具箱中拖拽一个Code活动到Visual Studio工作流视图设计器上,设置该Code活动的ExecuteCode属性的值为“PreThrow”。
4.然后,从工具箱中拖拽一个Throw活动到设计器上,位置在上一步添加的Code活动的下面。
5.在Throw活动的属性面板上,选中它的FaultType属性,然后点击浏览(...)按钮。(以三个点作为text的按钮通常表示浏览。)
6.这将出现一个“浏览并选择.NET类型”对话框。在里面,选择Throw活动将构建的异常类型。我们输入或选择“System.Exception”,然后点击确定。
7.选中Throw活动的Fault属性,然后点击它浏览(...)按钮。
备注:未设置Fault属性,甚至未设置FaultType属性都不会导致编译失败。但是,这将设置一个message内容为“Fault属性未设置”的System.Exception类型的异常。
8.这将弹出“将Fault绑定到活动的属性”对话框。因为我们还未添加fault代码,因此我们点击“绑定到新成员”选项卡,然后在“新成员名称”中输入WorkflowException,最后点击确定。这就为你的root活动添加了WorkflowException属性。
9.添加第二个Code活动,设置它的ExecuteCode属性的值为“PostThrow”。此时的视图设计器界面如下:
10.现在我们的工作流就创建好了,然后将添加相应的代码。查看Workflow1.cs的代码,找到前面步骤添加的PreThrow事件处理程序,添加下面的代码:
Console.WriteLine("Pre-throwing the exception");
WorkflowException = new Exception(
"This exception thrown for test and evaluation purposes");
11.同样,找到PostThrow事件处理程序并添加下面的代码:
Console.WriteLine("Post-throwing the exception (You won't see this output!)");
12.工作流现在就设计完成了,我们现在回到主应用程序继续工作。打开Program.cs文件,定位到Main方法。在Main方法内,找到下面的代码:
// Print banner.
Console.WriteLine("Waiting for workflow completion.");
13.在上面的代码下,添加下面的代码:
WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(ErrorFlow.Workflow1));
instance.Start();
14.现在,为主应用程序添加对ErrorFlow工作流库的项目引用。
15.编译应用程序,纠正任何编译错误。按下F5或Ctrl+F5运行该应用程序。你将看到下面的结果:
假如你仔细看看输出结果,你将看到WorkflowTermination事件处理会被调用,并为我们显示了终止的原因:有一个异常。该异常的Message和我们在步骤10中添加的类型为Exception的WorkflowException异常的相关信息相匹配。
备注:当你像在步骤8中一样添加新的属性时,这些由Visual Studio插入的属性就是依赖属性(看看第四章)。
现在我们看了在WF中异常是怎样构建的,我们又怎么捕获处理它们呢?毕竟,在工作流的终止事件处理程序中处理异常的话通常都太迟,对于我们没有任何价值。还好,WF为我们提供了FaultHandler活动,我们现在就来看看。
使用FaultHandler活动
使用FaultHandler的方式和我们目前为止看到过的其它任何活动的使用方式有细小的差别。其实,我们更应该仔细地去看看视图设计器。为什么呢?因为相比其它的工作流活动,错误处理有一个单独的设计界面(其实,还有第三个设计界面,它为取消处理服务,我们也将在此看到)。
备注:在第15章(“工作流和事务”)中,我们将看到补偿活动,它包含补偿事务。处理错误就是其中的一部分。补偿的意思是,产生一个动作,以减轻异常可能带来的危害。
快速浏览工作流视图设计器
在此时,假如你创建过我给出的工作流实例的话,你可能会对拖拽活动到工作流视图设计器上,然后设置它们的属性,然后编译并执行基本工作流的代码的这一系列的工作方式感到满意。然而,我还有一些东西没有告诉你,我保留它们的原因是因为我们此前的焦点是放在工作流程序的编写和执行上。
但现在,你已经使用过工作流中的设计工具,体会过工作流的编写,Visual Studio的使用。我们就花点时间来看看Visual Studio提供的在工作流辅助设计方面的其它东西,这主要有两个,简要的说就是:附加的视图设计器界面和调试。
附加的视图设计器界面
假如你回头看看一至六章,你会看到那些我们从工具箱拖拽活动到Visual Studio为我们呈现的视图设计器界面上的例子。但你注意到视图设计器窗口右键快捷菜单中的“查看工作流”、“查看取消处理程序”、“查看错误处理程序”三个菜单项没有?如下图所示:
图7-1 视图设计器界面右键快捷菜单
“查看工作流”菜单激活目前本书中我们已经使用过的默认的工作流视图编辑界面。“查看取消处理程序”激活到另一个工作流取消视图,我们可在其中为“取消”处理程序书写代码(见图7-2)。“查看错误处理程序”激活到工作流异常视图(见图7-3)。
图7-2 工作流取消视图设计器界面
图7-3 工作流异常视图设计器界面
你随时可能需要通过一些活动名称下面的智能标记来访问附加设计界面,如图7-4。但有些活动,像EventHandlingScope活动(第十章),你还可访问到更多的界面。
图7-4通过智能标记进行界面的切换
没有什么奇怪的,你拖拽到取消设计界面上的工作流活动在该工作流实例被取消时执行。这使你有机会在工作流实例真正停止执行前去执行一些清理或通知的任务。
错误处理设计界面可容纳许多的错误处理。每一个错误处理可处理一种,也仅能处理一种异常类型。组合活动一般而言都包含错误处理,假如需要的话也允许子活动进行处理错误而不用把它们发送到父活动中。回头看看图7-3,你可看到外面围着蓝色圆圈的两个箭头按钮。错误处理活动可拖到这两个箭头之间,这些箭头允许你进行滚动以显示屏幕之外的错误处理活动。箭头按钮下面的区域是另一个和活动相关联的异常处理的工作流设计界面。通常是拖拽一个Code活动到这里为你做些在错误情况下的清理工作或执行其它必要的处理。在更多地了解工作流的调试视图设计界面后我们将对此做一些练习。
调试视图设计器
你或许不知道你可在工作流的视图设计器上设置断点。其实,在你的工作流中你还能一个活动一个活动地单步执行(而不是在你的源代码中一行一行地执行)。
在工作流视图设计器上设置断点的方法是,右键单击要设置断点的活动,然后在快捷菜单中依次选择“断点”、“插入断点”,参见下图:
图7-5 使用工作流视图设计器设置断点
然后工作流视图设计器会在该活动的图形界面中放置一个红色圆球,就像你在代码中某一行上设置断点进行调试时看到的红色圆球一样,参见7-6。你也能移除断点,禁用所有断点等等。
图7-6 带断点的活动
有了这些知识储备,我们现在就可向我们的工作流添加FaultHandler活动了。
修改我们的工作流,以便使用FaultHandler活动
1.在Visual Studio中打开ErrorThrower应用程序,选择ErrorFlow项目,选中Workflow1.cs文件,点击视图设计器,激活到工作流视图设计器界面上,我们需要修改些东西。
2.通过右键快捷菜单(“查看错误处理程序”)切换到工作流异常视图设计器界面,如下图:
3.从工具箱中选中FaultHandler活动,拖拽到工作流设计器的异常处理界面上,然后放到两个蓝色按钮之间。现在你的视图设计器显示效果如下所示:
4.我们需设置一些属性,以使错误处理全面投入运作。我们首先要设置的是FaultType属性。在Visual Studio的属性面板中选中FaultType属性,点击浏览按钮(该按钮是三个点)激活“浏览并选择.NET类型”对话框。如下图:
5.在“浏览并选择.NET类型”对话框打开后,选择或输入“System.Exception”类型名称,点击确定关闭对话框,你会发现FaultType属性的值已被设置为“System.Exception”了。
备注:我们设置FaultHandler活动使用的异常类型和我们本章前面使用Throw活动时设置的异常类型是相同的,但这不是巧合。它们的设置要匹配。假如在你的工作流中没有为一个Throw活动进行相应的异常处理,那请牢记,假如运行时抛出异常,你的工作流将很快就执行到WorkflowTerminated事件。假如你不想这样,就应添加恰当(属性)的FaultHandler活动。
备注:尽管在前面的图片中我们看到了Fault属性,但它实际上是禁用的,因此不能设置,我们忽略它。
6.迄今为止,我们添加了FaultHandler活动,并设置了它将处理的异常类型,但我们其实还未写入任何代码来处理可能抛出的异常。因此,我们要从工具箱中拖拽一个Code到我们添加的FaultHandler活动的下方区域中,该区域以“faultHandlerActivity1”命名,它看起来就像是一个微型的工作流视图设计器。我们拖入的Code活动的ExecuteCode属性设置为“OnException”,然后按下回车键。
7.然后Visual Studio将切换到代码视图中,找到Visual Studio刚添加的OnException事件处理并为其添加以下代码:
Console.WriteLine("Exception handled within the workflow! The exception was: '{0}'",
WorkflowException != null ? WorkflowException.Message :
"Exception property not set, generic exception thrown");
8.现在编译并执行代码,你将看到下面的执行结果:
备注:在这种层次上抛出和处理异常,你的工作流实质上仍旧会被停止。这样做的优点是你的工作流能带着异常工作而不是把异常抛出给工作流运行时处理。假如你想在特定的异常抛出后还能继续进行处理,你就不要用Throw活动和FaultHandler活动来处理它们。相反,你要使用try/catch来包围活动代码,以便异常绝不会传给运行时处置。假如你不能充分地在(try/catch)内部处理异常,那只能求助于Throw活动和FaultHandler活动。
使用Suspend活动
另一个在特定条件下会对你有用的活动是Suspend活动。事实上,它常见的使用场景是使用FaultHandler活动处理错误后,再使用Suspend活动进行暂停,然后发送需要人为干预的信号。
使用Suspend活动时,你需要为该活动的Error属性提供一个字符串。这个属性可以绑定在一个依赖属性上(这一点像Throw活动)、一个类的属性或字段、甚至是一个文本字符串(本例中我们将这样做)。当Suspend活动执行时,工作流运行时会触发WorkflowSuspended事件,传给该事件的argument参数中将带有该error字符串。
使一个工作流实例处于暂停状态的含义是,该实例当前不再执行,但它也不被卸载。本质上它维持这种形式,等待一些动作。它也不被认为是空闲状态,因此自动的持久化对它也不起作用。使用Suspend活动相对简单,下面你就将看到。
备注:在你的基于工作流的应用程序中处理WorkflowSuspended事件是一个好主意,这使工作流实例进入暂停状态后为你提供一个动作。至少你能得到工作流实例已经被暂停了的通知,你可移除、恢复或者重新启动这些工作流实例。
修改我们的工作流,以便使用Suspend活动
1.下载本章源代码,在Visual Studio中打开ErrorSuspender应用程序,选中ErrorFlow项目中的Workflow.cs文件,打开该文件的工作流视图设计器界面。选择工作流异常的设计界面,我们将为System.Exception错误处理程序添加Suspend活动。
2.从工具箱中拖拽一个Suspend活动到错误处理程序界面上,并把它放到Code活动的下面,如下图所示:
3.设置该Suspend活动的Error属性为“This is an example suspension error...”
提示:我们在这里输入的是一个文本字符串,但是,你也能把它绑定到一个字符串类型的依赖属性,这样当你的工作流执行时可更容易地对它的值进行设置。方法是单击浏览按钮(三个点的按钮),打开“将Error绑定到活动的属性”对话框,然后你就可对它要绑定的属性进行选择。
4.因为在我们的主应用程序中没有WorkflowSuspended事件处理程序,因此我们需要对主应用程序的Program.cs文件进行编辑。在合适的位置添加下面的代码:
workflowRuntime.WorkflowSuspended+=newEventHandler<WorkflowSuspendedEventArgs>(workflowSuspended);
5.因为我们使用了名称为WorkflowSuspended的事件处理程序,因此我们需要实现该事件处理程序,代码如下:
static void workflowSuspended(object sender, WorkflowSuspendedEventArgs e)
{
Console.WriteLine("Workflow instance suspended, error: '{0}'.", e.Error);
waitHandle.Set();
}
6.编译该应用程序,然后按F5或Ctrl+F5执行该程序。该程序的输出结果如下:
运行该应用程序时,你会在控制台中看到主应用程序中的WorkflowSuspended事件处理程序产生的输出结果。但你能做更多的工作,而不是仅仅向控制台输出一串文本。你能为你的业务处理工作流产生任何其它动作。尽管在这里你可能恢复该工作流的处理过程,但通常并不建议这样做。一是全部正处理的活动将会被跳过,保留你的工作流实例以进行处理过程的恢复是后面阶段要做的事,这可能不是好事情(跳过了那些步骤,你又怎么说明它的原因呢?)。但是至少,你能从处理进程中干净地移除该工作流实例,并可使用任何必要的清理代码。
似乎异常和暂停工作流实例都有不足,因此,假如你需要的话,你可这样做,那就是终止你的工作流实例。让我们来看看怎么做。
使用Terminate活动
有些时候事情会变得很糟糕,例如你没有资源,需要结束某个工作流实例;也许从外部进程中返回的一些数据的格式或者计算结果是错误的;或者数据库服务器出现问题,没有它你就不能前进等等。
WF为我们提供了一个现成的方式来终止我们的工作流,那就是使用Terminate活动。Terminate活动的使用方法和Suspend活动完全相同,事实上它们的属性也是相同的。不同之处在于,当Terminate执行时,所有期望你的工作流实例要继续执行的事情都将丢失。
当Terminate执行时,工作流运行时触发WorkflowTerminated事件,这正像有一个未处理的异常一样。当处理WorkflowTerminated事件时获取两个不同方面的信息是困难的,所有你能做的实际上就是检查WorkflowTerminatedEventArgs参数,看看它的Exception属性。假如该工作流实例是使用Terminate活动终止的,该异常类型将会是System.Workflow.ComponentModel.WorkflowTerminatedException而不会是其它(甚至是更加常见)的异常类型。
我们就来看看在我们的工作流代码中怎样使用Terminate活动。
修改我们的工作流,以便使用Terminate活动
1.下载本章源代码,用Visual Studio打开ErrorTerminator文件夹中的解决方案(ErrorTerminator Completed文件夹中为本例的最终源代码)。选中ErrorFlow项目中的Workflow1.cs文件,打开它的工作流视图设计器界面。
2.在错误处理程序的设计界面上删除已存在的Suspend活动,然后从工具箱中拖拽一个Terminate活动到错误处理程序的设计界面上,把该活动放在Code活动的下面。
3.在放好该Terminate活动后,设置它的Error属性为“This is an example termination error...”字符串。
备注:再重复一遍,你可像我们现在做的一样,设置该属性为一个文本字符串,但你也能把该属性绑定到某个活动的字段、属性或者依赖属性上。
4.编译该应用程序,修正所有的编译错误,然后按下F5或者Ctrl+F5运行该应用程序,你将看到下面的运行结果:
Terminate活动和Suspend活动一样,都是相当简单的活动,但它很强大。你通常不会需要它,但当你的工作流出现问题不能继续运行时,Terminate活动就是工具箱中最好的工具。