WF 从入门到精通

wanghuasz

贡献于2014-06-26

字数:0 关键词: .NET开发

WF 从入门到精通 第一章:WF 简介 ............................................................................................1 第二章:WORKFLOW 运行时 ........................................................................7 第三章:WORKFLOW 实例..........................................................................14 第四章:活动及 WORKFLOW 类型介绍.......................................................23 第五章:WORKFLOW 跟踪..........................................................................31 第六章:加载和卸载实例...............................................................................50 第七章:基本活动的操作...............................................................................63 第八章:调用外部方法及工作流....................................................................82 第九章:逻辑流活动....................................................................................108 第十章:事件活动 .......................................................................................133 第十一章:并行活动....................................................................................163 第十二章:策略和规则 ................................................................................188 第十三章:打造自定义活动 .........................................................................213 第十四章:基于状态的工作流......................................................................240 第十五章:工作流和事务.............................................................................258 第十六章:声明式工作流.............................................................................279 第一章:WF 简介 学习完本章,你将掌握: 1.了解工作流的概念和理论 2.把 WF 和 BizTalk 与 WCF 做比较 3.开始使用 WF 进行编程 4.知道怎样使用 Visual Studio 工作流支持 以下是一小段进行邮政编码验证的代码 string USCode = @"^(\d{5}$)|(\d{5}$\-\d{4}$)"; string CanadianCode = @"[ABCEGHJKLMNPRSTVXY]\D[A-Z]\d[A-Z]\d"; public static bool ValidatePostalCode(string str) { return (Regex.IsMatch(str,USCode)||Regex.IsMatch(str,CanadianCode)); } 这没有什么特别的:“测试一个输入的字符串,如果为美国 ZIP 编码或者加拿大邮政编 码则返回 True,否则返回 False”。这是一段很实用的代码,事实上假如你不想在 ASP.NET 中使用其它验证控件的话,你就可在你的 ASP.NET 中使用这段验证逻辑。我们现在就创建一 个 Workflow 应用程序,它将进行同样的验证,并且返回验证是否通过的信息。 创建一个基于控制台的 Workflow 项目 1.启动 Visual Studio 2008 2.创建顺序工作流控制台应用程序项目 3.项目名称中输入 PCodeFlow 4.点击确定,将自动生成下面的初始界面 在编辑代码或插入 Workflow 项前,停留片刻看看 Workflow 项目为你生成的两个文件: Program.cs:从许多方面可以看出,这是一个典型的控制台应用程序源文件。然而,这 个模板增加了大量代码来支持 Workflow 的操作。理解这些代码是本书的一个主要目标,但 本章只是简单了解一下它做了些什么。 Workflow1.cs:这是一个 Workflow 组件,我们将对其进行修改以进行邮政编码的验证, 第一次使用你就可以放入许多东西,但我们现在还是从使用这个简单的 Workflow 开始工作 吧。 创建一个工作流 在工具箱中拖一个 IfElse 活动组件到 workflow 设计界面上。 我们现在将使用 IfElse 活动组件来问下面的问题:“我们已有的一个字符串是一个合 法的邮政编码吗?”。我们在代码中将确实使用先前你看到的代码段中的正则表达式来问这 一问题。 但在我们进行这一步前请仔细看看 workflow 的视图设计器。workflow 视图设计器提醒 我们没有提供相应的代码来做这一决定。假如你看看命名为“ifElseBranchActivity1”的 左边分支的右上角,你将看到里面有一个惊叹号标记的小圆形图标。这是 workflow 视图设 计器在告诉你 workflow 并不完整。假如你试图编译带有这种提醒图标的项目时,将会编译 报错。如你把鼠标移到图标上并单击呈现出的向下箭头时,你 还 能 看到更多关于这一错误情 况的信息。 在 IfElse 活动的分支上添加代码 1.移动鼠标到命名为“ifElseBranchActivity1”的左边分支上,单击以在属性面板上 激活这一分支的属性。 2.我们添加一个条件,意思是说 workflow 执行到分支时将强制其执行一些动作(条件 判断为 True 时,将执行左边分支)。为做到这些,单击“condition”属性激活条件类型属 性的下拉列表。从列表中你可以选择“代码条件”类型、“(无)”类型和“声明性规则条 件”类型。这里选择“代码条件”类型。 3.上述步骤完成后,“ condition”类型属性用户界面会包含一个“+”号,我们单击展 开它的子属性,该子属性也命名为“condition”,单击以激活它。 4.“condition”属性需要输入我们想添加的内部事件名字。当条件需要判断时这个事 件将激发。在本例子中我们输入“EvaluatePostalCode”。 Visual studio 2008 在幕后为你在 workflow 源文件中添加了你在“condition”属性 中所指明的事件。稍候我们将添加在事件激发时所要执行的正则表达式代码段。 在我们做这些工作之前,让我们继续在 workflow 视图设计器上工作,我们刚刚增加了 一个条件,它将引发工作流选择左边路径还是右边路径。但是两条路径中都没有指明工作流 将进行的动作。我们在左边“ifElseBranchActivity1”分支和右边“ifElseBranchActivity2” 分支中添加活动。 添加 Code 活动 1.拖一个“Code”活动到 workflow 视图设计器上,并放到左边分支 (ifElseBranchActivity1)的区域内。 2.就像先前添加条件判断的代码一样,我们将为该分支添加被选中时执行的代码。单击 “codeActivity1”图标以在属性面板中激活它的属性。 3.在“ExecuteCode”属性中输入“PostalCodeValid”。 Visual Studio 2008 会自动插入该事件。稍候我们会提供对应的执行代码。右边分支 也同样做,只是要在“ExecuteCode”属性中输入“PostalCodeInValid”。 在我们的 workflow 中添加事件处理代码 1.打开 Workflow.cs 准备进行编辑 2.添加引用:using System.Text.RegularExpression; 3.定位到“EvaluatePostalCode”方法上,插入下面的代码: private void EvaluatePostalCode(object sender, ConditionalEventArgs e) { string USCode = @"^(\d{5}$)|(\d{5}$\-\d{4}$)"; string CanadianCode = @"[ABCEGHJKLMNPRSTVXY]\D[A-Z]\d[A-Z]\d"; e.Result = (Regex.IsMatch(_code, USCode) || Regex.IsMatch(_code, CanadianCode)); } 变量 e 是“ConditionalEventArgs”类型的实例,它用来告知“IfElse”活动应选择哪 条路径。 4.我们也需要为 workflow 活动添加一种能力,以 便接受输入的字符串来进行验证工作。 为此我们添加下面的代码,我们将声明一个名为“PostalCode”的公有属性。 private string _code=string.Empty; public string PostalCode { get { return _code; } set { _code = value; } } 有了这些,我们的 workflow 应用程序就可以进行编译了,但程序并不完整,我们还要 在 Workflow1.cs 文件中定位到“PostalCodeValid”方法并插入下面的代码: Console.Write("The postal code {0} is valid.", _code); 同样在“PostalCodeInValid”方法中插入下面的代码: Console.Write("The postal code {0} is *invalid*.", _code); 调用工作流 1.打开 Program.cs 文件。 2.定位到: WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(PCodeFlow.Workflow1),wfArgs); 3.把上述代码替换为: Dictionary wfArgs = new Dictionary(); wfArgs.Add("PostalCode", args.Length > 0 ? args[0] : ""); WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(PCodeFlow.Wor kflow1),wfArgs); 编译应用程序 执行你的工作流应用程序 1.打开命令提示符窗口。 2.在命令提示符下定位到编译后所生成的应用程序目录。 3.输入 pcodeflow 12345 查看执行结果。 4.输入 pcodeflow 1234x 再看看执行结果。 第二章:workflow 运行时 学习完本章后,你将掌握: 1.在你的应用程序中使用 workflow 2.理解“WorkflowRuntime”对象的的基本功能 3.知道如何启动和停止 workflow 运行时 4.能够使用各种 workflow 运行时的相关事件 当你在 WF 环境中执行任务时,需要一些东西来监管执行的过程,这个东西就是命名为 “WorkflowRuntime”的对象。WorkflowRuntime 启动一个独立的工作流任务。在你的任务 执行过程中,WorkflowRuntime 也会针对不同的情况响应对应的事件。并且, WorkflowRuntime 还能在你的执行环境中增加一个附加的服务来保持跟踪。 WF 架构纵览见下图: WF 和你的应用程序并行执行。其实,我们需要你的应用程序作为宿主。宿主应用程序 可以是 Windows Forms 应用程序,控制台应用程序,ASP.NET WEB 应用程序,甚至可以是一 个 Windows Server。WF 运行时和你的应用程序同在一个.NET 应用程序域执行,每个应用程 序域只有一个唯一的 WorkflowRuntime 实例,试图在一个应用程序域中建立第二个 WorkflowRuntime 的实例的话,其结果就是抛出一个“InvalidOperationException”异常。 workflow 应用程序-“workflows”-意思指创建的逻辑上的一组活动。这些逻辑上的活 动用来完成你需要的工作流任务。当你宿主 workflow 运行时的时候,其实你就在操作工作 流中的活动并让 workflow 运行时执行他们。其 结 果 就 是 生成一个 workflow 实例,workflow 实例是一个当前正执行的 workflow 任务,它 能 自 己 完 成逻辑上的一组活动,回 忆 第 一章吧, 活动能执行你提供的代码并且能对输入的数据做出相应的决定。下一章我们将简述工作流实 例,后面几章将对活动进行介绍。 在宿主应用程序中添加 WF 一、创建一个名称为 WorkflowHost 的控制台应用程序项目 二、为项目添加名为 System.Workflow.Runtime 的引用 三、宿主 workflow 运行时 1.打开 Program.cs 文件准备编辑 2.在“using System.Text;”下添加以下代码: “using System.Workflow.Runtime” 3.定位到“Main”方法,在里面添加以下代码: WorkflowRuntime workflowRuntime=new WorkflowRuntime(); 4.编译程序确认没有错误。在本章我们都将使用这一应用程序。 四、深入了解 WorkflowRuntime 对象 我们现在已经在我们的宿主应用程序中建立了一个 WorkflowRuntime 类型的实例,该 是简单的了解怎样和这个对象交互的时候了。和大多数有用的对象一样,WorkflowRuntime 也暴露了一些方法和属性,我们可用他们来控制 Workflow 运行时的环境。表 2-1 列出了所 有 WorkflowRuntime 属性,表 2-2 则列出了我们经常使用的方法。 表 2-1 WorkflowRuntime 的属性 属性 功能 IsStarted 用来指明 workflow 运行时是否已经启动并准备接受 workflow 实例。当 宿主调用“ StartRuntime”前 IsStarted 为 False。期间它一直维持 True 直到宿主调用“StopRuntime”为止。需注意的是当它正在运行中你不能 增加核心服务。 Name 获取或设置和 WorkflowRuntime 关联的名字。Workflow 运行时正在运行 中你不能设置这个属性(也就是说当 IsStarted 为 True)。企图这样做 的结果就是抛出一个“InvalidOperationException”异常。 表 2-2 WorkflowRuntime 的方法 方法 功能 AddService 为 workflow 运行时添加指定的服务。能添加的服务类型和时间受到种种 限制。关于服务的详细信息将在第五章介绍。 CreateWorkflow 创建一个 workflow实例,它 包含一些指定(但可选)的参数。假如 workflow 运行时没有启动,该方法就调用 StartRuntime 方法。 GetWorkflow 通过 指明 workflow 实例的标识符(由一个 Guid 组成)来检索 workflow 实例。假如这个 workflow 实例是空闲和持久化保存的,它将被重新加载 并执行。 StartRuntime 启动 workflow 运行时和相关服务,并引发“Started”事件。 StopRuntime 停止 workflow 运行时和相关服务,并引发“Stoped”事件。 还有更多的关于WorkflowRuntime的方法,但表2-2中列出的方法是最经常用到的方法, 也是我们将重点关注的方法。在 workflow 运行期间,WorkflowRuntime 也将在各种时间引 发许多事件,但我们将在后面的章节中介绍。 创建一个 Workflow 运行时工厂 单例和工厂设计模式的组合是强大的,因为工厂能保证只创建出一个曾创建的对象的单 一实例,这 正 好符 合 我们的要求(在这里使用单例模式的原因主要是从效率上考虑,其次 一 个 应 用程序域也只能只有一个 WorkflowRuntime),因为 WorkflowRuntime 完全有可能在不 同的应用当中加载和启动(例如独立的应用模块)。让我们看看怎样创建一个 WorkflowRuntime 工厂。 一、在项目中添加一个类型为类的新项,文件名为 WorkflowFactory.cs。 二、在 WorkflowFactory.cs 源文件中添加如下的引用 using System.Workflow.Runtime; 三、在类中添加下面的代码: //workflow runtime 的单一实例 private static WorkflowRuntime _workflowRuntime = null; private static object _syncRoot = new object(); 四、在上述代码后添加如下方法: //工厂方法 public static WorkflowRuntime GetWorkflowRuntime() { //多线程环境下防止并发访问 lock (_syncRoot) { if (null == _workflowRuntime) _workflowRuntime = new WorkflowRuntime(); } return _workflowRuntime; } 五、为类加上 Public 关键字,为防止类被直接实例化,还必须为类加上 static 标记, 如下所示: public static class workflowFactory 启动 workflow 运行时 参考表 2-2,里面有一个 StartRuntime 方法,从我们的工厂对象中调用这个方法很有 意义,外部对象要求 workflow 运行时对象无需处理或担心运行时环境状态的初始化。我们 需要在我们的应用程序通过这一步来建立我们需要的 workflow 环境。外部调用对象也需要 workflow 运行时对象易于使用。 并不是一定要调用 StartRuntime。假如我们建立了一个 workflow 实例,StartRuntime 实际上就已被调用。假如我们曾经创建了一个 workflow 实例,或许并不用担心需要明确的 调用 StartRuntime。但是,一旦我们添加服务时,明确地调用它就很有必要,因为可增强 代码的可维护性并确信运行时环境的状态已建立,这样任何人就都能使用 workflow 运行时 对象。 因此让我们在我们的工厂对象中做些轻微的更改并直接调用 StartRuntime。 1.打开 WorkflowFactory.cs 文件并定位到下面的代码上: _workflowRuntime = new WorkflowRuntime(); 2.在上面的代码下添加以下的代码: _workflowRuntime.Starttime(); 停止 workflow 运行时 是否有办法启动一个workflow运行时很有意义,如 何 停 止 一个 workflow运行时也一样。 看看表 2-2 吧,里面有一个 StopRuntime 方法正好符合我们要求。调用 StopRuntime 方法会 卸载所有正执行的 workflow 和服务并关闭 workflow 运行时环境。当然,正确调用 StopRuntime 位置是在你申请停止逻辑结束之前或者应用程序域关闭前调用。 1.打开 WorkflowFactory.cs 文件并定位到下面的代码上 _workflowRuntime = new WorkflowRuntime(); 2.在上面代码的前面增加以下代码: _workflowRuntime.Starttime(); 3.在 WorkflowFactory.cs 中增加 StopWorkflowRuntime 事件处理函数: static void StopWorkflowRuntime(object sender, EventArgs e) { if (_workflowRuntime != null) { if (_workflowRuntime.IsStarted) { try { _workflowRuntime.StopRuntime(); } catch (ObjectDisposedException) { } } } } 以下是 WorkflowFactory.cs 文件的完整源代码,在 第 五 章 之 前我们不会做更多的改变: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Workflow.Runtime; namespace WorkflowHost { public static class WorkflowFactory { //workflow runtime 的单一实例 private static WorkflowRuntime _workflowRuntime = null; private static object _syncRoot = new object(); //工厂方法 public static WorkflowRuntime GetWorkflowRuntime() { //多线程环境下防止并发访问 lock (_syncRoot) { if (null == _workflowRuntime) { AppDomain.CurrentDomain.ProcessExit += new EventHandler(S topWorkflowRuntime); AppDomain.CurrentDomain.DomainUnload += new EventHandler( StopWorkflowRuntime); _workflowRuntime = new WorkflowRuntime(); _workflowRuntime.StartRuntime(); } } return _workflowRuntime; } static void StopWorkflowRuntime(object sender, EventArgs e) { if (_workflowRuntime != null) { if (_workflowRuntime.IsStarted) { try { _workflowRuntime.StopRuntime(); } catch (ObjectDisposedException) { } } } } } } 现在我们有了一个 workflow 运行时的创建工厂,然后我们将修改我们的主程序来使用 它。 使用 workflow 运行时创建工厂 1.打开 Program.cs 文件并定位到如下代码上: WorkflowRuntime workflowRuntime=new WorkflowRuntime(); 2.把上面的代码修改成以下代码: WorkflowRuntime workflowRuntime=WorkflowFactory.GetWorkflowRuntime(); 表 2-3 workflow 运行时的相关事件描述 事件 功能 Started 当 workflow 运行时启动后激发。 Stopped 当 workflow 运行时停止后激发。 WorkflowCompleted 当一个 workflow 实例完成后激发。 WorkflowIdled 当一个 workflow 实例进入空闲状态时激发。当 workflow 实例进入了空闲状态后,你就有机会把他们从内存中卸载 掉、存储到数据库并可在稍后的时间把它们加载进内存。 WorkflowTerminated 当一个 workflow 实例被终止后激发。在宿主中调用一个 workflow 实例的 Terminate 方法、或通过一个 Terminate 活动、或当 workflow 运行时产生一个未经捕获的异常时都 会终止该 workflow。 我们还将在第四章和第五章介绍更多的事件。 在我们为上面的事件添加相应的事件处理程序时,你 会 看到生成的代码和上一章我们创 建的基于工作台的顺序工作流应用程序中的代码完全一样(或几乎完全一样)。为了看看这 些事件的作用,我们需要停止应用程序主线程一段时间。因此,我们使用一个基于内核的自 动重置事件。一会 儿 后 ,我们将写出一些代码来使用上述事件中的几个,你 需要 不 时 看看第 一章中 PCodeFlow 项目中的 Program.cs 文件,对比它们的不同以及该写入什么样的代码。 尽管它们并不完全相同,但你在两个程序中还是能找到相同的内容。 处理 workflow 运行时事件 1.启动 Visual Studio,打开项目的 Program.cs 源文件,定位到下面的代码上: WorkflowRuntime workflowRuntiem=WorkflowFactory.GetWorkflowRuntime(); 2.假如你用过.NET 的委托,下面的代码你将非常熟悉。我们需要为我们感兴趣的事件 增加相应的事件处理程序。我们现在就来为 workflow 空闲时和完成后增加相应的事件处理 程序。稍候我们还会增加我们所需要的更多的事件处理程序。记住,下面的代码在步骤 1 定位的代码的下面: workflowRuntime.WorkflowIdled += new EventHandler(workflowIdled); 3.下面的代码添加了对 workflow 完成后的事件处理: workflowRuntime.WorkflowCompleted += new EventHandler(workflowCompleted); 4.现在添加对 workflow 终止后的事件处理: workflowRuntime.WorkflowTerminated += new EventHandler(workflowTerminated); 5.假如你编译并运行 WorkflowHost(本项目),这个应用程序能通过编译并运行。但 没有执行 workflow,因为我们并未告知 workflow 运行时去启动一个 workflow 实例(我们 将在下章添加)。为以后做准备,我们还要添加一些代码。首先,为了激发 workflow 中的 事件(以便我们观察它们),我们需要停止主线程足够长的时间,因此我们还将添加自动重 置事件。在步骤 3、4 的代码下添加以下代码。 Console.WriteLine("对待 workflow 完成。"); waitHandle.WaitOne(); Console.WriteLine("完成."); 6.在 Main 方法前定义一个名为 waitHandle 的静态成员: private static AutoResetEvent waitHandle = new AutoResetEvent(false); 7.添加名称空间: using System.Threading; 8.由 Vistual Studio 2008 创建的上面三个事件对应的事件处理程序内都包含“throw new NotImplementedException();”。我们需要移除这些代码并定位到 workflowIdled 的事 件处理程序内,写入下面的代码: Console.WriteLine("workflow 实例空闲中"); 9.定位到 workflowCompleted 的事件处理程序内,写入下面的代码: Console.WriteLine("workflow 实例已完成"); waitHandle.Set(); 10.定位到 workflowTerminated 的事件处理程序内,写入下面的代码: Console.WriteLine("workflow 实例已终止,原因:'{0}'。",e.Exception.Message); waitHandle.Set(); 完整的代码见列表 2-2。 列表 2-2 WorkflowHost 应用程序的完整代码 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Workflow.Runtime; using System.Threading; namespace WorkflowHost { class Program { private static AutoResetEvent waitHandle = new AutoResetEvent(false); static void Main(string[] args) { WorkflowRuntime workflowRuntime = WorkflowFactory.GetWorkflowRunt ime(); workflowRuntime.WorkflowIdled += new EventHandler(workflowRuntime_WorkflowIdled); workflowRuntime.WorkflowCompleted += new EventHandler(workflowRuntime_WorkflowCompleted); workflowRuntime.WorkflowTerminated += new EventHandler(workflowRuntime_WorkflowTerminated); Console.WriteLine("等待 workflow 完成。"); waitHandle.WaitOne(); Console.WriteLine("完成."); } static void workflowRuntime_WorkflowIdled(object sender, WorkflowEven tArgs e) { Console.WriteLine("workflow 实例空闲中"); } static void workflowRuntime_WorkflowCompleted(object sender, Workflow CompletedEventArgs e) { Console.WriteLine("workflow 实例已完成"); waitHandle.Set(); } static void workflowRuntime_WorkflowTerminated(object sender, Workflo wTerminatedEventArgs e) { Console.WriteLine("workflow 实例已终止,原因:'{0}'。 ",e.Exception.Message); waitHandle.Set(); } } } 下一章我们将深入 workflow 实例,假如现在你执行这个程序,他会一直挂起。为什么 呢?因为我们从未执行一个 workflow 实例,因此我们加入的事件的从未被激发,也就未执 行对应的事件处理程序。程序将永远挂起(或者你亲自终止它)。在下一章中当我们添加一 个 workflow 实例并执行它时我们还会看到这个程序。 第三章:workflow 实例 学习完本章,你将掌握: 1.使用不带参数和带参数二种方式初始化一个 workflow 实例 2.测定你运行中的 workflow 实例的状况 3.停止 workflow 实例 4.确定你的 workflow 空闲或终止的原因 一个 workflow 实例由一个或多个活动组成。(我们将在第七章开始介绍各种活动: “Basic Activity Operations.”)“primary activity”或者“root activity”被称作 “workflow definition”。“workflow definition”通常的行为是为其它将要工作的活动 充当一个容器。 注:“workflow definition”是你要求 workflow 去执行的东西,而一个 workflow 实 例是一个正在执行的“workflow definition”。它们之间有明显的区别,一个正在执行当 中,而另一个则不是。 workflow 实例从哪里来?它们当然应由你来创建。如你有困难来完成这个任务,并且 自动创建的 workflow 符合你的应用要求的话,也可由软件来完成,但至少你也要写出 workflow 的任务或者 workflow 运行时将为你执行的任务。Microsoft 提供了 workflow 运行 时,你还得创建余下的东西。毕竟,这是你的应用。 WF在上述这些地方的创建上可以为你提供帮助,WF 不仅将执行你创建的workflow实例, 而且也将帮助你去创建它们。WF 集成了丰富的图形界面设计器,它能帮你以相同的方式把 workflow 集成到你创建的 ASP.NET Web Forms、Windows Forms 或者 WPF 应用中。你可在工 具箱上滚动鼠标,从许多活动项中选中一个,然后把它拖到设计界面上并释放它。假如这个 活动项具有可配置的属性,你还可使用 Visual Studio 中的属性面板来配置它,使它符合你 的意图。我们已在第一章简要地使用过 workflow 设计器,在这里我们将再次使用它,毕竟 与 WF 相关的工作几乎全是创建 workflow 任务,workflow 可视化设计器的使用是开发过程 中巨大的一个组成部分。 workflow 实例和任何其它软件类似。它们会开始执行、运行,直到遇到终止条件时终 止。这些或许是数据库中的所有记录已被处理,所有需被压缩的档案已被压缩,或者 workflow 发向各个审批方的文档已被批复(同意或不同意),或者是处理已经完成。它只 有一个正常的启动位置,但有一个或多个正常的可能停止的位置。 实例也能维持错误、异常。你可以处理这些异常也可不处理它。在某些情况下,或许你 不想去处理出现的异常,并留到以后进行处理。 有时,一个 workflow 处理过程会执行很长很长时间才能完成。例如,一个处理过程发 送了一份零件的订单并等待订单被接收。在 workflow 终止前须确认零件的型号和数目,而 这或许会花去几天,几周甚至几月。因此,难道一个 workflow 实例也需要在内存里维持激 活状态几天,几周或者几月吗?假如服务器崩溃或电源断电怎么办?你的 workflow 实例、数 据、应用程序状态不是通通丢失了吗? workflow 实例和组成实例的活动是 workflow 处理过程中的重要部分。WF 已经为 workflow 实例的创建及执行提供了强大的支持。我们就来看看 WorkflowInstance 对象。 WorkflowInstance 对象介绍 workflowInstance 是一个 WF 对象,它为你提供了你的独立的 workflow 任务上下文(环 境)。你 可 使用这个对象去找到在你的处理任务中事情将是如何进行的。就像我们有方法和 属性去控制 workflow 运行时一样,我们也有方法和属性并用它们和我们的 workflow 实例进 行交互。表 3-1 列出了大多数 WorkflowInstance 属性,表 3-2 列出了经常使用的方法。我 们还将在第五章看到一些额外的属性和方法,“工作流跟踪”。 表 3-1 WorkflowInstance 的属性 属性 功能 InstanceId 得到 workflow 实例的唯一标识(一个 Guid) WorkflowRuntime 得到本 workflow 实例的 WorkflowRuntime 表 3-2 WorkflowInstance 的方法 方法 功能 ApplyWorkflowChanges 通过 WorkflowChanges 对象申请对 workflow 实例进行更改。这允 许你在 workflow 执行时修改它(增加、移出或更改活动),当动 态的更改实施时,workflow 实例会被暂停。 GetWorkflowDefinition 检索本 workflow 实例的根(root)活动。 Resume 恢复执行先前被暂停的 workflow 实例。假如 workflow 实例并未处 于暂停状态,则不做任何事情。假如 workflow 实例处在暂停状态, workflow 运行时就会在 workflow 实例刚被恢复后触发 WorkflowResumed 事件。 Start 启动一个 workflow 实例的执行,在这个 workflow 实例根活动上调 用 ExecuteActivity。假如 Start 发生异常,它 通过调用 Terminate 终止这个 workflow 实例,并附加异常相关信息作为终止的原因。 Suspend 同步暂停本workflow 实例。假如 workflow 实例本就处于暂停状态, 则不做任何事情。假如 workflow 实例正在运行,则 workflow 运行 时就暂停该实例,然后设置 SuspendOrTerminateInfoProperty(说 明原因)并进入 Suspend,触发 WorkflowSuspended 事件。 Terminate 同步终止本 workflow 实例。当宿主需要终止 workflow 实例时, workflow 运行时就终止这个实例并试图持久化实例的最终状态。 然后 WorkflowInstance 设置 SuspendOrTerminateInfoProperty (说明原因)并进入 Terminate。最后,它 触 发 WorkflowTerminated 事件并把终止原因传到 WorkflowTerminateException 中的 Message 属性并包含到 WorkflowTerminatedEventArgs 事件参数 中。另外,假如在持久化时发生异常,workflow 运行时取而代之 地就把异常传到 WorkflowTerminatedEventArgs 事件参数中。 还有更多和 WorkflowInstance 相关的方法还未列出。到第六章“实例的加载和卸载”, 我们持久化工作流到数据库中时将看到他们的更多细节。 启动一个工作流实例 当我们启动一个 workflow 实例前,我们必须有一个 workflow 任务让 WF 去执行。在第 一章,我们通过 Visual Studio 为我们创建了一个基于 workflow 的项目,它自动包含一个 workflow 任务,我们对它进行了修改以进行 U.S.和加拿大邮政编码的验证。如果需要的话, 我们可以返回到那个项目去复制源代码,或者引用 PCodeFlow.exe 程序集。然后我们就可直 接使用这个已创建的 workflow。实际上,你可以这么去做。 然而,我们还是应该试着去学会写 workflow 的应用。让我们通过使用一个包含延时的 顺序工作流去模拟一个长时间运行的任务吧。我们将在延时前执行一些代码,以 弹 出 一个信 息对话框。在 经过 延 时后,我们将再次弹出一个信息对话框来指明我们的工作已经结束。通 过本书的学习过程,我们的例子将会越来越详细和丰富,但现在我们还处于入门阶段,我们 还将保持我们的例子并把注意力更多的放到概念上而不是提高技巧上。 注:记住,顺序工作流执行活动时一个接着一个。这个处理方式可和状态机工作流做下 比较,状态机工作流执行活动时是基于状态的转变。假如你现在对此一片茫然的话,不用担 心,我们将在下章进入该主题。 在 WorkflowHost 解决方案中添加一个顺序工作流项目 1.启动 Visual Studio 2008,加载上一章创建的名为“WorkflowHost”的解决方案准 备进行编辑。 2.在解决方案中添加一个崭新的 workflow 项目。 3.项目模板选择顺序工作流库。 4.项目名称起名为:LongRunningWorkflow。 现在打开 workflow 的视图设计器准备创建我们的 workflow 任务。在 视图设计器中的大 图片中,我们将添加三个活动到这个新 workflow 任务中:两个 Code 活动和一个 Delay 活动。 Delay 活动将被放到两个 Code 活动中间,目的是可让我们在 Delay 执行前和执行后都将弹 出一个信息对话框。最初我们会指定一个合适的延时时间值,但稍后我们将对 workflow 任 务进行修改,以使 workflow 任务初始化时能接受我们专门指定的一个延时时间值。 创建这个模拟需执行很长时间的顺序工作流 1.激活 workflow 视图设计器,移动鼠标到工具箱中。 2.从工具箱中选择 Code 活动,并把该组件拖拽到 workflow 设计器的表面。 3.释放鼠标并让 Code 活动组件落到该顺序工作流中。 4.就像在第一章一样,我们将添加一些代码到 Code 活动中以使 worflow 任务经过这个 活动时执行。在此单击 Code 活动以确保该活动的属性面板已被激活。 5.在属性面板中激活 ExecuteCode 属性的下拉编辑框,它将允许你命名将被触发的事 件,该事件在 Code 活动中的代码执行时触发。 6.输入“PreDelayMessage”。这样就添加了一个事件到 worflow 代码中。稍候,我们 将修改这段代码以显示一个信息对话框。但现在我们仍继续在 workflow 的视图设计器上工 作,因为我们需要添加另外两个活动。 7.从工具箱中选择 Delay 活动并添加到 Code 活动的下面。 注:顺序活动,就像我们现在所做的工作一样,是以顺序的方式执行活动。顺序由 workflow 视图设计器中活动的位置决定。在 workflow 设计器窗口的顶部的活动首先执行, 对于其它活动的执行顺序则按到视图设计器窗口底部的走向(箭头)决 定 。在下章我们还将 重温这一过程。 8.我们需要为我们的 Delay 活动建立一个延时时间值。为此,我们要在 Visual Studio 属性面板中改变 TimeoutDuration 的属性。把最后两个“00”改为“10”,意思是 Delay 活动将等待 10 秒钟才允许 workflow 继续下一步的处理。 9.现在我们需要添加第二个 Code 活动来显示第二个信息对话框。为此,重复步骤 2 和 步骤 6 添加一个新的 Code 活动,但设置 ExecuteCode 的属性为“ PostDelayMessage"来作为 事件的命名。以下是在 workflow 视图设计器中展示的 workflow 的最终结果: 我们还剩下两个任务未完成。最终,我们需要把我们的 workflow 程序集引入到我们的 主应用程序中以便执行它。但首先,我们必须添加必要的代码以显示那两个信息对话框。我 们已经在我们的 workflow 代码创建了两个事件:PreDelayMessage 和 PostDelayMessage。 我们将为它们添加事件处理代码,在里面实际上就是弹出信息对话框的代码。 为延时前和延时后的事件添加代码 1.单击 LongRunningWorkflow 项目中的 Workflow1.cs 文件,查看其代码。 2.添加“System.Windows.Forms"的引用,并在 Workflow1.cs 文件声明以下名称空间: using System.Windows.Forms; 3.定位到新插入的 PreDelayMessage 方法,在方法中插入以下代码: MessageBox.Show("正在执行延时前的代码。"); 4.和上一步类似,定位到新插入的 PostDelayMessage 方法,在方法中插入以下代码: MessageBox.Show("正在执行延时后的代码。"); 假如你这时编译这个解决方案,那没有任何错误,但是 WorkflowHost 应用程序仍旧会 像先前一章一样挂起。为什么呢?因为尽管我们创建了一个我们能够使用的 workflow 程序 集,但我们并未请求主应用程序去执行它。WorkflowCompleted 事件从未被触发,因此自动 重置事件也就不会释放应用程序主线程。 为执行我们的 workflow 任务,我们要引用我们新创建的 workflow 程序集并添加代码, 以使 WorkflowRuntime 对象来揭开 workflow 任务工作的序幕。我们现在就开始吧。 宿主一个自定义 workflow 程序集并启动一个不带参数的 workflow 实例 1.首先在项目 WorkflowHost 中添加对项目 LongRunningWorkflow 的引用。 2.假如我们现在去编译这个应用程序,WorkflowHost 将编译失败。为什么呢?原因是 在前一章我们创建 WorkflowHost 项目时,我们仅仅添加了必须的引用以支持当时的环境编 译通过。但现在,我们添加了一个“System.Workflow.Runtime”引用。同时又引入了一个 现成的 workflow 程序集到我们的宿主应用程序中,因此我们需要为 WorkflowHost 项目添加 更多的和 workflow 相关的引用。我们需要添加的引用有“System.Workflow.Activities” 和“System.Workflow.ComponentModel”。 3.打开 Program.cs 文件并定位到 Main 方法内的下面代码上: Console.WriteLine("等待 workflow 完成。"); 4.在上述代码下面添加以下代码: WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(LongRunningWo rkflow.Workflow1)); instance.Start(); 5.编译并执行 WorkflowHost 应用程序。 执行结果如下: 让我们回到下面非常关键的代码上: WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(LongRunningWo rkflow.Workflow1)); instance.Start(); 在这里,我们使用了 WorkflowRuntime 对象的 CreateWorkflow 方法创建了一个我们想 去执行的 workflow 实例。当我们得到了一个返回的 WorkflowInstance 对象后,我们调用了 它的 Start 方法来初始化 workflow。注意这个 workflow 实例不需要我们预先输入参数就能 执行。如果能预先输入一个可变的延时值那该多好?下一节我们将讨论这一话题。 启动一个带参数的 workflow 实例 带输入参数启动的 Workflow 实例把接收到的参数和相关的公有属性对应起来。就是说, 为传入一个可变的延时值,我们只需在我们的 workflow 实例上创建一个公有的名为“ Delay” 属性,并在创建这个实例时提供延时值即可。假如你对 XML 序列化和.NET 中的 “XmlSerializer”熟悉的话,创建一个 workflow 实例的过程就和把 XML 流反序列化成一 个.NET 对象的过程相似。事实上,这几乎差不多。 期望被传入 workflow 实例的参数值通常存储在一个 Dictionary 对象的 Values 中, Dictionary 对象的关键字使用 string 类型,对应的值使用简单的 Object 对象。典型的代 码如下: Dictionary parms = new Dictionary(); 然后你可使用 Dictionary 对象的 Add 方法添加参数。关键字必须是一个 string,它表 示 workflow 的 root 活动所暴露的公有属性的名称。另外,对应的值的类型必须和活动的属 性类型一致。例如,我们传入一个整形类型的延时值并且我们的 workflow 实例中暴露了一 个名为 Delay 的属性和其对应,那添加一个参数到 Dictionary 中的代码就应像下面的一样: parms.Add("Delay",10); //延时 10 秒。 我们再次来写一些代码吧,我们相对做些小的修改,但这会获得许多功能。我们以控制 台命令行的方式接收我们输入的一个整形数值作为延时值。为使我们的程序不会永远运行下 去,我们会把这个值限制在 0 到 120 之间,意思延时范围从 0 秒到两分钟之间。我们也将对 workflow 增加 Delay 属性。让我们一起来对我们的 workflow 组件做第一次修正。 为 workflow 添加一个输入属性 1.打开 Workflow1.cs 文件准备编辑。 2.在 Workflow1 的构造函数后,添加以下代码: private Int32 _delay = 10; public Int32 Delay { get { return _delay; } set { if (value < 0 || value > 120) value = 10; if (ExecutionStatus == ActivityExecutionStatus.Initialized) { _delay = value; delayActivity1.TimeoutDuration = new TimeSpan(0, 0, _delay); } } } 我们对传入的整形值进行了检查,假如它超出范围,我们就指定一个默认值。我们检查 了 workflow 是否处在即将执行状态(不是已执行状态)。这可防止有人在我们的 workflow 运行中对延时值进行修改。我们也需要对 Main 方法进行少量修改。我们需在命令行中输入 一个参数作为延时值。假如它不是一个整形值,我们就退出。否则,我们就接受它。假如它 超过范围(0 到 120 秒),我们就对它进行必要的约束(为默认值 10 秒)。对 Main 所做的 修改步骤如下: 启动一个带参数的 workflow 实例 1.打开 Progrom.cs 文件准备编辑。 2.定位到下面的代码上: workflowRuntime.WorkflowIdled += new EventHandler(workflowRu ntime_WorkflowIdled); workflowRuntime.WorkflowCompleted += new EventHandler(workflowRuntime_WorkflowCompleted); workflowRuntime.WorkflowTerminated += new EventHandler(workflowRuntime_WorkflowTerminated); 3.在上述代码后添加下面的代码: Int32 delay = 0; string val = args.Length > 0 ? args[0] : "10"; if (!Int32.TryParse(val, out delay)) { Console.WriteLine("你必须输入一个整形值!"); return; } Dictionary parms = new Dictionary(); 4.找到下面的代码: WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(LongRunningWo rkflow.Workflow1)); 5.把上述代码改为: WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(LongRunningWo rkflow.Workflow1), parms); 编译并像在第一章那样运行试试,通过输入不同数值的延时时间,就可看到弹出的两个 信息对话框所反映出的延时效果。 确定 Workflow 实例的状态 有趣的是,假如你看看 workflow 运行时对象及 workflow 实例对象的方法和属性,你 找 不 到 和 状态相关的属性。你怎么知道是否有一个 workflow 在执行呢?假如有一个,它处在 那个状态呢?空闲吗?正执行当中吗?我们怎么确定? 我将向前跳一小段,这其中大部分逻辑都放在 workflow 状态的确定上。一个给定的 workflow 实例的 workflow definition 为您提供 workflow 的执行状态。基类 Activity 暴 露了一个 ExecutionStatus 属性,它是一个 ActivityExecutionStatus 枚举的一个成员。下 表 3-3 列出了 ActivityExecutionStatus 的枚举值和相关的意义。 表 3-3 ActivityExecutionStatus 枚举值 属性 功能 Canceling 活动正在取消中。 Closed 活动已被关闭。 Compensating 活动处于补偿状态。 Executing 活动当前正在运行。 Faulting 活动已产生并维持一个异常。 Initialized 活动已被初始化但还未运行。 表 3-3 中的所有枚举值都涉及到一个活动对象,但你需记住 workflow definition 就是 一个活动。这意味着假如我们查询 workflow definition 的状态,我们就能有效地确定整个 实例的状态。下面的过程演示了我们怎样添加相应代码来查询 workflow definitely。 确定 workflow 实例执行状态 1.打开 WorkflowHost 项目的 Program.cs 文件准备编辑。 2.找到 Main 方法并定位到下面的代码上: instance.Start(); 3.为了让我们看到 workflow 实例的状态,我们直接查询 workflow definition 的状态 并把结果输出到控制台中显示出来。在上一步中定位到的代码下插入以下代码: Console.WriteLine("workflow 处在:{0}状态。", instance.GetWorkflowDefiniton().ExecutionStatus.ToString()); 终止 Workflow 实例 假如你需要这样做的话,你也能容易地终止一个 workflow 实例,方法是通过执行 workflow 实例对象的 Terminate 方法。假如你在你的应用中添加了 WorkflowTerminated 的 事件处理,你 就 能 从 Exception 的 Message 属性获取终止的原因。你将发现 Exception 被包 装到 WorkflowTerminatedEventArgs 中,并传入到 WorkflowTerminated 的事件处理程序中。 这些代码 WorkflowHost 中已经包含了,我们还需添加一行代码来结束 workflow 实例。 终止 workflow 实例 1.打开 Program.cs 文件,找到如下我们刚添加的代码上: Console.WriteLine("workflow 处在:{0}状态。", instance.GetWorkflowDefinition().ExecutionStatus.ToString(); 2.在上述代码下添加以下代码: instance.Terminate("用户取消"); 假如你现在编译并运行 WorkflowHost 程序,为 他 提供一个 25 秒的延时值,你不会再看 到任何一个信息对话框,控制台的输出结果如下: Dehydration 和 Rehydration 在我们离开 workflow 实例的这一话题之前,我想再谈谈“dehydrating”和 “rehydrating”一个实例的概念。假如你有一个长时间运行的 workflow 任务或者有大量的 任务执行,你就能卸载任务并把必须的执行环境信息存储到一个 SQL Server 数据库中,这 要用到运行在 WF 之上的一个服务。 我们将在第六章详细讨论存储的问题,我在这提及它是因为,对一件事来说,处理的目 标是 workflow 实例。但另一方面,我们应听听这些术语,我不想让你在深入此书后却还不 理解它们的基本意思。 当你“dehydrate”一个实例时,你就正在把它从执行状态中移除并进行存储以便以后 恢复。典型的做法是使用 WF 的持久化服务,但你也能写你自己的服务来做同样的任务。以 后当你的应用程序侦测到需重启 workflow 实例时,你就“ rehydrate”这个实例它就返回当 时的执行状态。这样做的原因有很多,所有这些本书稍后都会简要说明 第四章:活动及 workflow 类型介绍 学习完本章,你将掌握: 1.workflow 活动是怎样形成的 2.顺序工作流和状态机工作流之间的区别 3.创建一个顺序工作流 4.创建一个状态机工作流 活动是 WF 中 workflow 处理的基本单位,假如你再把一个业务处理过程(或 workflow 任务)进行分解,你会发现它由更小、更细的任务组成。假如需设计这样一个大的任务,它 需把信息送到一系列的数据处理系统进行处理,那么子任务或许就包括这样一些事情:从数 据库读取数据,使用这些数据生成一个文件,通过使用 FTP 或 XML Web service 把文件传到 一个远程服务器上,标记信息已经被处理(如通过写入数据库并进入审计步骤),等等。这 些子任务都聚焦到一个特定的任务上:读数据库、上传文件、进行审计。换句话说,它们是 活动。 当你创建 workflow 时,你 会 把这些单独的活动捆在一起,并让活动从一个转到另一个。 一些活动可作为其它活动的容器。一 些活动执行一个单一任务,这 我们已谈过。基于容器的 活动用来容纳其它活动,在前一章中我们谈及的 root 活动就是这种活动。root 活动既可是 一个顺序活动也可是一个状态机活动,本章中我们将对这些活动的类型进行说明。 活动怎样知道在本步骤完成后下一步将做什么呢?本章将主要把焦点放在这上面上。或 许活动会以你创建一个 root 活动时指定的顺序执行,或者可能是仅在一个特定的事件发生 后才去执行一个指定的活动。为了让我们更好地理解活动,我们首先要去看看 WF Activity 对象,然后看看活动是怎样链接在一起的。 活动介绍:基本的工作单位 WF 为你提供了一个 Activity 对象。Activity 实现了一个看起来很简单的基类。它不会 做许多智能任务,但它可进行 workflow 的相互交互(这可不简单)。活动对象由“Activity” 派生,提供出了强悍的功能。你可自如地创建你自己的活动,这个话题将在第 13 章(自定 义活动)进行介绍。事实上,本书的第二部分都是在介绍活动(第 7 章至第 13 章)。表 4-1 列出了许多我们通常感兴趣的 Activity 的属性,表 4-2 列出了你会经常用到的方法。在第 13 章,你还会看到更多的和自定义活动相关的方法和属性。 表 4-1 活动(Activity)的属性 属性 功能 Description 获取或设置用户定义的关于活动的描述。 Enable 获取或设置一个指明实例能否被执行和验证的值。 ExecutionResult 获取实例最后运行的结果(ActivityExecutionResult)。(有 Canceled、Compensated、Faulted、None 和 Succeeded)。 ExecutionStatus 得到 workflow 的状态,其为 ActivityExecutionStatus 的枚举值 (Canceling、Closed、Compensating、Executing、Faulting 和 Initialized)之一。 Name 获取或设置活动实例的名称。 Parent 获取包含本活动的父活动。 WorkflowInstanceId 获取和该活动相关的 workflow 实例的标识符。 表 4-2 活动(Activity)的方法 属性 功能 Cancel 取消活动的执行。 Clone 返回活动的一个深拷贝。 Execute 以同步方式运行活动。 GetActivityByName 假如在一个组合活动上执行,本方法将返回组合活动中所包含的指 定名称的活动。 Load 从一个流中加载一个活动的实例。 RaiseEvent 触发一个和指定的依赖属性相关的事件。 RaiseGenericEvent 触发和所引用的依赖属性相关的事件。RaiseEvent 和 RaiseGenericEvent 的作用是一样的——第一个事件 RaiseEvent 直 接指出 DependencyPropenty,而 RaiseGenericEvent 则是一个泛型 版本。 Save 把活动保存到流中。 活动的方法通常都具有虚拟和受保护的属性。目的是你可去覆盖它们,使其提供一个符 合你自己的活动所需要的实现。目前为止,最关键的方法是 Execute。当 workflow 运行时 调用这个方法时,你的活动便开始执行了。 活动可被分为两个大类:组合活动和基本活动。组合活动包含其它活动。一个极好的例 子是我们目前为止贯穿书中的 Sequential 活动(译者注:它是基于顺序工作流中所有活动 的载体,在创建一个顺序工作流时 Visual Studio 就已为我们创建好了,可在视图设计器中 看到)。目前为止所有的程序实例执行 workflow 实例的方式都是 Sequential 活动,它包含 其它活动,如它自身、Delay 活动和 Code 活动。 基本活动,就像我刚谈到的 Delay 活动和 Code 活动,它 们 是一个基于单一任务的活动, 我在本章早些时候谈过它。最终,你 需要基本活动去实际承载特定的任务。组合活动或许可 指挥任务和数据的流动,但基本活动能做更多。 ActivityExecutionContext 对象 许多 Activity 对象的方法需要一个 ActivityExecutionContext 对象来进行输入。在 workflow 运行时把你要执行的 workflow 实例入队的时候 ActivityExecutionContext 对象 被创建,因此,它不是你直接要创建的对象。workflow 运行时为你创建它。 ActivityExecutionContext 对象的作用是提供活动以方法和服务,以 便 和 workflow 实 例挂钩。这些如初始化,定时器和产生执行流。它本质上是一个 helper 对象。在 13 章将更 详细的对活动上下文(环境)进行讨论。 备注:假如你熟悉 ASP.NET 编程的话,这个 context 对象本质上和 System.Web.HttpContext 对象的作用是一样的。其它相似的还有 System.Threading.Thread.CurrentContext。所有这些 Context 对象的目标都是一样的:提 供一个存储位置并容易地恢复一个当前执行实例的信息。这 种情况下,它是一个执行当中的 活动的一个实例。 依赖属性(Dependency Properties) 在表 4-2 中,你将看到一些依赖属性(DependencyProperty)。什么是依赖属性呢? 通常,假如你为类创建了一个属性的话,你 也会在类中创建一个字段来存储该属性的值。 普遍的代码如下: class MyClass { protected Int32 _x=0; public Int32 X { get { return _x;} set { _x = value; } } } 字段_x 更正式的叫法是 backing store。在这个例子中,你的类为 X 属性提供了 backing store。 然而,WF 和 WPF 通常都非常需要去访问你类中的属性。WPF 需要指明容器中控件的空间 和大小以便能最佳地被render。WF需要依赖属性来方便地进行活动绑定。WF中ActivityBind 类可为你方便地进行活动绑定。 活动验证 活动通常都具有验证能力,你可回忆第一章。 在第一章的例子中有这样一种情况,如在 IfElse 活动中未指定应该选择哪一个分支进 行执行的条件时,Visual Studio 会提醒我们。其它活动实现了不同的验证算法。假如我们 编译带有验证错误的代码,我们的编译都会失败。我们必须纠正这些验证条件不充分的代码, 才能编译和执行我们的 workflow 代码。 workflow 类型 你已创建过workflow应用程序,因此你可能注意到可以创建不同类型的workflow 应用。 workflow 应用程序的类型很大程度上依赖于你选择的 root 活动。 尽管你注意到在新项目对话框中仅仅只有两种 workflow 类型的应用程序可供选择, 但实际运用中存在三种主要的类型。迄今为止本书中你已经创建过顺序工作流,因此它们并 不神秘。 当你创建 workflow 时,你的活动以你规定的顺序执行。 另一种从新项目对话框中看到的 workflow 类型是状态机工作流。我将在本章讨论它的 更多细节。 第三种 workflow 类型基于顺序工作流,但它是规则驱动的。它不是仅仅执行你指定的 任务, 而是由 Policy 活动和规则条件组成的基于规则的 workflow,来执行基于你指定的业务规则 workflow 任务。 我们将在 12 章更多地学习这种 workflow 类型:“ Policy 活动”。因 为这种类型的 workflow 以顺序活动作为 root, 因此在新项目对话框中没有这种类型的 workflow 应用程序的模板可供选择。你应以顺序工 作流作为起始, 然后增加基于规则的活动。 选择一种 workflow 类型 在什么情况下一种类型的workflow比另一种类型的 workflow更好?你如何选择合适的 workflow 类型呢? 表 4-3 可为你提供一些基本的参考。 表 4-3 选择基本的 workflow 类型的判定表 workflow 类型 适用条件 顺序工作流 workflow 任务可以自治的执行,很少由外部进行控制。主要由 workflow 自身来对执行的任务进行控制。只有少量用户或没有用户来和它进行交 互。它的 root 活动是 SequentialWorkflow 活动。 状态机工作流 workflow 任务严重依赖外部来控制和指示其执行。预期有很多的用户交 互(或其它外部控制)。对于基于状态的 workflow,root 活动是 StateMachineWorkflow 活动。 基于规则的工作 流 业务逻辑中包含复杂的判断条件,既不像顺序工作流也不像状态机工作 流。基于规则的工作流或者有一个顺序的 root 活动,或者有一个基于状 态的 root 活动。 顺序工作流的理想应用是去执行业务处理。假如你需要从源中读数据,处理这些数据, 发送通知,往你的一个数据池中写入结果的话,顺序工作流或许将符合你的需求。这 并不 意 味 着 顺序工作流不适合处理依赖于用户交互的特定任务,如赞同或不同意之类的审批任务。 其实这样一些的用户交互不应成为 workflow 自身的关注焦点。 假如你需要大量的用户交互,当你的 workflow 发送通知给用户或其它系统(有各种原 因:通知、需要批复、选择一个选项等等)以使用户或其它系统进行响应(它们的响应来自 事件)时,状态机工作流可能是更好的选择。这些事件触发了 workflow 从一种处理状态转 化到另一种处理状态。我将在本章后面及 14 章(“基于状态的工作流”)更多地讨论这些。 最后一种 workflow 类型(我们将在 12 章看到)是基于规则的 workflow。这 些 workflow 基于业务规则判定是否进行转化,并判定转化后的目标是什么。这些 workflow 通常都预置 了更加复杂的剧情。 你或许会认为所有的 workflow 都能以基于规则的工作流类型来创建,但我们通常并不 总是使用这种方式进行创建。因为其它的 workflow 类型,如顺序工作流和状态机工作流, 它们能更容易地创建和测试。 要用最合适的 workflow 类型来构建你的系统。通常,在许多真实案例中你会发现你自 己使用了所有三种 workflow 类型的组合。 顺序活动 让我们进一步深入顺序复合活动吧。尽管迄今为止我们使用这些活动贯穿本书,但我在 之前有意地拖延谈论关于它的更多内容。现在我们去理解了 workflow 运行时和 workflow 实例是怎样工作的,并且知道 workflow 实例是我们正运行中的 workflow 活动的版本,我们 能更好的了解发生了什么。 执行顺序活动意味着这些活动以一个指定的顺序执行。首先要做的事最先执行,最后才 做的事最后执行。一个顺序活动就像在根据目录执行。你 需 要 记 下 首先 要 做的任务,接下来 要做的任务和最后要做的任务。假如这些任务以顺序活动的方式存储,WF 将以你指定的顺 序精准地执行每个任务。 ` 备注:本书中我们不会看到以动态的方式添加活动,但你应知道这是可以做到的。 在 Visual Studio 中,workflow 的视图设计器可帮你展示你的 workflow。当你创建一 个顺序工作流应用程序并在设计器中打开 root 活动时,你可把任务放到屏幕的最上方以便 首先被执行。那些朝向底部的任务将晚些执行。从可视化界面可看出,活动运行的顺序是从 上到下。当然顺序活动还可以是一个复合活动。 创建顺序工作流 在本书中迄今为止我们已创建过一些顺序工作流应用程序,因此这里我不再创建它们。 但我还是把完整的步骤重复一下。 建立一个顺序工作流应用程序 1.打开 Microsoft Vistual Studio 2008。 2.在文件菜单上,选择新建项目。然后将呈现新项目对话框。 3.在项目类型面板中,展开 Vistual C#树形节点,呈现出基于 workflow 项目的模板。 4.在模板面板中,点击顺序工作流控制台应用程序或顺序工作流库。前者创建一个可执 行的应用程序并以控制台的方式执行,而后者创建一个动态链接库并在其它应用程序中使 用。 5.输入你的项目或应用程序的名称。 6.输入或选择你想保存你的项目的所在路径。 7.点击确定,Visual Studio 2008 将为你建立一个基本项目,其中包含 workflow 视图 设计器用户界面。 然后,你就可方便地从工具箱中拖拽你需要的活动,调整它们的属性以符合你的需求。 假如你需要增加更多 workflow 库的项目,你可参考我前一章中的描述,或者简单地直接在 你的应用程序中增加一个新的 workflow 类。接下来我们还会看到大量的例子。 状态活动 迄今为止在本书中我们还未看到过状态机工作流。14 章完全把焦点放到基于状态的工 作流的工作上,但我在这里将介绍一些概念,我们也会快速地创建一个基于状态的工作流。 看看这样一个术语:有限状态机。我们把这个术语分成三个词:有限、状态和机器。有 限,意思是我们将进行转化的状态的数目是有限的。状态是我们的应用程序在事件发生时进 行转化的逻辑条件。机器则意味自动化。我将用一个例子来阐明。 在工程学校,或 许 会 要 求 你使用有限状态机来设计一些数字系统。例如自动售货机和洗 衣机。看看自动售货机,思考一下机器工作必须具有的步骤,以 便 它能 为你提供你需要的商 品(如汽水、糖果、点心等等)。当你投入硬币时,它会合计你投入的硬币金额,直到你投 入的硬币金额能购买商品时为止。当你选择一个商品时,它会检查存货清单。假如有货,它 就把你选择的东西分发给你。 我们可以使用有限状态机来构建自动售货机。在有限自动机的图示中,我们使用圆来表 示状态,箭头来表示状态之间的转换,转换由事件触发。图中有一个逻辑上的起点和一个或 多个逻辑上的终点。假如我们停在其它地方,我们的应用程序就被称作未指定状态或者无效 状态。我们的工作就是防止无效状态,如 我们不能免费地获取商品,我们也不应该接收超过 商品价格的多余的钱。假如自动售货机接受了钱但又未提供商品的话,用 户 毫 无 疑 问 会 暴 怒 。 假象一下简化的自动售货机,让我们画出状态和导致状态转换的事件吧。正如我提到的, 状态用圆来表示。使你的机器从一个状态变为另一个状态的事件用箭头表示。它 们 都 可 命 名 以 便 我们知道这些是什么状态和相关的转换。我们毫无疑问需要一个开始状态,如下图: 图 4-2 有限状态机起始状态符号 这个状态表示机器所处的这样一个位置:等待有人来投入一个硬币。因此看看当有人来 投入一个硬币,但它还不够买一个商品的情况,我们通过创建一个新状态来进行模拟,这个 状态叫 WaitCoins(等待硬币)状态,通过 CoinInserted(投入硬币)事件转换到该状态, 如图 4-3: 图 4-3 转换到 WaitCoins 状态 在用户投入足够金额的钱以能购买其中的商品之前,机器一直处在 WaitCoins 状态,并 接受 CoinInserted 事件,否则会触发 SufficientCoins(金额足够)事件使我们的机器转 到 WaitSelection(等待选择)状态。在这里我们的自动售货机会耐心地等待用户选择一个 商品。(在实际生活中,用户也能在任何时候要回投入的硬币,为了简单起见,本例还是不 考虑它吧。) 当用户选择商品后,商品会被分发给用户,我们的(状态)转换也就结束了。完 成 状态, 或者称作结束状态,由二个圆圈来指明,参见图 4-4。 图 4-4 尽管这个自动售货机在现实世界中或许太过于简单,但此处只是期望为你提供一个的简 要描述,使你明白状态机是如何工作的。当我们设计状态机时,我们指明其离散的状态,或 者逻辑位置来等待事件发生,然后我们指明转换机器状态的事件。有 些 事 件可让机器返回到 同一状态,如开始状态。其它事件则会在一个新的事件被处理后使机器转换到一个新的状态。 没有事件被触发就没有状态的转换,理想情况下应没有无法预料的事件或异常。 这种模型和我们用过的顺序工作流模型有很大的不同。在 顺序工作流里,活动以指定的 顺序依次执行。一 旦 在 该 顺序链上的一个活动执行完它的任务,在 该 链 上的下一个活动就开 始执行它的工作。在 workflow 处理中或许有事件参与其中,但它们在 workflow 任务的处理 (如定时器事件)上相对简单。 但状态机会花费大量时间在等待上。它 们 等待事件,依赖事件来使它们的状态进行转换。 状态自身不会激发事件(尽管它们可能会调用外部代码)。它们就是事件处理,因此它们会 耐心地等待它们需要的事件来进行状态的转换。依靠事件,它完全可能从一个状态切换到任 何一个离散的不同的状态。假如我们的自动售货机处在 WaitCoins 状态,当接受一个 CoinInserted 事件、RefundRequested 事件或 ImminentPowerdown 事件时会分别做不同的事 情。但我并未在图 4-4 中这个经过简化的模型里画出这些事件,但我相信你能看懂不同的事 件是怎样驱动你的有限状态机转换到不同状态的。 在 WF 里,基于状态的 workflow 内的个别状态由 State 活动创建。State 活动是一个复 合(组合)活动,但它对容纳的子活动有限制。你将在 14 章学习到基于状态的 workflow 的更多东西。 备注:正如顺序工作流使用一个特别的 Sequence 活动来容纳整个工作流一样,基于状 态的工作流也有一个特别的 root 活动做这事,这 就 是 StateMachineWorkflow 活动,它是一 个特别的 State 活动。特别之处是它是必须的,这样当初始化执行时 root 活动就能接受初 始化参数。 创建一个状态机工作流应用程序 怎样创建一个基于状态的 workflow 呢?创建一个基于状态的工作流和创建一个顺序工 作流一样容易。我们现在就来看看怎样创建一个状态机工作流。然而我们现在不会添加任何 代码——虽然本书后面有很多时候需我们这样去做,但我们现在只要需要了解怎样创建一个 状态机工作流就行了。 创建一个状态机工作流应用 1.启动 Microsoft Visual Studio 2008。 2.在文件菜单上,选择新建一个项目,这将打开新建项目对话框。 3.展开项目类型面板中的 Visual C#节点,这将显示所有使用 C#语言的项目类型。 4.在 Visual C#节点下点击 Workflow 节点,这将显示所有基于工作流的项目模板。 5.在模板面板内,点击状态机工作流控制台应用程序或状态机工作流库。如下图所示: 6.输入程序或项目的名称。 7.输入或选择项目文件要保存的位置。 8.点击确定。Visual Studio 2008 将为你创建一个包含 workflow 视图设计器用户界面 的项目,如下图: 我们需要去触发引发工作流改变状态的事件,因此,我们需理解 workflow 实例是怎样 和它们的宿主应用程序进行通信的。我们将在第八章(“调用外部方法”)看到宿主和 workflow 之间的通信,在第十章(“事件活动”)中我们将学习状态机工作流的事件驱动。 第五章:workflow 跟踪 学习完本章,你将掌握: 1.workflow 的可选服务 2.创建一个事件跟踪数据库 3.激活事件跟踪服务 4.创建一个自定义跟踪 5.查看你的 workflow 的跟踪信息 目前为止,我们看过 workflow 的一些基本对象。我们通过活动创建 workflow 任务,它 们 在 执 行 时由 WorkflowInstance 对象进行管理。workflow 实例由 WorkflowRuntime 编入队 列并进行控制。但 WF 不只是为我们提供了这些对象,它也为我们提供了一些服务来和这些 对象一起协同工作。 可插拔(可选)服务 工作流服务是一些附加的软件库,你的工作流能使用它来完成它们的的任务。有 些 服务 是非必须可选的,如本章介绍的跟踪服务。而其它的服务需要你的工作流必须执行它。 一个可插拔服务是这样一个服务,它 能 像 照菜单点菜一样被选中以执行特定任务。例如, 有管理线程的服务、跟踪的服务、事务服务等等。你可选择那些适合你的工作流的服务。你 甚至还能自己进行创建。 哪这些服务看起来像什么?他们能为我们做什么?表 5-1 列出了可获取的基本服务,它 很 好地为你描述了这些可获取的服务的概念,并告诉你他们能做什么。 当中的大部分服务我们不会直接使用。我们普遍的用法是使用从这些基本服务派生出的 服务。 表 5-1 基本工作流服务 服务 功能 WorkflowPersistenceService 抽象基类,派生出所有持久化的服务。 WorkflowQueuingService 该基类为你提供了一些方法,使你能用来管理 和一个工作流实例相关的工作流队列。 WorkflowRuntimeService 抽象基类,派生出工作流运行时的内核服务。 WorkflowScheddulerService 所有在工作流运行时宿主上创建线程以运行 工作流实例的类的基类。 WorkflowSubscriptionService 那些管理订阅(Subscriptions)工作流运行 时类的基类。 WorkflowTransactionService 所有事务服务的基类。 TrackingService 一个抽象基类,在 跟踪服务和运行时跟踪基础 结构(infrastructure)间提供了基本的接口。 请记住这些是基类。我们使用的服务实际上从它们派生。例如,当我们运行一个工作流 实例时,有时需为实例创建一个线程去使用。DefaultWorkflowSchedulerService 正是做这 个工作的,它使用 WorkflowSchedulerService 作为它的基类。但假如你想自己提供这个线 程,你可使用 ManualWorkflowSchedulerService 代替。在本章中我们将看到由 SqlTrackingService 提供的跟踪服务,它使用了 TrackingService 作为它的基类。 “可插拔(可选)”一词部分来源于下面的情况:你可能考虑在任何时间上你都可能需 要使用一个调度程序服务,运行时服务,入队和订阅(定时器)服务。但你还能在工作中进 一步添加持久化和跟踪服务,以及外部数据通信服务。 工作流跟踪 在本章,我们将把重点放到跟踪服务上。其它服务将在其它章节进行介绍。WF 由一个 主要的跟踪服务——SqlTrackingService 承载。但是假如你需要的话,也有两个额外的服 务可用。它们是 ConsoleTrackingService 和 SimpleFileTrackingService,这二个服务允 许你把跟踪信息写到控制台窗口或者文件中而不是 Microsoft SQL Server 数据库。在这里 我们不会使用这两种服务,但你需要的话你可使用它们。 使用 SqlTrackingService 进行工作流事件跟踪 通过添加一个跟踪服务(通常是 SqlTrackingService)到工作流运行时中,你可跟踪 你的工作流的处理过程。假如你有特定的跟踪需求,你 也能创建你自定义的跟踪事件。假如 捕获的事件为你提供了过多的跟踪数据,你也能创建跟踪配置文件来过滤这些跟踪数据。 当跟踪的事件激发时,WF 创建并管理跟踪记录。尽管你不用做这些工作,但你还是能 容易地从 WF 中直接访问这些跟踪记录。你要知道这些信息也被记录到数据库中,因此直接 从数据库中检索这些信息也是可能的。通常都在记录这些跟踪信息后的某个时间,使用一个 象 WorkflowMonitor 或你自己设计的工具之类的外部跟踪监控工具,来查询这些跟踪信息。 表 5-2 列出了在你的 WF 事件跟踪中经常使用的对象,在本章我们将使用其中的一些。 假如你需要自定义你的工作流事件跟踪,那你应知道 WF 为你提供了一个和跟踪相关对象的 强大类库。 表 5-2 事件跟踪对象 对象 功能 指定要从活动中提取并在跟踪点匹配时与关 联的批注集合一起发送到跟踪服务的属性或 字段。 ActivityTrackingCondition 表示一个条件,该条件通过使用指定的比较运 算符将活动成员的值与指定值进行比较。 ActivityTrackingLocation 定义与根工作流实例的可能执行路径中的某 个活动状态事件相对应的活动限定位置。 ActivityTrackingRecord 包含运行库跟踪基础结构在 ActivityTrackPoint 匹配时发送到跟踪服务 的数据。它 还 用在 ActivityEvents 属性的返 回列表中。 ActivityTrackPoint 定义工作流实例的可能执行路径中要跟踪的 点,该点与活动执行状态更改关联。 SqlTrackingQuery 包含用于管理跟踪数据查询的方法和属性,跟 踪数据包含在 SqlTrackingService 使用的 SQL 数据库中。 SqlTrackingQueryOptions 包含一些属性,这些属性用于约束 SqlTrackingQuery.GetWorkflows 调用所返 回 SqlTrackingWorkflowInstance 对象的集 合。 SqlTrackingWorkflowInstance 通过工作流实例的 SqlTrackingService 提 供对 SQL 数据库中保留的跟踪数据的访问 TrackingProfile 定义根工作流实例的可能执行路径中的关注 点,应将有关该关注点的信息通知跟踪服务。 它过滤跟踪事件,并把过滤后的跟踪记录返回 给某个跟踪服务。这 里 有 三 种类型的跟踪事件 能被过滤:活动状态事件、工作流状态事件和 用户事件。 UserTrackingLocation 定义与根工作流实例的可能执行路径中的某 个用户事件相对应的活动限定位置。 UserTrackingRecord 包含运行库跟踪基础结构在 UserTrackPoint 匹配时发送到跟踪服务的数据。 UserTrackPoint 定义一个要跟踪的点(与用户事件关联),该 点位于根工作流实例的可能执行路径中。 WorkflowDataTrackingExtract 指定要从工作流的根活动中提取,并在跟踪点 匹配时随关联的批注集合一起发送到跟踪服 务的属性或字段。 WorkflowTrackingLocation 定义对发生在根工作流实例中的特定工作流 事件的关注;用于按跟踪配置文件中的 WorkflowTrackPoint 进行匹配。 WorkflowTrackingRecord 包含运行时跟踪基础结构在匹配了 WorkflowTrackPoint 时发送到跟踪服务的数 据。它 还 用在 WorkflowEvents 属性的返回列 表中。 WorkflowTrackPoint 定义一个与一组工作流状态事件关联的点,这 些 事 件 在 根 工作流实例的可能执行路径中进 行跟踪。 这些对象可考虑归为两个大类:跟踪数据检索和跟踪详细说明。跟踪检索对象,如 SqlTrackingQuery,一 旦跟踪数据被存储到数据库中,你 可 使用它们采集跟踪数据。跟踪详 细说明对象,如跟踪点和位置对象,允许你能在工作流代码中控制该跟踪什么。 像跟踪点和位置对象之类的跟踪详细说明对象还可被归为三大组:活动事件、工作流事 件和用户事件。和活动相关的跟踪对象,如 ActivityTrackingPoint 或 ActivityTrackingLocation,用 来 记 录 相关联的活动的事件信息并保存到跟踪数据库中。这 些 事 件 包含如下这些:活动取消、未处理的异常和执行的事件。工作流事件跟踪对象的工作 方式和工作流相关的事件的工作方式相像(但工作流启动和停止,实例 的 创建、空闲和完成 及其它相似的相关联的事件除外)。最后是用户事件跟踪,它用在自定义你特有的工作流跟 踪需求中指定你的工作流并完全依赖于你的工作流想怎样进行跟踪。在本章中当我们学习跟 踪配置文件时会看到它们中的几个。 跟踪记录通过批注加以装饰。批注是一些保存跟踪记录并被记录进跟踪数据库的字符 串。关联活动和关联工作流的跟踪记录有一个创建好的批注的集合,但你可以为用户关联事 件的跟踪记录提供一个额外的批注。 在 WF 中跟踪这一术语和平常“跟踪”的概念没有什么不同。平常意义上的“跟踪”是 一个有用的调试工具,在 ASP.NET、像 WPF 之类的.NET 技术及 Windows Forms 中都支持跟踪 调试的能力。跟踪允许你过滤跟踪信息记录以满足你的需要,你既可只看异常的跟踪信息, 也能看到整个跟踪栈。 WF 跟踪基于相似的概念,事实上是过滤。正如你可能想到的,关联活动事件和关联工 作流事件将产生所有类型的跟踪记录,你或许能从中找到感兴趣的记录(如未处理的异常或 空闲状态),你可以决定其它的事件不用进行跟踪。 为过滤掉你不想进行跟踪的事件,你 要创建一个跟踪配置文件。一个跟踪配置文件是一 个 XML 文档,它指 明 了 跟踪的对象和要排除跟踪的对象。和 跟踪不同,跟踪配置文件指明哪 些东西要写入跟踪数据库,而不是指明以后哪些东西能被查看到。假如你排除了一些事件, 这些排除的事件就不会向数据库里写任何东西。和 跟踪的另一个不同之处是,跟踪配置文件 的 XML 文档也被记录进跟踪数据库,当执行工作流时被恢复。换句话说,跟踪记录了指定的 要去跟踪的任何东西,但不进行跟踪信息归类。 设置 SQL Server 进行跟踪 尽管你可创建自定义的跟踪服务来把跟踪数据记录进各种存储中(如消息队列或数据文 件),但本章,我们将把注意力放到 SQL Server 2005 数据库上,WF 有能力把事件数据记 录到 SQL Server 2005 数据库中。WF 为使用 SQL Server 2005 提供了内置的创建支持。 我们先在 SQL Server Management Studio(或者 Express 版本)中创建一个新的数据 库。然后需运行一些由 WinFX 组件提供的 SQL 脚本,这 些 脚 本将创建数据库角色、表 和 视图、 必须的存储过程以和你的工作流进行交互。我们就来通过创建一个新数据库并运行一些准备 好的脚本来开始吧,然后我们将使用 WF 跟踪服务记录下跟踪数据并写入数据库。 备注:我在下面的步骤中使用 SQL Server Express,但这些步骤对于其它版本的 SQL Server 2005 同样适用。 创建一个 SQL Server 2005 跟踪数据库 1.启动 SQL Server Management Studio,连接数据库引擎。 2.在数据库节点上单击右键激活右键快捷菜单,选择“新数据库”。 3.在新数据库对话框中输入“WorkflowTracking”作为数据库的名称字段,点击确定。 4.下一步将执行 WF 为设置跟踪所提供的脚本(这会创建表、视图以及工作流跟踪的角 色)。这些脚本的位置在<%WINDIR%>\Microsoft.NET\Framework\3.0\Windows Workflow Foundation\SQL\ZH-CHS,在这里<%WINDIR%>是指你的 Windows 目录(通常是 C:\Widows)。 在 SQL Server Management Studio 打开 Tracking_Schema.sql 文件。 5.SQL Server Management Studio 会在一个新窗口中导入文件中的脚本,但在我们运 行脚本前,我们需指明在哪个数据库中运行这些脚本,因此我们要选择 WorkflowTracking 数据库。 6.点击工具栏上的执行按钮执行这些脚本。 7.重复 4-6 步执行 Tracking_Logic.sql 脚本。这将在数据库中创建必须的存储过程。 我们现在就创建了一个将记录跟踪信息的数据库,但怎样得到已记录的信息呢?什么组件进 行这方面的工作呢?让我们看看! 使用 SqlTrackingServer 服务 在工作流跟踪数据库设置好后,现在就是实际使用它的时候了。我们先创建一个新的工 作流并看看我们怎样去跟踪事件。我们将创建一个稍微复杂一些的工作流,里面有几个事件 可以提供给我们去进行跟踪。在 我们创建一个原始的工作流后,我们将增加必要的跟踪代码。 创建一个新工作流并进行跟踪 1.为更方便些,我已创建了两个版本的样例应用程序。Workflow 包含两个不同版本的 应用程序:一个是不完全版本,一个是完全版本。完全版本已完全编写完成并可直接运行, 非完全版本可方便你进行修改,并按步骤完成相应练习。你 可通过本章后面的下载链接下载 这些项目文件。 2.下载本章源代码,打开 TrackedWorkflow 解决方案,像第三章中相应步骤一样创建一 个顺序工作流库的项目,名称为 TrackedWorkflow。 3.在你完成以上步骤后,Visual Studio 会打开工作流设计器以便进行编辑。 4.从工具箱中拖动一个 IfElse 活动到设计器界面上。如下图: 5.单击左边的 ifElseBranchActivity1 分支,激活它的属性使其在 Visual Studio 中的 属性窗口中显示。 6.寻找 ifElseBranchActivity1 的 Condition 属性。点击下拉列表框上向下的箭头打 开下拉列表框,选择其中的代码条件节点。如下图: 7.Condition 属性现在会在它的左边呈现出一个“ + ”号 。单击这个+号展开其属性网格, 这会暴露出 Condition 属性的 Condition 名称字段。在编辑框中,输入 QueryDelay。我们 将使用这个方法来决定我们将执行 IfElse 活动的那个分支。 8.下一步我们在左边的分支(这个分支在条件值为 True 时执行)添 加一些活动。首先, 从工具箱中拖拽一个 Code 活动到 IfElse 的左边分支即 ifElseBranchActivity1 上。 9.你看到的惊叹号标记的意思在前面的章节我已描述过,意思是我们还有工作要做。在 这 里 ,它指出我们需添加一个方法,Code 活动添加到工作流中执行时将调用这个方法。在 Visual Studio 的属性面板上,定位到 ExecuteCode 属性,在该编辑框中输入 PreDelayMessage。 10.也许你要看看我要做什么……其实就是添加一个延时工作流,在第三章我们已经创 建过。就像在第三章做的一样,再拖拽一个 Delay 活动和另一个 Code 活动进 ifElseBranchActivity1 中,然后设置它们的属性。Delay 活动延时 10 秒(00:00:10),第 二个 Code 活动执行一个名称为 PostDelayMessage 的方法。完 成 这 些 步骤后的设计器界面如 下图所示: 11.在设计器中的工作完成后,我们就来添加相应代码。在解决方案资源管理器中的 Workflow1.cs 文件上单击右键,选择查看代码。然后在项目中添加对 System.Windows.Forms 的引用,然后在 Workflow1.cs 文件的顶部声明和其对应的下面的名称空间。 using System.Windows.Forms; 12.你查看这个文件,你会看到 Visual Studio 为你添加的作为活动属性的三个事件处 理程序:PreDelayMessage、PostDelayMessage 和 QueryDelay。和第三章类似,在 Code 活 动中添加消息对话框,以使应用程序能在工作流执行时通知你。对于 PreDelayMessage,添 加下面的代码: MessageBox.Show("Pre-delay code is being executed."); 对于 PostDelayMessage,添加下面的代码: MessageBox.Show("Post-delay code is being executed."); 13.我们些许更感兴趣的是在 QueryDelay 中添加的以下代码: e.Result = false; // 假定我们不延时 if (MessageBox.Show("Okay to execute delay in workflow processing?", "Query Delay", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { // 需进行延时处理 e.Result = true; // 显示消息 Console.WriteLine("Delay path taken "); } // if else { // 显示消息 Console.WriteLine("Delay path NOT taken "); } // else 14.完成上述步骤后,我们需要为我们的 WorkflowTracker 主应用程序添加对工作流项 目 TrackedWorkflow 的项目引用,步骤略。 15.在 WorkflowTracker 项目中打开 Program.cs 文件,查找下面的代码: Console.WriteLine("Waiting for workflow completion."); 16.为创建一个 Workflow 实例,在上述代码下添加下面的代码: // 创建工作流实例。 WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(TrackedWorkflow.Workflow1)); // 启动工作流实例。 instance.Start(); 17.编译解决方案,纠正任何编译错误。 18.按下 F5(或 Ctrl+F5)执行这个应用程序,你将看到以下控制台的输出结果: 我们现在就有了一个基本的工作流,我们可使用它去体验 WF 的跟踪能力。我们现在就 回去添加我们需要的代码以执行跟踪。 为我们的工作流添加 SqlTrackingService 1.WF 由活动和工作流事件跟踪能力承载,因此我们不需为跟踪事件做太多工作。尽管 如此,我们仍然需在主程序文件中添加一些逻辑。首先,要为 WorkflowTracker 应用程序添 加 System.Configuration 引用,我们需要它来把访问数据库的连接字符串存储到应用程序 的配置文件里。 2.下一步,为 WorkflowTracker 应用程序添加一个应用程序配置文件。方法是在 Visual Studio 的解决方案管理器中的 WorkflowTracker 树节点上单击右键,依次选择添加、新建 项。在 呈 现 的 添加 新 项对话框中选择应用程序配置文件,点击确定。这 就为 我们的应用程序 添加了一个新的 app.config 文件。参见下图: 3.打开 app.config 文件,在 Configuration 的开始标记和结束标记间插入下面的内容: 备注:上面的连接字符串可能和你实际应用中有所不同,你需要灵活进行配置。 4.点击 WorkflowTracker 项目中的 WorkflowFactory.cs 文件,查看其代码。 5.在该文件中声明以下名称空间(需添加 System.Configuration 引用): using System.Workflow.Runtime.Tracking; using System.Configuration; 6.在 WorkflowFactory.cs 文件中,找到我们创建 WorkflowRuntime 实例的地方,在这 里我们需要为 WorkflowRuntime 引入 SqlTrackingService。在 GetWorkflowRuntime 方法中 添加下面的代码: String conn = ConfigurationManager.ConnectionStrings["TrackingDatabase"].Connec tionString; _workflowRuntime.AddService(new SqlTrackingService(conn)); 完成了上述步骤,我们就添加了实际中要去执行跟踪的代码(稍后,我们会添加更多的 代码来显示跟踪结果)。编译该解决方案,然后按 F5 或 Ctrl+F5 执行它。 备注:假如程序中出现 ArgumentException 异常,最可能的原因是运行时没有访问数 据库的权限。 假如工作流运行正常,你 可 在 WorkflowTracking 数据库的 ActivityInstance 表中看到 下图 5-1 中显示的结果。 图 5-1 表 ActivityInstance 中的记录 检索来自于工作流的跟踪记录 1.打开 WorkflowTracker 项目中的 Program.cs 文件。 2.在文件中声明以下名称空间: using System.Configuration; using System.Workflow.Runtime.Tracking; 3.在 Main 方法中,找到下面的代码: waitHandle.WaitOne(); 4.在上面的代码下添加以下的的代码: ShowWorkflowTrackingEvents(instance.InstanceId); ShowActivityTrackingEvents(instance.InstanceId); 5.上面我们调用的一组方法并不存在,我们需要添加它们。在 Program 类中添加这些 方法: // The activity tracking record display method static void ShowActivityTrackingEvents(Guid instanceId) { SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery(ConfigurationMan ager.ConnectionStrings["TrackingDatabase"].ConnectionString); SqlTrackingWorkflowInstance sqlTrackingWorkflowInstance = null; sqlTrackingQuery.TryGetWorkflow(instanceId, out sqlTrackingWorkflowInstan ce); if (sqlTrackingWorkflowInstance != null) { Console.WriteLine("\nActivity Tracking Events:\n"); Console.WriteLine(" Status :: Date/Time :: Qualified ID"); foreach (ActivityTrackingRecord atr in sqlTrackingWorkflowInstance.Ac tivityEvents) { Console.WriteLine(" {0} :: {1} :: {2}", atr.ExecutionStatus, atr .EventDateTime, atr.QualifiedName); } // foreach } // if } // The workflow instance tracking record display method static void ShowWorkflowTrackingEvents(Guid instanceId) { SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery(ConfigurationMan ager.ConnectionStrings["TrackingDatabase"].ConnectionString); SqlTrackingWorkflowInstance sqlTrackingWorkflowInstance = null; sqlTrackingQuery.TryGetWorkflow(instanceId, out sqlTrackingWorkflowInstan ce); if (sqlTrackingWorkflowInstance != null) { Console.WriteLine("\nWorkflow Instance Events:\n"); Console.WriteLine(" Description :: Date/Time"); foreach (WorkflowTrackingRecord workflowTrackingRecord in sqlTracking WorkflowInstance.WorkflowEvents) { Console.WriteLine(" {0} :: {1}", workflowTrackingRecord.Tracking WorkflowEvent, workflowTrackingRecord.EventDateTime); } // foreach } } 在最后一步中忽然冒出大量的代码,但实际上并不太复杂。我们首先创建了一个 SqlTrackingQuery 的实例,为它提供了我们曾提供给 SqlTrackingService 的相同的连接字 符串。然后我们通过当前工作流实例的 ID(一个 Guid)标识,从数据库中查询该实例的跟踪 信息。该查询由 SqlTrackingService.TryGetWorkflow 执行。假如数据库中有我们指定的工 作流的跟踪信息,我们循环获取跟踪记录(查询返回给我们的是一个 workflowTrackingRecord 对象的集合),从中提取我们感兴趣的信息。假如查询结果中没 有记录,也就没有跟踪信息写到控制台窗口中。最终的屏幕的输出结果如图 5-2 所示(在调 试模式下运行代码的话,你或许需要设置一个断点才能看到下图的输出结果)。 跟踪用户事件 SqlTrackingService 是 WF 的一部分,它具有跟踪事件的能力。也就是说,它能跟踪活 动和工作流激发的标准事件。但由你生成的事件呢?我们又如何跟踪它们呢? Activity 活动支持一个名叫 TrackData 的方法,TrackData 有两个重载版本:一个版本 接受一个要存储进跟踪数据库中的对象,另一个版本接受一个字符串类型的键及一个要存储 进跟踪数据库中的对象。 假如你执行 TrackData 并为跟踪传入通常是字符串类型的数据,那这些信息将作为用户 事件数据存入跟踪数据库。 检索来自你的工作流的跟踪记录 1.打开 WorkflowTracker 项目中的 Workflow1.cs 文件。 2.找到我们在创建工作流时添加的 PreDelayMessage 方法和 PostDelayMessage 方法。 3.在名为 PreDelayMessage 的方法内的显示信息对话框的代码下面添加以下代码: this.TrackData("Delay commencing"); 4.同样,在名为 PostDelayMessage 的方法内的显示信息对话框的代码下面添加以下代 码: this.TrackData("Delay completed"); 5.编译并执行。 现在打开 WorkflowTracking 数据库中的 UserEvent 表,里面有两行,我们在工作流中 每调用 TrackData 一次就产生一条记录,表中部分内容如图 5-3 所示。 图 5-3 UserEvent 表中显示的调用 TrackData 的结果 创建自定义跟踪配置文件 在本章我已谈到过跟踪配置文件,但当时并未详细深入,在这节我将深入了解它的细节。 你可回忆一下,跟踪配置文件用来限制 WF 跟踪架构将存储到跟踪数据库中的信息数量。 跟踪配置文件不仅仅是一个 XML 文档,也用来规定一个给定的工作流的跟踪将包含和排除的 东西。但在代码中完成这些事(比手动添加一个 XML 的跟踪配置文件)更加容易。这里有一 个 TrackingProfile 对象及在表 5-2 中看到的其余对象可用,它们用来创建这个 XML 文档。 有了这个 TrackingProfile对象,你或许会自然的想到这也是一个有用的 XML序列化器, 可用来把 TrackingProfile 对象转换成你需要的 XML 文档并存入数据库,事实上,这是 TrackingProfileSerializer。WF 并不内在支持把 XML 信息写入数据库,但你可使用类型化 的 ADO.NET 技术及跟踪数据库中提供的存储过程容易地来完成这一工作。 假如你回去看看表 5-2,你会找到一些带有“location”和“point”名称的对象,它 们分别和 activity、workflow 和 user 事件相对应。我们在这时谈到“location”和“ point” 究竟意味着什么呢? 在本章我已谈到过跟踪配置文件,但当时并未详细深入,在这节我将深入了解它的细节。 其实,“ location”指在你的工作流中活动、工作流或用户相关的事件发生时的一个指 定的位置。Locations 描述了要跟踪的事件。使用一个 location 对象,你可更精确地指定 你想跟踪及排除的的事件。 跟踪 points 收集 locations,它 们 能 在你的工作流中的一个或多个地方触发跟踪信息。 你可把跟踪点当成一个感兴趣的点来考虑。它 能 在你的工作流代码中跨越不同的位置。在你 为跟踪点指定条件和位置后,在跟踪时它视情况可能触发也可能不触发。 为什么谈及所有这些呢?因为当我们建立一个跟踪配置文件时,你 真 正 要 做的工作是把 跟踪点和位置添加到 profile 对象中,以作为跟踪事件的过滤器去使用。 创建一个新的跟踪配置文件 1.打开 WorkflowTracker 项目中的 Program.cs 文件。 2.我们将加入的代码并不一定就难于理解,只是它相当大。首先有必要声明以下一些名 称空间: using System.Workflow.ComponentModel; using System.Data; using System.Data.SqlClient; using System.Globalization; using System.IO; 3.上述步骤后,我们去找到 Main 方法。在 该 方 法 中 创建工作流的代码前添加以下代码: TrackingProfile profile = CreateProfile(); StoreProfile(profile, ConfigurationManager.ConnectionStrings["TrackingDatabase" ].ConnectionString); 4.我们在上面的代码中调用了两个方法,我们还需要在 Program 类中添加它们。在 Program 类中添加 CreateProfile 方法的代码如下: static TrackingProfile CreateProfile() { // Create the basic profile TrackingProfile profile = new TrackingProfile(); // Create the activity location, meaning the events we're interested in ActivityTrackingLocation actLoc = new ActivityTrackingLocation(typeof(Act ivity)); actLoc.MatchDerivedTypes = true; actLoc.ExecutionStatusEvents.Add(ActivityExecutionStatus.Executing); // Create the activity track point and add the location we just created ActivityTrackPoint actPt = new ActivityTrackPoint(); actPt.MatchingLocations.Add(actLoc); profile.ActivityTrackPoints.Add(actPt); // Create the workflow location WorkflowTrackingLocation wfLoc = new WorkflowTrackingLocation(); wfLoc.Events.Add(TrackingWorkflowEvent.Started); wfLoc.Events.Add(TrackingWorkflowEvent.Idle); // Create the workflow track point WorkflowTrackPoint wfPt = new WorkflowTrackPoint(); wfPt.MatchingLocation = wfLoc; profile.WorkflowTrackPoints.Add(wfPt); // Set the version of the profile this version must not already exist // in the database. profile.Version = new Version("1.0.0.0"); return profile; } 5.同样,添加 StoreProfile 方法: static void StoreProfile(TrackingProfile profile, string connString) { // First, serialize the profile into an XML string TrackingProfileSerializer serializer = new TrackingProfileSerializer(); StringWriter writer = new StringWriter(new StringBuilder(), CultureInfo.I nvariantCulture); serializer.Serialize(writer, profile); // Then, write the XML string to the database SqlConnection conn = null; try { if (!String.IsNullOrEmpty(connString)) { // Create a connection object conn = new SqlConnection(connString); // Create a dummy for the stored proc name string storedProc = "dbo.UpdateTrackingProfile"; // Create the command SqlCommand cmd = new SqlCommand(storedProc, conn); cmd.CommandType = CommandType.StoredProcedure; // Add the parameters SqlParameter parm = new SqlParameter("@TypeFullName", SqlDbType.N VarChar, 128); parm.Direction = ParameterDirection.Input; parm.Value = typeof(TrackedWorkflow.Workflow1).ToString(); cmd.Parameters.Add(parm); parm = new SqlParameter("@AssemblyFullName", SqlDbType.NVarChar, 256); parm.Direction = ParameterDirection.Input; parm.Value = typeof(TrackedWorkflow.Workflow1).Assembly.FullName; cmd.Parameters.Add(parm); parm = new SqlParameter("@Version", SqlDbType.VarChar, 32); parm.Direction = ParameterDirection.Input; parm.Value = "1.0.0.0"; cmd.Parameters.Add(parm); parm = new SqlParameter("@TrackingProfileXml", SqlDbType.NText); parm.Direction = ParameterDirection.Input; parm.Value = writer.ToString(); cmd.Parameters.Add(parm); // Open the connection conn.Open(); // Write the XML data cmd.ExecuteNonQuery(); } // if } // try catch (Exception ex) { // If the exception is telling us we've already written // this profile to the database, just pop up an informational // message. if (ex is SqlException) { // Check to see if it's a version error if (ex.Message.Substring(0,24) == "A version already exists") { // Version already exists Console.WriteLine("NOTE: a profile with the same version alre ady exists in the database."); } // if else { // Write error message Console.WriteLine("Error writing profile to database: {0}", e x.ToString()); } // else } // if else { // Write error message Console.WriteLine("Error writing profile to database: {0}", ex.To String()); } // else } // catch finally { // Close the connection if (conn != null) { conn.Close(); } // if } // finally } 6.假如你在现在执行本程序,CreateProfile 方法会创建的配置文件并把它写入数据 库。假如你再仔细地看看步骤 4 中的代码,你 会 注 意 到 仅仅只跟踪了很少数的活动事件和工 作流事件。因此,你可能会期望在工作台窗口中将由 ShowActivityTrackingEvents 和 ShowWorkflowTrackingEvents 输出很少的几行信息,但实际上,正确的结果如下图 5-4(可 把它和图 5-2 比较)。 图 5-4 WorkflowTracker 跟踪数据的屏幕输出结果 CreateProfile 方法创建了一个新的 TrackingProfile 并添加了一个活动跟踪点和工作 流跟踪点。每个跟踪点都有一个单一的跟踪位置,它定 义 了 要 跟踪哪些事件,因此我们只能 看到来自活动的 Executing 事件和来自工作流实例的 Started 事件和 Idle 事件。 而 StoreProfile 方法,它把跟踪配置文件序列化成 XML 形式,然后用典型的 ADO.NET 技术把这个 XML 存入跟踪数据库。试 图更新一个跟踪配置文件的同一版本会被认为是一种错 误,因此会抛出一个异常。 用 WorkflowMonitor 查看跟踪信息 假如有人想出一个现成的用来监测工作流事件的工具那不是很好?就像我们本章前面 一样,能把跟踪记录输出是很棒的事,但用一个好的图形用户界面来做这个工作将是更加棒 的一件事。事实上,我们是幸运的!当我们加载 WF 时,你也可加载了一套示例,里面包含 的是一个叫做 WorkflowMonitor 的应用程序。在这里我们需要去做的是编译这个应用程序。 编译 WorkflowMonitor 1.WorkflowMonitor 是工作流示例库的一部分,它 由 Windows SDK 承载。把 WFSample.zip 文件复制到本章解决方案的目录下并进行解压。WFSamples.zip 文件在下面的位置可找到 (声明:本 人未在下面的目录找到该文件,但从微软官方网站上可进行下载,本章的源代码 中也提供有 WorkflowMonitor 的源代码): C:\Program Files\Microsoft SDKs\Windows\v6.0\Samples\WFSamples.zip 2.在 Visual Studio 中打开 WorkflowMonitor.sln 文件。 3.编译并生成该应用程序。 这个应用程序编译时没有错误,然后你却不能执行它。当 SqlTrackingService 把跟踪 记录写进跟踪数据库时,工作流对象的数据类型就是记录的一批信息中的一条。假如支持你 的工作流的类型没有在全局 Assembly Cache 中,WorkflowMonitor 就不能在视图设计器中 加载你的工作流对象。因此,对于 TrackedWorkflow 来说,你 必须把你的工作流组件放到全 局 Assembly Cache 中或者把 TrackedWorkflow 中的 DLL 文件放到和 WorkflowMonitor 的可 执行文件(即 WorkflowMonitor.exe)的相同的目录下。在本例中,更容易的方法是复制 WorkflowMonitor.exe 文件到我们的工作流的可执行代码的相同目录下。 执行 WorkflowMonitor 1.复制 WorkflowMonitor.exe 可执行文件到 WorkflowTracker 解决方案目录中的 bin\Debug 子目录下(在此生成的是调试版本的应用程序)。 2.双击 WorkflowMonitor.exe 文件执行该应用程序。 3.WorkflowMonitor 把配置信息存储在 WorkflowMonitor.config 配置文件中,可在 “Application.LocalUserAppDataPath”找到。(假如你正运行 SQL Server Express,当 简单地单击确定,在 WorkflowMonitor 试图连接该数据库时你可能会看到一条错误信息。) 因为这可能是在你的系统上运行 WorkflowMonitor 的第一时间,配置文件还未存在。 WorkflowMonitor 已考虑这些并立即显示一个设置对话框。如下图所示: 4.你可通过这个设置来修改跟踪数据库所在的主机名、跟踪数据库名、轮询周期(默认 是 5 秒)等。现在,我们真正要做的是设置服务器的名称和数据库的名称,在你输入这些值 后点击确定。 5.然后 WorkflowMonitor 监控器建立一个工作数据库的链接,并读出找到的跟踪记录。 假如记录中有类型信息,它将在设计器中显示找到的工作流。在本例中,唯一能找到的工作 流是 TrackedWorkflow,但在你创建的工作流越多,显示的也将越多。WorkflowMonitor 程 序的用户界面如下图所示: 第六章:加载和卸载实例 学习完本章,你将掌握: 1.理解工作流实例为什么要卸载和重新加载及其时机 2.理解工作流实例为什么要持久化及其时机 3.搭建 SQL Server 2005,使其为 WF 和工作流持久化提供支持 4.使用 SqlWorkflowPersistenceService 服务 5.在你的工作流代码中进行实例的加载和卸载 6.使持久化服务能自动地加载工作流实例及卸载空闲中的工作流实例 假如你花点时间真正考虑该怎样使用 WF 和工作流在你的应用程序中进行处理的话,你 或许想像的许多解决方案中都包含那些需长时间运行的处理过程。毕竟,商业软件本质上就 是模拟和执行业务处理过程,这 些许多的处理过程中都包含人或厂商、订货和发货、计划安 排等等。人没有在几毫秒内自动进行处理的响应能力,但在已加载的企业服务器上则能做到 点。服务器是宝贵、繁忙的资源,需让它进行线程的轮转,让线程等待几分钟、几小时甚至 几天、几周是不能接受的,原因有很多。 因此 WF 的设计器必须提供一个机制,当等待一些长期运行的任务时,空闲工作流应暂 时脱机。WF 决定提供 Microsoft SQL Server 作为一个可选的存储介质,因为数据库是储存 (而不是失去)宝贵数据的好地方。WF 也集成了另一个可插拔服务,我们可轻易地把它纳 入我们的工作流中以支持其持久化机制。怎么做到这些、为什么做、什么时候做是我们在本 章中将探讨的问题。 持久化工作流实例 你知道现代 Microsoft Windows 操作系统本质上是一个非常特别的批处理软件,它 负责 为各请求线程分配占用处理器的时间吗?假如一个单一的线程独占了处理器一个过份的时 间周期,其它线程会“饿死”,系统将会死锁。因此这个批处理软件,也就是任务调度程序, 要把线程从处理器的执行堆栈上移进和移除,以便所有的线程都能得到执行时间。 从某个意义上说,工作流也类似。假如你有许许多多长时间运行的工作流,它 们 都挂 到 某 一特定的计算机上竞争处理时间和资源的话,那么系统最终会阻塞未处理的工作流。这 就 没有了可伸缩性。事实上,WF 在维护它的工作流队列时是非常高效的,但你可能对这些必 须有一个物理上的上限表示赞同,把 空闲、长期运行的工作流从激活执行状态移除是一个好 主意。 或者发生什么意外,系统忽然被关闭呢?工作流完全在内存中处理,除非我们采取步骤 持久化它们。因此,除非我们在突发事件发生之前有所准备,否则我们就将丢失了执行中的 工作流实例。假如那些长期运行的工作流正管理着关键的进程,它 们 丢失了我们能承受得起 吗?在大多数情况下,我们都承受不起,至少我们不会心甘情愿地允许这些进程在毫无防备 措施的情况下就消失掉。 好消息是 WF 不仅为您提供了卸载工作流实例,然后又重新加载的方法,它也支持一个 服务:SqlWorkflowPersistenceService,它用来把工作流实例序列化进 SQL Server 数据库。 假如你读过前面一章,你 可 能已经熟悉甚至对把工作流实例的信息写入数据库的主张感到满 意。 因此,在什么时侯卸载工作流实例,并且假如他们被卸载,我们应怎么持久化它们呢? 在执行中有一些非常特别的点可卸载工作流实例。在大多数情况下,发生这种情况是在我们 会自动为刚才我之所以提到的——WF 不能离开(或不愿离开)长期运行的工作流程,在内 存中不必要地消耗资源和处理时间的时候。但我们也能亲自进行控制。这 里列出了工作流实 例的卸载点,在那些地方可进行持久化。 1.在 ActivityExecutionContext 完成并结束(卸载)后。我们在第四章(活动类型和 工作流类型介绍)简要谈过 ActivityExecutionContext 对象。 2.在 Activity 进入空闲状态时。 3.一旦一个 TransactionScopeActivity 完成(卸载)时。我们将在第十五章(工作流 和事务)看到这个活动。 4.一旦一个带有 PersistOnCloseAttribute 属性的 Activity 完成。 5.当你明确调用 WorkflowInstance.Unload 或 WorkflowInstance.TryUnload 时。 通过调用 WorkflowInstance 对象的特定方法或通过使用一个 Delay 活动让你的工作流 进入空闲状态,你 可控制工作流实例被持久化的时机。在 延 时 的 时 候 ,通过传递一个参数到 持久化服务的构造函数中,你将可对自动的持久化行为进行控制。 备注:暂停工作流实例和对工作流实例进行延时是不相同的。使用 Delay 活动将自动地 把工作流实例写入数据库(前提是你正使用 SqlWorkflowPersistenceService 并已经对其进 行配置,本章的最后一节将看到这些)。暂停仅仅是使工作流从激活的处理状态中撤出。然 而你也可选择使用 Unload 或 TryUnload 以手动的方式把工作流写入到数据库中。 WF 是如何完成这些的呢?这通过使用 SqlWorkflowPersistenceService 并结合创建一 个特定的数据库来完成这项任务(这非常像我们在前一章创建的跟踪数据库)。你 可 使用相 关脚本来创建一个数据库的架构(表和视图)以及执行持久化所需的存储过程。首先让我们 来创建数据库。 搭建 SQL Server 以进行持久化 就像前一章一样,我们通过在 SQL Server Management Studio Express 中创建一个新 数据库来开始我们的工作。 创建一个 SQL Server 2005 持久化数据库 1.启动 SQL Server Management Studio,连接数据库引擎。 2.在数据库节点上单击右键激活右键快捷菜单,选择“新数据库”。 3.在新数据库对话框中输入“WorkflowStore”作为数据库的名称字段,点击确定。 4.下一步将执行 WF 为设置持久化所提供的脚本(这会创建表和视图)。这些脚本的位 置在<%WINDIR%>\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\ZH-CHS,在这里<%WINDIR%>是指你的 Windows 目录(通常是 C:\Widows)。 在 SQL Server Management Studio 打开 SqlPersistence.Schema.sql 文件。 5.SQL Server Management Studio 会在一个新窗口中导入文件中的脚本,但在我们运 行脚本前,我们需指明在哪个数据库中运行这些脚本,因此我们要选择 WorkflowStore 数据 库。 6.点击工具栏上的执行按钮执行这些脚本。 7.重复 4-6 步执行 SqlPersistence.Logic.sql 脚本。这将在数据库中创建必须的存储 过程。 SqlWorkflowPersistenceService 服务介绍 保存和恢复工作流实例是可选的:假如你不想(持久化)的话,你就可避免使用持久化 存储介质(如数据库)来保存工作流实例。因此通过可插拔服务 (SqlWorkflowPersistenceService)来实现持久化或许更有意义。当工作流实例正处于运行 中的时侯,WorkflowInstance 和 SqlWorkflowPersistenceService 协作工作以执行存储和 恢复任务。 表面上,所有这些听起来相对地简单。假如我们需要把工作流实例换出并存储到数据库, 我们就通知持久化服务为我们存储它。但假如我们使用单一的数据库来持久化不同进程中运 行的工作流会发生什么呢?在工作流实例执行中是怎样进行停止和重启的呢? 使用单一的数据库来存储工作流实例并不罕见。但每个实例可能在不同的机器不同的进 程中执行,假如要保存和稍后恢复工作流实例,我们也必须要有一个手段来存储工作流在执 行时刻实际的系统状态。例如,SqlWorkflowPersistenceService 会存储实例是否被阻塞(等 待其它东西),它的状态(执行中,空闲等等)以及像序列化实例数据和拥有者标识等各种 各样的辅助信息。所有这些信息对于在以后的某个时间重现实例是必须的。 我们能够通过 WorkflowInstance 对象的三个方法来控制上述的持久化,参看表 6-1。 表 6-1 WorkflowInstance 方法 方法 功能 Load 加载先前被卸载(持久化)的工作流实例 TryUnload 试图从内存中卸载(持久化)该工作流实例。 和调用 Unload 不同的是,调用 TryUnload 时 假如工作流实例不能立即被卸载,那它将不会 被阻塞(维持执行状态)。 Unload 从内存中卸载(持久化)该工作流实例。注意 该方法为进行卸载将阻塞当前执行的线程,直 到工作流实例被真正地卸载。这 可 以是一个漫 长的操作,这取决于个人的工作流任务。 正如表 6-1 中所指出的,我们有两个方法来用于卸载和持久化工作流实例。你 该 使用哪 个方法取决于你的代码想要做什么事。Unload 会暂停工作流实例来为其持久化做好准备。 假如这要花费很长时间,该线程执行 Unload 操作也就要等待很长时间。然而,TryUnload 在请求卸载一个执行中的实例时将立即返回,但这不能保证该工作流实例真正被卸载并持久 化到数据库中。为进行检验,你应检查 TryUnload 方法的返回值。假如该值是 true,该工 作流实例本身就是卸载和持久化了的,假如该值是 false,则该工作流实例还没有被卸载和 持久化。TryUnload 的优点是你的线程不会处在等待状态,当然缺点是你可能要对该执行中 的工作流实例重复地使用 TryUnload 方法(进行检查)。 卸载实例 尽管 WF 在特定的时间将卸载和持久化你的工作流实例,但有时候你可能想亲自进行控 制。对于这些情况,WorkflowInsance.Unload 和 WorkflowInstance.TryUnload 是有助于你 的。 假如你首先没有插入 SqlWorkflowPersistenceService 就调用上述两个方法中的任何 一个的话,WF 将抛出一个异常。当然,假如有某种形式的数据库错误,你也将收到一个异 常。因此,好的做法是使用 try/catch 块来包围这些调用,以阻止你的整个应用程序(发生 异常时)崩溃。(注意这并不是说做任何事都与异常有关,有时你可能想要忽略它。) 我们来试试吧!我们先创建一个小图形用户界面,它为我们提供了一些按钮,我们使用 这些按钮来迫使应用程序产生特定的行为。应用程序的复杂性会增加一点,但我们也将走向 更加真实的应用中。 这里我们创建的应用程序仍然相对简单。它 仅仅有几个按钮,我们在特定的时间点击它 们来迫使工作流实例卸载。(在下一节,我们将重新加载它。)我将故意强制一个长时间运 行的工作流卸载,但和至今我们看到过的工作流不同,它将不会使用 Delay 活动。这样做的 原因就像你或许猜到的一样简单,是 因 为 Delay 活动很特别,它 会 使 自 己 自动伴随着持久化。 相反,我会强制我们的工作流实例卸载而不是像 Delay 活动那样自动进行卸载。在本章的“在 空闲中加载和卸载实例”这一节我们将看到 Delay 活动和它们的作用。当前,我们将请求工 作流线程休眠(sleep)10 秒,以便为我们提供充足的时间来按下我们程序中的按钮中的一 个。 创建一个新的宿主应用程序 1.就像你在前一章做的一样,打开 Visual Studio 创建一个新应用程序项目。但是,不 是要创建一个基于控制台的应用程序,而是创建一个 Windows 应用程序,名称为 WorkflowPersister。下 面 的 步骤在第二章中已经描述过:包含“添加工作流 assembly 引用”、 “宿主工作流运行时”、“创建 WorkflowRuntime 工厂对象”,“ 启动工作流运行时”,“ 停 止 工作流运行时”,“使用工作流运行时工厂对象”,“处理工作流运行时事件”过程。最 后,添加一个 app.config 文件(可参考前一章中的“添 加 SqlTrackingService 到你的工作 流中”,可不要忘记添加 System.Configuration 的引用)。 2.现在向 app.config 文件中添加恰当的数据库连接字符串(数据库为 WorkflowStore)。 3.当你创建了 WorkflowPersister 项目时,Visual Studio 显示了 Windows Forms 视图 设计器。在 Windows Forms 视图设计器中把鼠标移到工具箱上,选择一个 Button 控件,并 把它拖放到设计器的界面上。 4.我们将为这个按钮设置一些富有意义的文字属性,以便于我们知道我们点击的是什 么。选中这个按钮,然后在 Visual Studio 的属性面板中选择该按钮的 Text 属性,把该属 性的值设置为“Start Workflow”。 5.为该按钮添加 Click 事件的处理程序,具体代码将在后面的步骤中添加。 6.修改按钮的位置和大小,如下图所示: 7.重复步骤 3 至步骤 5,再添加两个按钮,一个的 text 属性为“Unload Workflow”, 另一个的 text 属性为“Load Workflow”。如下图所示: 8.现在就为测试我们的工作流创建好了用户界面,该是为我们将执行的应用程序添加事 件处理代码的时候了。当应用程序加载时我们需要初始化一些东西,做这些工作的一个很合 适的地方是在主应用程序窗体中的 Load 事件处理程序。 9.在该事件处理程序(处理方法)中输入下面的代码: _runtime = WorkflowFactory.GetWorkflowRuntime(); _runtime.WorkflowCompleted += new EventHandler(Runtime_WorkflowCompleted); _runtime.WorkflowTerminated += new EventHandler(Runtime_WorkflowTerminated) ; 10。在 Form1 类中声明下面名称为_runtime 的字段: protected WorkflowRuntime _runtime = null; protected WorkflowInstance _instance = null; 11.添加 System.Workflow.Runtime、System.Workflow.ComponentModel 和 System.Workflow.Activity 三个工作流组件的引用(可参考前面章节),然后在该代码文 件中添加下面的命名空间: using System.Workflow.Runtime; 12.尽管我们现在有了一个应用程序来宿主工作流运行时,但它实际上没做任何事。为 完成些功能,我们需向按钮的事件处理中添加一些代码。先向 button1_Click 中添加下面的 代码: button2.Enabled = true; button1.Enabled = false; _instance = _runtime.CreateWorkflow(typeof(PersistedWorkflow.Workflow1)); _instance.Start(); 这些代码使“Start Workflow”按钮禁用,而让“Unload Workflow”按钮可用,然后 启动了一个新的工作流实例。 13.下一步,找到“Unload WorkflowInstance”按钮的事件处理:button2_Click,然 后添加下面的代码。这 里 ,我们使用 WorkflowInstance.Unload 方法来卸载工作流实例并把 它写入数据库。在工作流实例卸载后,我们让“Load Workflow”按钮可用。注意假如我们 在卸载工作流实例时产生异常,“Load Workflow”按钮是不可使用的。这样做的意义是: 假如卸载请求失败,也就不用再加载。 button2.Enabled = false; try { _instance.Unload(); button3.Enabled = true; } // try catch (Exception ex) { MessageBox.Show(String.Format("Exception while unloading workflow" + " instance: '{0}'",ex.Message)); } // catch123 备注:牢记 WorkflowInstance.Unload 是同步的,虽然我在本章前面已经阐述过,但这 点很重要。这意味着线程试图卸载工作流实例时将会被阻塞(暂停),直到操作完成后为止 (不管卸载实例时是成功还是失败)。在这种情况下,可准确执行我想要的行为(指卸载), 因为我不想反复查看实例是否被卸载。但有时,你想在卸载时不会被阻塞,就应使用前面说 过的 Workflowinstance.TryUnload。稍后,在你添加完最后一批代码并运行应用程序时, 当你单击“Unload Workflow”时密切观察,你会看到应用程序会简短地被冻结,因为它正 等待工作流卸载。 14.现在回到我们关注的工作流事件处理上:Runtime_WorkflowCompleted 和 Runtime_WorkflowTerminated。这 两 个事件处理实际上完成相同的动作,那就是重置应用程 序以便为另一个工作流实例的执行做好准备。在“button2”的“click”事件处理方法(该 方法包含代码我们已在前面的步骤中添加)的下面添加下面这些方法: void Runtime_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e) { WorkflowCompleted(); } void Runtime_WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e) { WorkflowCompleted(); } 15.当然,我们现在还要创建“WorkflowCompleted”方法。假如你熟悉 Windows 编程, 你可能知道 Windows 一直以来存在着的限制问题。这个限制简单的说就是你不能在创建窗口 控件的线程外的任何线程中改变该窗口控件的状态。因此假如你想改变某个控件的 text, 你必须在创建该控件的同一线程上指定该控件的 text,使用任何其它线程最有可能导致的 结果是造成你的应用程序崩溃。因此我们即将加入的代码对你来说可能很搞笑,但所有要做 的这些的真正目的是确信我们是在原来的创建按钮的线程中来使按钮能用和禁用。(事件处 理几乎总是在不同的线程上进行调用。)假如我们只是在按钮自身的事件处理中使其可用, 应用程序可能还是能工作,但它更大的可能是崩溃和挂起。简单地把下面的代码复制并放到 源文件 Form1 类的尾部,应用程序才会正确的工作。 private delegate void WorkflowCompletedDelegate(); private void WorkflowCompleted() { if (this.InvokeRequired) { // Wrong thread, so switch to the UI thread WorkflowCompletedDelegate d = delegate() { WorkflowCompleted(); }; this.Invoke(d); } // if else { button1.Enabled = true; button2.Enabled = false; button3.Enabled = false; } // else } 16.在创建我们将执行的工作流之前我们需要最后做一件事,那就是要修改 WorkflowFactory 类。假如你从第五章(“为你的工作流添加跟踪服务”)以来准确地遵循 了所有的步骤来创建和修改 WorkflowFactory 的话,你实际上创建了一个为工作流运行时提 供跟踪服务的工厂对象。我们将对该代码进行轻微的调整,把 SqlTrackingService 服务改 为 SqlWorkingPersistenceService,并且改变声明命名空间的语句(把 System.Workflow.Runtime.Tracking 改为 System.Workflow.Runtime.Hosting)。打开 WorkflowFactory.cs 文件进行编辑。 17.用下面的代码来替换声明的 System.Workflow.Runtime.Tracking 命名空间。 using System.Workflow.Runtime.Hosting; using System.Configuration; 18。最后为运行时添加持久化服务,方法是在创建工作流运行时对象后添加下面的代码: string conn = ConfigurationManager.ConnectionStrings["StorageDatabase"].Connect ionString; _workflowRuntime.AddService(new SqlWorkflowPersistenceService(conn)); 注意:因为我们在代码中创建的工作流实例的类型是 PersistedWorkflow.Workflow1 类型(在第 12 步中),因此我们的宿主应用程序编译并执行会出错,我们将在下面的一节 解决。 这里我们就有了一个 Windows 图形用户界面和宿主应用程序,我们使用它们来承载我们 的工作流。谈到工作流,我们不是要创建并执行它吗?其实,这在下面进行讲解。 创建一个新的可卸载的工作流 1.像前面一章一样,我们又将在我们的项目中创建一个新的顺序工作流库。在 Visual Studio 中激活 WorkflowPersister 应用程序,然后点击“文件”菜单,选择“添加”,当 子菜单弹出后,选择“新建项目”。从“添加新项目”的 对话框中添加一个“顺序工作流库” 的项目,项目名称为“PersistedWorkflow”。 2.在应用程序解决方案中创建并添加一个新项目后,将呈现该工作流的视图设计器界 面。从工具箱中拖拽一个“Code”活动到设计器界面上。在 Visual Studio 属性面板上设置 “Code”活动的“ExecuteCode”属性为 PreUnload 然后按下回车键。 3.然后 Visual Studio 将自动切换到该工作流的源代码文件中,向刚刚插入的 PreUnload 方法添加下面的代码: _started = DateTime.Now; System.Diagnostics.Trace.WriteLine(String.Format("*** Workflow {0} started: {1} ", WorkflowInstanceId.ToString(), _started.ToString("MM/dd/yyyy hh:mm:ss.fff"))); System.Threading.Thread.Sleep(10000); // 10 seconds 4.为了计算工作流消耗的时间(下面的步骤中我们将看到,该时间至少在两个 Code 活 动执行的时间之间),我在一个名称为“_started”字段中保存了启动时间。在你的源文件 中构造函数的上面添加该字段: private DateTime _started = DateTime.MinValue; 5.现在切换到设计视图,添加第二个 Code 活动。该活动的 ExecuteCode 属性设置为 “PostUnload”,并自动生成该方法。你将看到的设计器界面如下: 6.再次切换回工作流的源代码文件中,向 PostUnload 方法中添加下面必要的代码: DateTime ended = DateTime.Now; TimeSpan duration = ended.Subtract(_started); System.Diagnostics.Trace.WriteLine( String.Format("*** Workflow {0} completed: {1}, duration: {2}", WorkflowInstanceId.ToString(), ended.ToString("MM/dd/yyyy hh:mm:ss.fff"), duration.ToString())); 7.最后一步是添加一个项目级的引用把该工作流项目引用到我们的主应用程序中,具体 步骤可参考前面的章节。 备注:现在你或许想运行该程序,但请等等!假如你运行该应用程序,然后点击“Start Workflow”按钮而没有点击“Unload Workflow”按钮的话,该应用程序不会出现运行错误。 因为一旦工作流被卸载,但我们还没有添加代码来重新加载这个已被持久化(卸载)的工作 流实例。因此在下一节添加代码前,你不应单击“Unload Workflow”按钮。 这样做的目的是在工作流开始执行时,在第一个 Code 活动中让它休眠(sleep)10 秒 钟。在此期间,你可点击“Unload Workflow”按钮来卸载该工作流。在这 10 秒届满后,该 工作流将被卸载并持久化到数据库中。这 些事一旦发生,你 就可喝杯咖啡、吃吃棒棒糖或者 其它任何事休息休息:你的工作流已被保存到数据库中,正等待再次加载。让我们看看这又 是怎么工作的。 加载实例 WorkflowInstance 公开了二个卸载方法:“Unload”和“TryUnload”,但仅仅只有一 个“Load”方法,该方法不用关心工作流实例是怎样存储到数据库中的。一旦它(工作流实 例)被存储,你 就可使用 WorkflowInstance.Load 来把它再次重置到执行状态。现在我们将 向 WorkflowPersister 应用程序中添加合适的代码来做这些事情。 加载被持久化的工作流 1.在 Visual Studio 中打开 WorkflowPersister 应用程序,打开该源代码文件,找到主 应用程序窗体,定位到“button3_Click”事件处理方法。 2.在“button3_Click”事件处理中添加下面的代码: button3.Enabled = false; try { _instance.Load(); } // try catch (Exception ex) { MessageBox.Show(String.Format("Exception while loading workflow" + " instance: '{0}'", ex.Message)); } // catch button1.Enabled = true; 现在我们来看看所有这些能否真正工作。我们将运行两次来测试工作流:一 次我们直接 运行到结束,一次我们将强制其卸载。然后我们比较执行时间并看看 SQL Server 数据库内 记录了些什么。 测试 WorkflowPersisiter 应用程序 1.按下 F5 键调试 WorkflowPersisiter 应用程序,如有任何编译错误,请进行修正。注 意该测试我们会写一些输出的跟踪信息到“输出”窗口中,因此假如没有“输出”窗口的话, 在 Visual Studio 的“视图”菜单下选择“输出”使其呈现。 2.点击“Start Workflow”按钮创建并启动一个工作流实例,然后该“Start Workflow” 按钮会被禁用,而“Unload Workflow”按钮将能使用。因为我们让工作流线程休眠 10 秒钟, 经过 10 秒后,“Unload Workflow”按钮又会自动禁用,“Start Workflow”按钮则重新可 用。在本测试中,工作流会一直运行到结束,工作流总共执行的持续时间会是 10 秒。 3.再一次点击“Start Workflow”按钮。但是,这 次 在这 10 秒的休眠期间将点击“Unload Workflow”按钮。该按钮在该 10 秒期间内将会被禁用,过后,“Load Workflow”按钮将能 使用。在此时,你的工作流被实例化并保持被卸载状态,直到你重新加载它。 4.但在你重新加载该工作流实例前,请打开 WorkflowStore 数据库中的 InstanceState 表,会在该表中看到一行记录,这行就是你的被持久化的工作流实例! 5.回到 WorkflowPersister 程序的执行当中,点击“Load Workflow”按钮。“Load Workflow”按钮会被禁用,而“Start Workflow”按钮将能使用。 6.结束 WorkflowPersister 应用程序。 7.Visual Studio 输出窗口中会包含和我们执行的两个工作流有关的信息。 8.在输出窗口中滚动到底部,查找我们注入的文本(我们使用三个星号“***”来装饰 这些文本)。如下所示: 假如你回头看看 InstanceState 表的截图,并把你看到的工作流实例 ID 和你在 Visual Studio 输出窗口中看到的工作流实例 ID 做比较,你 会 看到在我们的例子中有两个相同的实 例 ID:bfb4e741-463c-4e85-a9e0-c493508ec4f1。该实例花费的时间为:00:01:41.4859296, 第一个工作流实例(ID 为:Workflow dab11c11-9534-4097-b5bc-fd4e96cfa66c)花费的时 间正如我们期望的,几乎就为 10 秒钟。两个实例 ID 和执行时间有区别,但实质上是一样的。 你卸载并持久化到数据库中的工作流实例的运行时间将超过 10 秒,在 InstanceState 表中 显示的实例 ID 会和 Visual Studio 输出窗口中显示的相匹配。 在空闲时加载和卸载实例 我在本章的前面部分曾经提到,在我们的工作流过程中我们将使用 System.Threading.Thread.Sleep 来替换 Delay 活动,以进行工作流的测试。在 那 时 我 说 过 , 我 选择这样做的原因。就持久化而言,Delay 活动有一些特殊的处理能力。我们现在就来简 要地看看 Delay 会为我们做些什么。 假如你在你的工作流中引入了一个 Delay 活动,那目的明显是要暂停处理一段时间,不 管 这是一个确切的时间段还是一个不确定的暂停,该暂停会在将来的某个特定时间点,如 五 天 后 被 取 消 。 当执行一个 Delay 活动时,假如工作流运行时已经附加了 SqlWorkflowPersistenceService 服务的话,该工作流运行时将会自动地为你持久化该工作 流,而且当延时周期过期后又自动恢复它。注意,不 管运行工作流运行时的系统是否被关闭、 重启等等上述事件都会发生。为了使之能自动持久化,你要在创建你的工作流运行时为 SqlWorkflowPersistenceService 服务添加一个特定的构造函数参数。(之前的例子省略了 这些,因此工作流不会自动进行持久化。) 我提到的构造函数参数能使 SqlWorkflowPersistenceService 的 internal 方法 “UnloadOnIdle”在工作流实例空闲时被调用。该方法通常不会被调用。你必须通过使用 SqlWorkflowPersistenceService 构造函数中的一个重载函数来明确指定这一点。在下面的 例子中,将使用一个集合参数,因为你既想传入连接字符串也想传入空闲卸载标志。甚至还 有其它更加灵活的构造函数(在本例中我们只描述这一个)。现在我们就来看看这个工作流 会自动持久化的例子。 创建一个新的在空闲时持久化的工作流 1.对于这个例子,为了让你快速领悟到在空闲时持久化是怎样工作的,我们将使用一个 简单的基于控制台的应用程序,打开 Visual Studio,创建一个新的 Windows 项目,该控制 台应用程序的名称命名为“ WorkflowIdler”。下 面 的 步骤,如“ 添 加 对 工作流模块的引用”、 “宿主工作流运行时”,“ 创建 WorkflowRuntime 工厂对象”、“启动工作流运行时”、“停 止工作流运行时”、“使用工作流运行时工厂对象”及“处理工作流运行时事件”等过程来 自第二章。 2.就像在前面例子(“创建一个新的宿主应用程序”)中的第 16 步和第 17 步一样,修 改 WorkflowFactory 类。但是,还有一些额外的修改工作是必要的,添加下面的语句: using System.Collections.Specialized; 3.和前面例子(“创建一个新的宿主应用程序”)中的第 18 步一样,在运行时对象被 创建后添加持久化服务: NameValueCollection parms = new NameValueCollection(); parms.Add("UnloadOnIdle", "true"); parms.Add("ConnectionString", ConfigurationManager.ConnectionStrings["StorageDa tabase"].ConnectionString); _workflowRuntime.AddService(new SqlWorkflowPersistenceService(parms)); 4.和前面的例子一样添加一个应用程序配置文件(连接字符串仍然相同)。具体过程可 参考第五章中的“为你的工作流添加跟踪服务”,那里添加了该 app.config 文件。 5.创建一个单独的顺序工作流库项目来承载我们的新工作流,工作流库的名称为 IdledWorkflow。 6.重复前一个名称为“创建一个新的可卸载的工作流”例子中的步骤 2 到步骤 4。这些 步骤放置了二个 Code 活动到你的工作流中。 7.在源代码文件中添加下面的代码到“PreUnload”方法中(先前一步你已添加了 “PostUnload”方法的代码)。 _started = DateTime.Now; System.Diagnostics.Trace.WriteLine( String.Format("*** Workflow {0} started: {1}", WorkflowInstanceId.ToString(), _started.ToString("MM/dd/yyyy hh:mm:ss.fff"))); 8.返回到工作流视图设计器上,拖拽一个 Delay 活动到两个 code 活动之间。 9.指定 Delay 活动的“TimeoutDuration”属性为 30 秒。这将有充足的时间来查看 WorkflowStore 数据库中的 InstanceState 表。 10.工作流现在就设计完成了,向 WorkflowIdler 应用程序中添加对该工作流的项目引 用。 11.在 WorkflowIdler 项目中打开 Program.cs 文件,找到下面的这行代码: Console.WriteLine("Waiting for workflow completion."); 12.当然,因为没有工作流被启动,该应用程序也就不会等待工作流完成。因此,创建 一个工作流实例,在你找到的那行代码下添加下面的代码: // Create the workflow instance. WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(IdledWorkflow.Workflow1)); // Start the workflow instance. instance.Start(); 13.按 F6 键编译该解决方案,纠正任何弹出的编译错误。现在,当你执行该 WorkflowIdler 应用程序时,Delay 活动将强制工作流实例持久化到存储数据库中。然而, 你会等上超过 30 秒(多达 2 分钟)的时间,实例才被重新加载。那是因为工作流运行时在 空闲状态下由于延时,要周期性地对持久化工作流进行检查,但它不能保证那些工作流将仅 仅等上所期望的延时时间。WF 会周期性地轮询数据库,寻找那些正等待计时器事件的空闲 并已被持久化的工作流(Delay 活动使用了一个计时器)。默认的轮询时间是 2 分钟。 备注:可以更改默认的数据库轮询时间,方法是使用 SqlWorkflowPersistenceService 服务时为其提供一个 TimeSpan,可使用四个参数的构造函数(分别是连接字符串、工作流 处于空闲状态时是否卸载的标志、工作流保持锁定的时间长度以及持久化服务轮询数据库以 查找计时器已过期的工作流的频率)。 第七章:基本活动的操作 学习完本章,你将掌握: 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.Workflow 1)); 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.Wor kflow1)); 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 += new EventHandler(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 活动就是工具箱中最好的工 具。 第八章:调用外部方法及工作流 学习完本章,你将掌握: 1.创建并调用你的工作流外部的本地数据服务 2.理解怎样使用接口来为宿主进程和你的工作流之间进行通信。 3.使用设计的外部方法在你的工作流和宿主应用程序之间传输数据。 4.在一个正执行的工作流中调用其它工作流 在写前面的章节时,我自己不断地思考,“ 我 不 能 再 等 了,我要弄清楚在哪里可把(工 作流中的)真实数据返回到宿主应用程序中!”为什么?做了这么多的活动和工作流的演示, 但都没有实际返回某些感兴趣的东西给宿主应用程序。我不知写过多少我们感兴趣的工作流 的实例和演示,但至多只是仅仅处理过数据的初始化(就像第一章-WF 简介中你看过的邮政 编码的例子)。但事情变得更加有趣,坦率地说,当我们启动工作流,然后从外部源中寻找 并处理数据、返回处理后的数据给我们的主应用程序要更加接近现实。 为什么不这样呢?公开一个对象,来从执行的工作流中传给宿主应用程序,或者从宿主 应用程序传给工作流不就行了吗?其实,使用现有的串行化技术,如 .NET Remoting 或者 XML Web 服务,就可完成这些事。串行化,也叫序列化,它可把数据从原有的形式转换成合适的 形式,以在不同进程甚至不同计算机之间进行传输。 为什么谈到序列化呢?因为你的工作流是在你的宿主进程中的不同线程上执行,不 同 线 程 之 间 传送数据,如不进行适当的序列化,将 会 引 发 灾 难 ,具 体 原因超出了本书的讨论范围。 其实,你的工作流能在一个持久化的状态下发送它的数据。这 并 没有在不同线程上,甚至它 不在执行中。 但我们想在我们的工作流和正控制该工作流的宿主进程间传送数据时,使用.NET Remoting 或者 XML Web 服务这样的技术为什么并没有认为是多余的呢?其实这绝对有必要! 我们将创建 local 通信,本章将以此出发。我们将搭建必须的体系来满足线程数据序列化, 以进行计算机之间或进程之间的数据传输。 创建 ExternalDataService 服务 当工作流和它的宿主进行通信时,在它发送和接收数据的时候,工作流要使用队列和消 息。WF 为我们做的越多,我们就可把重点更多的放到应用中特定任务的解决上。 工作流内部进程通信 对于简单的通信任务,WF 使用“ abstraction layer”来在工作流和宿主之间进行缓冲。 抽象层像一个黑盒,你为它提供输入,它会执行一些神奇的任务,然后信息流出到另一边。 但我们不用知道它是如何工作的。 在这种情形下,该黑盒就是一个知名的“local communication”服务。和 WF 术语中的 任何一种服务一样,它也是另一种可插拔服务。区别是它不像 WF 中的那些已预先创建好的 服务,你 需 要 写 出 这个服务的一部分。为什么呢?因为你在宿主应用程序和你的工作流之间 传递的数据有一定的特殊性。更进一步说,你 可创建各种各样的数据传输方法,你 可 使用你 设计的各种方法从宿主应用程序发送数据,然后在工作流中接收数据。 备注:这 里有 些 事 情你 需 要 进行关注,那就是对象或集合的共享问题。因为宿主应用程 序和工作流运行时在同一个应用程序域执行,因此引用类型的对象和集合就是通过引用而不 是值进行传递。这意味着宿主应用程序和工作流实例在同一时间会访问和使用同一个对象, 多线程环境下这会产生 bug,出现数据并发访问错误。因此,对于可能要进行并发访问的对 象或集合,你可考虑传递一个对象或集合的副本,或许这可通过实现 ICloneable 接口,或 者考虑亲自序列化该对象或集合并传递序列化后的版本。 你可写这种 local service,把它插进工作流,然后打开连接,发送数据。这些数据可 以是字符串,DataSet 对象,甚至可以是你设计的任何可被序列化的自定义对象。通信可以 是双向的,尽管在本章我没有演示它。(这里,我仅仅是把数据从工作流中传回给宿主应用 程序。)从工作流的角度来说,我们使用工具生成活动的目的是发送和接收数据。从宿主应 用程序的角度来说,接收数据等同于一个事件,而发送数据就是在一个服务对象上的方法的 简单调用。 备注:我们在后面几章看到更多的活动后还会重温该双向数据传输的概念。工作流活动 从宿主应用程序中接收数据基于一个 HandleExternalEvent 活动,我们将在第 10 章“ Event 活动”中看到。我们也需要更深入地了解这些概念间的相互关系,这在第 17 章“宿主通信” 中将进行介绍。对于当前,我们只是在工作流实例完成它的任务后,简单地返回复合数据给 宿主。 我们需要做的还不仅仅是这一点,我们最终需要添加 ExternalDataService 服务到我们 的工作流运行时中。ExternalDataService 是一个可插拔的服务,它方便了工作流实例和宿 主应用程序之间进行序列化数据的传输。在 紧 接 下 来 的一节我们将写出的该服务的代码将做 很多事(包括序列化数据的传输)。让我们来看看大体的开发过程。 设计并实现工作流内部进程通信 我们先决定将传送些什么数据。它是一个 DataSet 吗?是一个像整形数字或字符串之类 的系统直接支持的对象吗?或者是一个由我们自己设计的自定义对象吗?无论它是什么,我 们都将设计一个 ExternalDataService 能够绑定的接口。这个接口将包含我们设计的一些方 法,这 些方法能分别从工作流实例的角度上及宿主的角度上来发送数据和接收数据。使用该 接口中的方法,我们就可来回传送数据。 我们然后需要写一些代码:外部数据服务的一部分。它表述了连接或者称作桥接代码, 宿主和工作流将使用它来和 WF 提供的 ExternalDataService 进行交互。假如我们正涉及一 个 XML Web 服务,Visual Studio 会为我们自动地创建代理代码。但对于工作流来说没有这 样的工具,因此我们需要亲自设计这个桥接代码。我们这里使用的“桥”实际上由两个类组 成:一个 connector 类和一个 service 类。你 可用你喜欢的名称来命名它们,但我推荐使用 这样的名字来命名它们。connector 类管理数据管道(状态维护),而 service 类被宿主和 工作流用来直接进行数据交换。 在创建好接口后,我们将使用一个工具:wca.exe,它的位置通常是在你的“Program Files\Microsoft SDKs\Windows\v6.0A\Bin”目录下。该工具叫做 Workflow communications Activity generator utility,该工具的作用是,给出一个接口,它将生成两个活动,你能 使用它们去把该接口和你的工作流实例进行绑定。一个用来发送数据,为 invoker,另一个 用来接收数据,为 sink。一旦它们创建好后,你就能从 Visual Studio 工具箱中把它们拖 拽到工作流视图设计器上,它 们 也 和 任何其它工作流活动一样进行工作。但前面我已经提到 过,我们没有一个工具创建连接桥代码,这样的工具在工作流方面一定很有用。 提示:从项目的角度考虑,我倾向于为宿主应用程序创建一个或一组项目,为 前面提到 的接口和连接桥创建另一个项目,为工作流代码再单独创建一个项目。这 可 让 我方便地从宿 主应用程序和工作流中添加对该接口和桥接类的引用,做 到 了在程序集之间进行简洁的功能 隔离。 我们有了这些程序集后,我们需要连通我们的工作流和宿主应用程序之间的通信。在 执 行 时 ,通过使用 ExternalDataService 整个过程被简化了。我们先快速看看本章中的最基本 的应用程序实例(就它而言,它比我们目前看到过的例子都有复杂),然后开使创建我们需 要的工作流外部数据通信代码。 机动车数据检查应用程序 本示例应用程序是一个 Windows Forms 应用程序,它提供了一个用户界面,上面集中了 指定驾驶员的机动车数据。该应用程序本身已是很有意义的,我不想再重复创建它的每一个 细节。相反,你将使用这个已经提供好了的样本代码来作为本章的起点。但是,我将展示怎 样把它们绑进工作流组件中。 主用户界面窗体见图 8-1。下拉列表框控件包含了三个驾驶员的姓名,选择其中一个的 姓名都会生成一个新的设计好的工作流的实例来对该驾驶员的机动车信息进行检索,并返回 一个完整的 DataSet。该 DataSet 然后被绑定到两个 ListView 控件,一个是违规信息。 图 8-1 MVDataChecker 窗体的主用户界面 当你点击“Recrieve MV Data”按钮时,你就会初始化一个新的工作流实例,用户界面 会禁用该检索按钮及驾驶员下拉列表框控件并显示一个“ searching”通知,如 图 8-2 所示。 你在该窗体底部看到的 picture box 控件是一个动画图片文件。该应用程序根据情况对其中 的 label 控件和 picture box 控件进行隐藏或显示。 图 8-2 MVDataChecker 窗体的“searching”用户界面 当工作流实例来完成了它的工作后,它会使用我们将要创建的一个活动来激发一个事 件,宿主应用程序会截获该事件,该事件把数据已准备好的消息通告该宿主应用程序。因为 Windows 窗体的 ListView 控件不能直接绑定到 DataTable 对象,因此我们从工作流中检索 到数据后将一行一行地把数据插入到该控件中,如图 8-3 所示。 图 8-3 MVDataChecker 窗体检索数据后的用户界面 在应用程序执行到此时,你 可 选择是检索另一个驾驶员的信息还是退出程序。假如你在 查询过程中退出该应用程序,正执行的工作流实例会被异常终止。 然后我们来看看需要添写完成所有这些任务的代码,首先我们需要为 WF 提供一个接口, 以便它能激发我提过的“数据已准备好”的事件。 创建服务接口 该服务接口完全要由你创建,它 应基于你想在你的工作流实例和你的宿主应用程序之间 进行通信的数据之上。对于本示例,想 像 你 需 要设计一个工作流来从各个源数据中检索驾驶 员的信息,然后你想把这些信息整理为一个单一的数据结构:带多个表的 DataSet,一个表 是车辆标识信息,一个表是驾驶员违规信息。我们将简单地使用虚拟的数据,以 便 更 侧重 于 把 焦 点放到工作流自身上。在 宿 主应用程序中,我们将在两个 ListView 控件中显示这些(伪 造的)数据。 你要把驾驶员的名字传入工作流实例中,该工作流实例使用它来查找驾驶员和车辆的信 息。在 获取了这些数据后,工作流实例通知宿主应用程序数据已经准备好了,然后宿主应用 程序读取并显示这些信息。 因此实际上在我们的接口只需要一个单一的方法:MVDataUpdate。我们知道需要发送一 个 DataSet,因此我们把这个 DataSet 作为方法的参数传入到 MVDataUpdate 中。 创建一个工作流数据通信接口 1.该 MVDataChecker 示例应用程序,同前面的例子一样,包含两个版本:练习版本 (MVDataChecker 目录中)和完整版本(MVDataChecker Completed 目录中),它们可在本章 的源代码中进行下载。我们现在就使用 Visual Studio 打开练习项目中的解决方案。 2.在该解决方案中包含三个项目。在 Visual Studio 解决方案浏览器中展开 MVDataServic 项目,然后打开 IMVDataService.cs 文件。 3.在 MVDataService 名称空间中添加下面的代码并进行保存。 public interface IMVDataService { void MVDataUpdate (DataSet mvData); } 这样就大功告成了!这就是所有你需要为创建一个接口所要做的工作。不过 ,我们需要 添加一个属性,以使这个接口适合于 WF 的使用,我们将在下面的一节介绍。 使用 ExternalDataExchange 特性 尽管有了接口:IMVDataService,但我们仍不能把该接口提供给 WF,以让 WF 真正使用 它来进行数据通信。为此,我们需要添加 ExternalDataExchange 特性。 ExternalDataExchange 特性是一个简单的标记,WF 使用它来指明接口可适合于本地通 信服务使用。记得我提到的 wca.exe 工具吗?它和 Visual Studio 都使用这个特性来指明接 口可被你的工作流实例使用。我们就来添加 ExternalDataExchange 特性。 备注:不要让词语“特性标记”所欺骗,你不要认为该 ExternalDataExchage 特性不是 一个关键组成部分。它相当重要。当工作流运行时试图进行外部数据传送时会寻找该特性。 没有它,工作流和宿主之间进行数据传输就不可能。 创建一个工作流数据通信接口 在 Visual Studio 中打开 IMVDataService.cs 文件,为 前面定义的接口添加下面的代码: [ExternalDataExchange] IMVDataService 接口的完整代码在下面的清单 8-1 中。此时不要担心该应用程序编译 出错。在编译无错之前,我们还需要添加更多的代码。 清单 8-1 IMVDataService.cs 完整代码 using System; using System.Collections.Generic; using System.Text; using System.Workflow.Activities; using System.Workflow.Runtime; using System.Data; namespace MVDataService { [ExternalDataExchange] public interface IMVDataService { void MVDataUpdate(DataSet mvData); } } 使用 ExternalDataEventArgs 我在前面提到过,宿主应用程序和正执行的工作流之间使用事件进行通信。宿主应用程 序无法事先准确地知道工作流实例准备好数据的时间,对该数据进行轮询效率又低得可怕。 因此 WF 使用异步模式,当数据准备好了的时候激发一些事件。宿主应用程序捕获这些事件 然后读出数据。 因为我们想把信息发送给事件的接收者,因此我们需要创建一个自定义事件参数的类。 假如你在前面的工作中已创建过一个自定义事件类,你或许就是使用 System.EventArgs 作 为基类。 但是,WF 外部数据事件需要带一个(和上述)不同的参数作为基类,以便该事件能承 载工作流实例的实例 ID。我们应使用的基类是 ExternalDataEventArgs,它从 System.EventArgs 类派生,这样我们就熟悉了它的背景。另外,还有两点要求:我们需要 提供一个以该实例 ID(一个 Guid)作为参数的基本的构造器,该构造器又把实例 ID 传给基 类构造器,第二点是我们必须使用 Serializable 特性来标记我们的类,以表明我们的类是 可序列化的。 我们现在就来创建我们所需要的外部数据事件参数类。 创建工作流数据事件参数类 1.使用 Visual Studio 打开 MVDataService 项目,定位在 MVDataAvailableArgs.cs 文 件上,打开该文件准备进行编辑。 2.在该文件所定义的名称空间中,添加下面的代码: [Serializable] public class MVDataAvailableArgs : ExternalDataEventArgs { } 3.最后我们需要添加一个构造器,以便把工作流实例 ID 传给基类: public MVDataAvailableArgs(Guid instanceId) : base(instanceId) { } 完整的事件参数类如清单 8-2 所示。 清单 8-2 完整的 MVDataAvailableArgs.cs 源文件 using System; using System.Collections.Generic; using System.Text; using System.Workflow.Activities; using System.Workflow.Runtime; namespace MVDataService { [Serializable] public class MVDataAvailableArgs : ExternalDataEventArgs { public MVDataAvailableArgs(Guid instanceId) : base(instanceId) { } } } 创建外部数据服务 我们现在来到了更加复杂的一节,我们的任务是为外部数据服务创建桥接代码。宿主必 须有这些代码,它 才 能 访 问到工作流实例试图传递过来的数据。我们将使用工具来为工作流 创建活动(这在下一节介绍),但对于宿主这边的通信连接来说,却没有现成的工具。 在这里,我们将创建一个稍微简化的连接桥版本(这是对于完整的连接桥架构来说)。 该版本仅仅支持工作流到宿主的通信。(当我们学到 17 章时,我们将会创建一个可重用的通 用双向连接桥。)我们在此将创建的连接桥被分成了两个部分:一是 connector,它实现了 我们前面已经开发好了的接口;二是 service,除了别的事情外,它有一个职责是激发“data available”事件以及提供一个“read”方法,使用该方法来把数据从工作流中取出。 提示:该代码应由你而不是 WF 来提供。我在写本地数据交换服务时提供了该代码,但 你要写的代码可以有所不同。唯一要求是本地数据交换服务实现了通信接口并提供一种机 制,用于检索需要交换的数据。 为什么如此复杂?和传统的.NET 对象不同,工作流实例在工作流运行时的范围内执行。 因此进出工作流实例的事件都由工作流运行时进行代理。工作流运行时必须做这些工作,因 为你的宿主应用程序不能把数据发送给已经被持久化或不处在执行状态下的工作流实例。 回到我们的连接桥上,该连接类包含一个字段,工作流将使用要被传回的数据来填充该 字段。对于我们正在创建的本示例应用程序来说,我们不允许并发执行工作流实例,但这仅 仅是出于方便。通常情况下,并没有阻止我们执行并发执行的工作流实例,这些我们将在第 17 章看到。 当然,每一个工作流实例可能会返回不同的数据,至少它传递的驾驶员会和另一个工作 流实例不同。连接类的职责是实现我们开发的在宿主这边接口,以 及 不 间 断 地 保 持这些数据。 当宿主请求该数据时,连接类根据工作流实例 ID 来确定应正确返回的 DataSet 是否已经到 达。 该服务类为你处理一些任务。首先,它使用工作流运行时注册该 ExternalDataService, 以便我们可在宿主和工作流实例间进行通信。它 维护一个连接类的单例副本,并把它自己作 为服务提供者绑定到该连接类。该服务类也充当了工厂(设计模式)的角色,确保我们有一 个且仅有一个连接类( 实 例)。(假如我们实现了双向的接口,该服务类也会提供一个“ write” 方法。)我们现在就来创建这些类。 创建桥接器(bridge connector)类 1.在 Visual Studio 中打开 MVDataService 项目,定位到 MVDataCnnector.cs 文件,最 后打开该文件。 2.在所定义的名称空间中添加下面的代码: public sealed class MVDataConnector : IMVDataService { private DataSet _dataValue = null; private static WorkflowMVDataService _service = null; private static object _syncLock = new object(); } 字段_dataValue 用来容纳工作流实例产生的数据。字段_service 用来容纳数据服务对 象的单一实例。_syncLock 对象仅仅用来进行线程的同步。 3.下面,我们添加一个 static 属性来访问该服务对象的单一实例。代码如下: public static WorkflowMVDataService MVDataService { get { return _service; } set { if (value != null) { lock (_syncLock) { // Re-verify the service isn't null // now that we're locked if (value != null) { _service = value; } // if else { throw new InvalidOperationException("You must provide a s ervice instance."); } // else } // lock } // if else { throw new InvalidOperationException("You must provide a service i nstance."); } // else } } 4.我们需要添加一个属性来访问该 DataSet,代码如下: public DataSet MVData { get { return _dataValue; } } 5.因为连接器类从 IMVDataService 派生,因此我们必须实现 MVDataUpdate 方法: public void MVDataUpdate(DataSet mvData) { // Assign the field for later recall _dataValue = mvData; // Raise the event to trigger host read _service.RaiseMVDataUpdateEvent(); } 工作流使用这个方法来把 DataSet 保存到_dataValue 字段中。它激发事件以通知宿主 数据已经准备好了。该桥接器类的完整代码参见清单 8-3。注意我们这时并没准备去编译整 个应用程序,我们还有更多的代码需要添加。 清单 8-3 完整的 MVDataconnector.cs 源文件 using System; using System.Collections.Generic; using System.Text; using System.Workflow.Activities; using System.Workflow.Runtime; using System.Data; namespace MVDataService { public sealed class MVDataConnector : IMVDataService { private DataSet _dataValue = null; private static WorkflowMVDataService _service = null; private static object _syncLock = new object(); public static WorkflowMVDataService MVDataService { get { return _service; } set { if (value != null) { lock (_syncLock) { // Re-verify the service isn't null // now that we're locked if (value != null) { _service = value; } // if else { throw new InvalidOperationException("You must pro vide a service instance."); } // else } // lock } // if else { throw new InvalidOperationException("You must provide a s ervice instance."); } // else } } public DataSet MVData { get { return _dataValue; } } // Workflow to host communication method public void MVDataUpdate(DataSet mvData) { // Assign the field for later recall _dataValue = mvData; // Raise the event to trigger host read _service.RaiseMVDataUpdateEvent(); } } } 创建桥接服务(bridge service)类 1.再次在 Visual Studio 中打开 MVDataService 项目,定位到 WorkflowMVDataService.cs 文件,打开该文件准备进行编辑。 2.我们创建好了 MVDataConnector 类,我们还要把下面的代码复制到 WorkflowMVDataService.cs 文件中: public class WorkflowMVDataService { static WorkflowRuntime _workflowRuntime = null; static ExternalDataExchangeService _dataExchangeService = null; static MVDataConnector _dataConnector = null; static object _syncLock = new object(); public event EventHandler MVDataUpdate; private Guid _instanceID = Guid.Empty; } 3.我们需要具有从类的外部访问_instanceID 的能力,因此添加下面的属性: public Guid InstanceID { get { return _instanceID; } set { _instanceID = value; } } 4.我们现在要添加一个静态的工厂方法,我们将用它去创建本类的实例。我们这样做是 为了确保在我们创建本桥接服务的时候,所有重要的事情都已完成。例如,我们需要确保 ExternalDataService 服务已被插入到了工作流运行时中。我们也将添加刚才已经创建好了 的桥接器类,并把它作为一个可插拔服务以便工作流能访问到该数据连接器类。因此,我们 在上面一步所添加的属性下面还要添加下面的方法: public static WorkflowMVDataService CreateDataService(Guid instanceID, Workflow Runtime workflowRuntime) { lock (_syncLock) { // If we're just starting, save a copy of the workflow runtime refere nce if (_workflowRuntime == null) { // Save instance of the workflow runtime. _workflowRuntime = workflowRuntime; } // if // If we're just starting, plug in ExternalDataExchange service if (_dataExchangeService == null) { // Data exchange service not registered, so create an // instance and register. _dataExchangeService = new ExternalDataExchangeService(); _workflowRuntime.AddService(_dataExchangeService); } // if // Check to see if we have already added this data exchange service MVDataConnector dataConnector = (MVDataConnector)workflowRuntime. GetService(typeof(MVDataConnector)); if (dataConnector == null) { // First time through, so create the connector and // register as a service with the workflow runtime. _dataConnector = new MVDataConnector(); _dataExchangeService.AddService(_dataConnector); } // if else { // Use the retrieved data connector. _dataConnector = dataConnector; } // else // Pull the service instance we registered with the connection object WorkflowMVDataService workflowDataService = MVDataConnector.MVDataSer vice; if (workflowDataService == null) { // First time through, so create the data service and // hand it to the connector. workflowDataService = new WorkflowMVDataService(instanceID); MVDataConnector.MVDataService = workflowDataService; } // if else { // The data service is static and already registered with // the workflow runtime. The instance ID present when it // was registered is invalid for this iteration and must be // updated. workflowDataService.InstanceID = instanceID; } // else return workflowDataService; } // lock } 5.在前面一节(“创建桥接器类”)我们创建的连接器对象中保存有我们在第 4 步中创建 的该桥接器对象。我们现在将添加一个静态方法,使用该方法可返回该桥接服务实例。尽管 这些现在看来没有太大必要,但稍后会讲讲我们这样做的理由。代码如下: public static WorkflowMVDataService GetRegisteredWorkflowDataService(Guid insta nceID) { lock (_syncLock) { WorkflowMVDataService workflowDataService = MVDataConnector.MVDataSer vice; if (workflowDataService == null) { throw new Exception("Error configuring data service service cannot be null."); } // if return workflowDataService; } // lock } 6.下面我们将添加我们(私有属性)的构造器和析构器。有了桥接器类后,我们需要确 保在桥接器对象和桥接服务对象间不会造成循环的引用。你需要添加下面的代码: private WorkflowMVDataService(Guid instanceID) { _instanceID = instanceID; MVDataConnector.MVDataService = this; } ~WorkflowMVDataService() { // Clean up _workflowRuntime = null; _dataExchangeService = null; _dataConnector = null; } 7.尽管我们为桥接服务类添加了一些重要的东西,但还没有把 ExternalDataService 引入工作流运行时中,我们仍然要添加一些代码,以使工作流运行时具有读取数据并返回给 宿主应用程序的能力。桥接器对象实际上是维持该连接状态,但宿主使用这个服务来获得要 访问的数据。下面是我们要添加的 read 方法: public DataSet Read() { return _dataConnector.MVData; } 8.要为我们的桥接服务添加的最后的功能块是一个方法,它激发“机动车数据更新 (motor vehicle data update)”事件。工作流使用这个方法来为宿主发送一个通知,告知 要挑选的数据已经获取完了。代码如下: public void RaiseMVDataUpdateEvent() { if (_workflowRuntime == null) _workflowRuntime = new WorkflowRuntime(); _workflowRuntime.GetWorkflow(_instanceID); // loads persisted workflow in stances if (MVDataUpdate != null) { MVDataUpdate(this, new MVDataAvailableArgs(_instanceID)); } // if } 完整的桥接服务代码参见清单 8-4: 清单 8-4 完整的 WorkflowMVDataService.cs 源文件 using System; using System.Collections.Generic; using System.Text; using System.Workflow.Activities; using System.Workflow.Runtime; using System.Data; namespace MVDataService { public class WorkflowMVDataService { static WorkflowRuntime _workflowRuntime = null; static ExternalDataExchangeService _dataExchangeService = null; static MVDataConnector _dataConnector = null; static object _syncLock = new object(); public event EventHandler MVDataUpdate; private Guid _instanceID = Guid.Empty; public Guid InstanceID { get { return _instanceID; } set { _instanceID = value; } } public static WorkflowMVDataService CreateDataService(Guid instanceID , WorkflowRuntime workflowRuntime) { lock (_syncLock) { // If we're just starting, save a copy of the workflow runtim e reference if (_workflowRuntime == null) { // Save instance of the workflow runtime. _workflowRuntime = workflowRuntime; } // if // If we're just starting, plug in ExternalDataExchange servi ce if (_dataExchangeService == null) { // Data exchange service not registered, so create an // instance and register. _dataExchangeService = new ExternalDataExchangeService(); _workflowRuntime.AddService(_dataExchangeService); } // if // Check to see if we have already added this data exchange s ervice MVDataConnector dataConnector = (MVDataConnector)workflowRunt ime. GetService(typeof(MVDataConnector)); if (dataConnector == null) { // First time through, so create the connector and // register as a service with the workflow runtime. _dataConnector = new MVDataConnector(); _dataExchangeService.AddService(_dataConnector); } // if else { // Use the retrieved data connector. _dataConnector = dataConnector; } // else // Pull the service instance we registered with the connectio n object WorkflowMVDataService workflowDataService = MVDataConnector.M VDataService; if (workflowDataService == null) { // First time through, so create the data service and // hand it to the connector. workflowDataService = new WorkflowMVDataService(instanceI D); MVDataConnector.MVDataService = workflowDataService; } // if else { // The data service is static and already registered with // the workflow runtime. The instance ID present when it // was registered is invalid for this iteration and must be // updated. workflowDataService.InstanceID = instanceID; } // else return workflowDataService; } // lock } public static WorkflowMVDataService GetRegisteredWorkflowDataService( Guid instanceID) { lock (_syncLock) { WorkflowMVDataService workflowDataService = MVDataConnector.M VDataService; if (workflowDataService == null) { throw new Exception("Error configuring data service service cannot be null."); } // if return workflowDataService; } // lock } private WorkflowMVDataService(Guid instanceID) { _instanceID = instanceID; MVDataConnector.MVDataService = this; } ~WorkflowMVDataService() { // Clean up _workflowRuntime = null; _dataExchangeService = null; _dataConnector = null; } public DataSet Read() { return _dataConnector.MVData; } public void RaiseMVDataUpdateEvent() { if (_workflowRuntime == null) _workflowRuntime = new WorkflowRuntime(); _workflowRuntime.GetWorkflow(_instanceID); // loads persisted wor kflow instances if (MVDataUpdate != null) { MVDataUpdate(this, new MVDataAvailableArgs(_instanceID)); } // if } } } CallExternalMethod 活动 你目前在本章看到过的所有代码都已支持一个特殊的 WF 活动:CallExternalMethod 活 动。CallExternalMethod 活动的作用是可以接受一个接口及该接口所支持的方法,并来调 用这个方法。现在的问题是,由谁来实现这个方法? 你可能会考虑由你的宿主应用程序来完成,但这不太正确。假如你向前看看前面的一节 “创建桥接器类”,你实际上在那里会找到这个方法。数据连接器由 ExternalDataService 捆住实现了该方法。该数据服务依次把该方法的调用转换成一个宿主应用程序能识别的事 件。 直接使用 CallExternalMethod 活动是允许的,你甚至可以绕开一些服务代码就可把它 插入到你的应用程序中。但是绕开这些服务代码对你来说还有一组难题。你的宿主应用程序 和你的工作流实例是一对一地联系在一起的。在这里使用数据服务来完成这件事要更适合一 些,当你把该数据服务结合起来后,你 就 能 使 许多的应用程序实例从许多的工作流实例中进 行数据访问,而绕过你创建好的那些数据服务后则不能做到这些。 对于直接使用 CallExternalMethod 活动,它通常更适合于创建自定义活动来为你调用 外部方法。你可使用一个工具来自定义你的数据交换接口和创建派生自 CallExternalMethod 的活动,更恰当地对其命名,对它们的属性(接口和方法名称)进行 配置。接下来我们就来看看该工具的使用方法。 创建和使用自定义外部数据服务活动 回头看看,我们刚刚写下的代码比目前整本书已写过的代码还要多。原因是 WF 事先不 知道我们的工作流将和我们的宿主应用程序之间交换些什么信息。因此在二者之间毫无疑问 必须做一些工作,以便对它们之间的差距进行填充。 但是,WF 知悉所有的工作流活动,我们可愉快地使用一个工具来对我们的数据传送接 口进行解释,使用 ExternalDataExchange 特性(attribute)来进行标记,自动地生成 WF 活动。 我们本章正生成的应用程序把数据从工作流中发送到宿主应用程序中,也就是说数据传 送是单向的。我故意这样做是因为,我们只有积累了足够的知识,才能更好地学习并充分理 解双向数据传送。我们将使用的 Workflow Communication Activity 生成器完全有能力创建 那些发送和接受宿主数据的活动。对于本应用程序的特殊性,我们将“ 扔 掉 ”它的输出部分, 因为我们不需要它。(其实,将生成的活动是畸形的,因为我们的接口没有指定宿主到工作 流的通信,这些我们将保留到第 10 章讲解。) 为此,我们就来执行 wca.exe 并创建一个可用来发送数据到我们的宿主应用程序的活 动。 创建通信活动 1.为使 wca.exe 能生成符合我们要求的代码,我们需要确保有一个接口。因此,确保 MVDataService 项目生成时无错误(如生成时有错误,请纠正所有的错误)并已生成了 MVDataService 程序集。 2.点击操作系统的开始按钮,然后点击运行菜单项打开运行对话框。 3.在打开的组合框控件中输入“cmd”, 然 后 点击确定进入命令提示符窗口。 4.更改起始目录以便我们能直接访问到“MVDataService”程序集。通常使用的是“cd” 命令。 5.wca.exe 文件默认情况下被安装到 Program Files 目录下的 Windows SDK 子目录中。 (当然,假如你没有使用默认目录进行安装,你在此需要使用你安装 Windws SDK 的目录。) 在命令行提示符下输入下面的命令来执行该工具(包含双引号): “C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\Wca.exe” MVDataService.dll 按下回车键,该工具的输出结果和下面的类似: 6.在命令提示符中键入 dir 你可看到 wca.exe 创建的文件。 7.IMVDataService.Sinks.cs 文件不是必须要的,可忽略它甚至是删除它,因为该文件 只是包含了一些指示,没有代码。(在我们的通信接口中没有定义事件。)我们在第十章将再 使用这个文件。对于另一个生成的文件:IMVDataService.Invokes.cs 文件,是我们要保留 的文件。它 包含我们能使用的一个新活动的源代码,该活动可把数据从工作流中发送给宿主 应用程序。因此,我们将重命名该文件以便更加好用。在命令提示符下输入“ren IMVDataService.Invokes.cs MVDataUpdate.cs”, 然 后 按 下回车键重命名该文件。 8.因为我们刚刚重命名的这个文件是一个工作流活动,因此我们需要把它从当前目录下 移动到 MVWorkflow 目录下以便编译和使用。在命令提示符下输入“move MVDataUpdate.cs ..\..\..\MVWorkflow”, 然 后 按 下回车键。 9.回到 Visual Studio,我们需要把这个新创建的 MVDataUpdate.cs 文件添加我们的工 作流项目中。 10.编译并生成 MVWorkflow 项目,假如出现编译错误的话,修正所有的错误。在你成功 编译后,在视图设计器界面模式下打开 Workflow1.cs 文件将会在 Visual Studio 的工具箱 中看到这个 MVDataUpdate 活动。 备注:假如 MVDataUpdate 因为某些原因没有添加进 Visual Studio 工具箱中,请关闭 该解决方案,然后再重新打开它。 我们现在就准备好了一个活动,我们可使用它来把数据发送到我们的宿主应用程序中。 该活动的基类是 CallExternalMethod,它的作用是触发对工作流执行环境的外部调用。 添加并配置该工作流通信活动 1.在 Visual Studio 中以视图设计器的模式打开 MVWorkflow 项目中的 Workflow1.cs 文件。该工作流预先加载了两个活动:一个是 Delay 活动,用来模拟处理数据的等待时间; 一个是 Code 活动,它创建并填充一个基于驾驶员姓名的 DataSet。 2.打开 Visual Studio 工具箱,定位到 MVDataUpdate 活动。 3.把该活动拖拽到工作流视图设计器界面上,放到 Code 活动的下面使它在 Code 活动执 行后执行。 4.我们的工作流在视图设计器上的设计工作这就完成了。现在该写少量代码来进行衔 接。因此以代码视图模式打开 Workflow1.cs 文件。在 Workflow1 类中找到 GenerateMVDData 方法。这个方法是 codeActivity1 所执行的方法,在 里 面 你 会 看到对 GenerateVehicleTable 和 GenerateViolationTable 两个方法的调用,它 们创建并填充所要返回的 DataSet。(其实, 你可用一些外部服务来为驾驶员的信息进行一个调用,但我们在此对这些进行了模拟)。在 生成了 DataSet 后,我们需要添加下面的代码以把 DataSet 返回给宿主: // Assign the DataSet we just created as the host data mvDataUpdate1.mvData = ds; 指定了我们所返回的 DataSet 后,我们就完成了工作流的开发,并且使用了工具来把该 DataSet 传给宿主应用程序。但我们的宿主应用程序需要做些什么才能接收到该数据了? 在宿主应用程序中检索工作流数据 现在让我们返回到我们的主应用程序中。我们现在要做的是修改应用程序,以使用我们 在本章的“创建外部数据访问”这一节中创建的桥接类。 备注:尽管这是一个简化的例子,但这个应用程序仍然是一个完全意义上的 Windows Form 应用程序,它演示了怎样处理工作流及其怎样进行多线程操作(比如 updating 控制的 时候)。 为了让我们的接口可使用工作流返回的数据集,我们需要使用桥接代码中的 connector 类来对我们的工作流实例进行注册(为了使我们能正确的接收 DataSet)。我们也需要勾住 (hook)MVDataUpdate 事件,以便我们的应用程序知道接收数据的时间。为方便做这些事。 我们将为“Retrieve MV Data”按钮的 event handler 添加一点代码,并为 MVDataUpdate 添加一个新的 event handler。 备注:假如你不熟悉匿名方法(anonymous methods)的话,现在就是简要学习它的一 个好机会! 为我们的宿主应用程序添加工作流外部数据服务 1.在 Visual Studio 解决方案资源管理器中打开 Form1.cs 文件,并切换到代码视图界 面。 2.找到 cmdRetrieve_Click 方法。在 该 响 应 按 钮 点击的事件方法中已经存在了初始化工 作流实例的代码,但我们还需要在创建工作流实例和启动该实例之间的地方插入一些代码, 也就是在调用“_workflowRuntime.CreateWorkflow”的下面添加如下的代码(为让 Visual Studio 为我们自动生成事件处理程序的代码,请尽量不要使用复制粘贴的方式,应在=号后 使用连续两个 Tab 键): // Hook returned data event. MVDataService.WorkflowMVDataService dataService = MVDataService.WorkflowMVDataService.CreateDataService( _workflowInstance.InstanceId, _workflowRuntime); dataService.MVDataUpdate += new EventHandler( dataService_MVDataUpdate); 3.在 Form1 类中,为 Visual Studio 刚刚创建的 dataService_MVDataUpdate 事件处理 程序添加下面的事件处理代码,并去掉存在的“not implemented”异常。 IAsyncResult result = this.BeginInvoke( new EventHandler( delegate { // Retrieve connection. Note we could simply cast the sender a s // our data service, but we'll instead be sure to retrieve // the data meant for this particular workflow instance. MVDataService.WorkflowMVDataService dataService = MVDataService.WorkflowMVDataService. GetRegisteredWorkflowDataService(e.InstanceId); // Read the motor vehicle data DataSet ds = dataService.Read(); // Bind the vehicles list to the vehicles table ListViewItem lvi = null; foreach (DataRow row in ds.Tables["Vehicles"].Rows) { // Create the string array string[] items = new string[4]; items[0] = (string)row["Plate"]; items[1] = (string)row["Make"]; items[2] = (string)row["Model"]; items[3] = (string)row["Color"]; // Create the list item lvi = new ListViewItem(items); // Add to the list lvVehicles.Items.Add(lvi); } // foreach // Bind the violations list to the violations table foreach (DataRow row in ds.Tables["Violations"].Rows) { // Create the string array string[] items = new string[4]; items[0] = (string)row["ID"]; items[1] = (string)row["Plate"]; items[2] = (string)row["Violation"]; items[3] = ((DateTime)row["Date"]).ToString("MM/dd/yyyy"); // Create the list item lvi = new ListViewItem(items); // Add to the list lvViolations.Items.Add(lvi); } // foreach } // delegate ), null, null ); // BeginInvoke this.EndInvoke(result); // Reset for next request WorkflowCompleted(); 就这样!我们的应用程序就完成了,编译并执行该应用程序。当你点击“Retrieve MV Data”按钮时,选中的驾驶员姓名就会被传给工作流实例。当 DataSet 创建好后,该工作流 实例就会激发 MVDataUpdate 事件。宿主应用程序代码会截获该事件进行数据的接收,然后 把它绑定到 ListView 控件。 在最后一步我们需注意一个关键的地方,就是在我们调用 WorkflowMVDataService 的静 态方法 GetRegisteredWorkflowDataService 来检索数据服务包含的 DataSet 后,我们使用 数据服务的 Read 方法来把该 DataSet 拉进我们的宿主应用程序执行环境中以便我们进行数 据绑定。 用 InvokeWorkflow 调用外部工作流 这儿要问你一个问题:假如你有一个正在执行的工作流,该工作流能执行第二个工作流 吗? 答案是 Yes!有这样一个活动,InvokeWorkflow 活动,可用它来启动第二个工作流。我们通 过一个例子来简要地看看这个活动。我们将创建一个新的控制台应用程序示例来启动一个工 作流,该工作流只是向控制台输出一条信息。在输出该信息后,该工作流实例启动第二个工 作流实例,被启动的工作流实例也输出一条信息,这样就生动地为我们展示了两个工作流都 执行了。 调用第二个工作流 1.和前面一样,本章的源代码中包含了完整版和练习版两种版本的 WorkflowInvoker 应用程序。我们现在打开练习版的 WorkflowInvoker 解决方案。 2.在 Visual Studio 加载 WorkflowInvoker 解决方案后,在 WorkflowInvoker 解决方案 中添加一个新的基于顺序工作流库的项目,工作流的名称命名为:Workflow1,保存该项目。 3.下一步,从工具箱中拖拽一个 Code 活动到工作流视图设计器界面上。在该活动的 ExecuteCode 属性中键入“SayHello”, 然 后 按 下回车键。 4.Visual Studio 会自动切换到代码编辑界面。定位到 Visual Studio 刚刚添加的 SayHello 方法,在该方法内输入下面的代码: // Output text to the console. Console.WriteLine("Hello from Workflow1!"); 5.我们现在需要添加第二个要执行的工作流,因此重复步骤 2,但工作流的名称命名为: Workflow2。重复步骤 3 和 4,但把信息“Hello from Workflow1!”替换为“Hello from Workflow2!”, 当 然 工作流源文件的名称也要重命名为 workflow2.cs,以避免冲突。 6.我们想在第一个工作流中调用第二个工作流,但这样做,我们还需要添加对第二个工 作流的引用。在这之前,我们需要编译并生成 Workflow1。 7.回到 Visual Studio 解决方案资源管理器,为 Workflow1 项目添加对项目 Workflow2 的项目级引用。 8.回到 Workflow1 的工作流视图设计器界面上。这次,拖拽一个 InvokeWorkflow 活动 到你的顺序工作流视图设计器界面上。 9.看看这个新活动的属性,我们会看到它有一个“ TargetWorkflow”属性需要我们去设 置。点击以激活它的 TargetWorkflow 属性,然后点击它的浏览(...)按钮(该按钮带三个 点)。 10.这将打开一个“ 浏览 和 选择一个.NET 类型”对话框。在 左边面板中选择 Workflow2, 这将在右边的面板中显示 Workflow2 类型。在右边的面板中选择 Workflow2 类型 (Workflow2.Workflow2 是它的完全限定名称),然后点击确定。 11.然后 Visual Studio 会检查该 Workflow2 工作流,并在工作流视图设计器的 InvokeWorkflow 活动内部展示它的图形界面。 12.工作流现在就完整地实现了,我们现在需要为 WorkflowInvoker 项目添加对 Workflow1 和 Workflow2 的项目引用。 13.接下来在 Program.cs 文件中定位到下面的代码上: Console.WriteLine("Waiting for workflow completion."); 14.在上面的代码下添加如下代码: // Create the workflow instance. WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(Workflow1.Workflow1)); // Start the workflow instance. instance.Start(); 15.我们现在将为宿主应用程序添加少量的代码,以便每个工作流完成后通知我们。在 WorkflowCompleted 的事件处理程序中插入下面的代码: if (e.WorkflowDefinition is Workflow1.Workflow1) Console.WriteLine("Workflow 1 completed."); else Console.WriteLine("Workflow 2 completed."); waitHandle.Set(); 第一个完成的工作流设置 AutoResetEvent,以便强制应用程序等待工作流完成。我们 可添加代码以使应用程序等待所有的工作流,但出于展示的目的这已足够。假如你编译并执 行该 WorkflowInvoker 应用程序,你将在控制台中看到下面图 8-4 中所展示的输出结果。假 如输出信息的顺序有些许的不同,不用吃惊,这是多线程程序的特征。 图 8-4 WorkflowInvoker 应用程序的控制台输出 第九章:逻辑流活动 学习完本章,你将掌握: 1.学会怎样使用 IfElse 活动来执行条件表达式 2.学会怎样使用 While 活动来执行循环 3.理解 Replicator 活动是怎样来模拟 for 循环的,以及它的使用方法。 我们已经看到过怎样执行工作流内部和外部的代码,已经知道怎样处理异常,暂停进程, 在事情脱离控制时终止我们的工作流。但无疑对于任何一个计算机系统的主要组成部分来 说,都应具有根据运行时的条件做出判断以执行不同的任务的能力。在本章,我们将演示要 求我们应付 if/else 场景及基本的循环的一些工作流活动。 条件及条件处理 现在,你可能不会感到奇怪,你发现 WF 提供了基于运行时的条件进行逻辑处理控制流 的活动。毕竟,假如 WF 提供了活动去抛出并捕获异常,那它为什么就没有相应的活动来根 据工作流的执行情况进行检测并根据它们获取的结果作出决策呢? 我们将在本章中进行测试的活动包括 IfElse 活动、While 活动和 Replicator 活动。 IfElse 活动的作用是测试一个条件并根据测试结果执行不同的工作流路径。While 活动用来 执行一个 While 循环。而对于 for 循环,则是使用 Replicator 活动来完成。现在通过本章 的示例应用程序开始我们的学习。 备注:在本章你将依靠 CodeCondition 来进行条件的处理,它(CodeCondition)的意 思是你将写下 C#代码来处理条件表达式。在 12 章(“策略和规则”)中,你将使用 RuleCondition 来对条件表达式的值进行处理,RuleCondition 使用了 WF 的基于规则的处理 方式。两种方式都同样有效。 Qustioner 应用程序 本章的示例应用程序是一个 Windows Form 应用程序,它会请你回答三个问题,问题内 容你能够进行修改。(问题的内容保存在应用程序的 settings property 中。)你也可指定 这些问题是各自独立还是相互关联的。 当工作流开始执行时你要把这些问题和相关的情况传入该工作流。相互关联的问题只有 在前面的问题回答正确时才会被进一步提出。例如,假如有人问你:“谈到的文档你看过 吗?”,假如你没有,则没多大意义问接下来这一问题:“这个文档你批准吗?”假如问题 是相关的,则 第一个问题回答是否定的话,就将返回否定的回答,余下的问题不予考虑也都 将返回否定的回答。 各自独立的问题要求你必须回答,而不管前面的问题中你回答的是什么。例如这个问题, “你喜欢冰淇淋吗?”就和问题“现在外面在下雨吗?”是不相关的。无论你喜不喜欢冰淇 淋,你的答案都和外面的天气这个问题是各自独立的。对于相互独立的问题来说,不 管 你在 前面的问题中是肯定还是否定的回答,都会进一步被问到。 用户界面如图 9-1。假如你修改三个问题中的任何一个的内容,新问题的都将自动地保 存到你的应用程序的 settings property 中(问题的类型也一样)。这些问题会产生“是/ 否”的回答,使工作流能够把这些回答作为一个 Boolean 类型的数组传回到宿主应用程序中。 图 9-1 Questioner 主应用程序界面 当你点击 Execute 按钮时,这些问题通过带“是”和“否”按钮的信息框依次呈现。一 旦工作流处理完所有的这些问题,它 就 返回一个 Boolean 数组给宿主应用程序。宿主应用程 序将检查该数组以显示不同的用户界面。 当工作流执行时,回答结果将以蓝色圆球的形式显示(如图 9-1)。当工作流任务完成 后,通过的回答将以绿色圆球的形式出现,未通过的回答将以红色圆球的形式出现。假如所 有的回答都通过了,则“最终回答结果”图片将以绿色圆球的形式呈现。但是,假如三个问 题中的任何一个没有通过,则“最终回答结果”图片将以带“8”字的圆球的形式呈现。如 图 9-2。 图 9-2 Questioner 应用程序执行期间的用户界面 对你来说,使用这个应用程序的目的是测试本章中的三个活动。第一次迭代,Questioner 将使用 IfElse 活动来判断要执行什么动作过程。第二次迭代时这些问题仍然会被问到,我 们将使用 While 活动来提问。最后一次迭代我们将使用 Replicator 活动来模拟 for 循环进 行提问。对于该应用程序的每一次迭代,我们都将使用前一章中演示的技术来把回答的结果 传回给宿主应用程序。 使用 IfElse 活动 IfElse 活动的作用是对 if-then-else 条件表达式进行模拟,其实你在前几章使用过这 个活动。 IfElse 活动要求你提供一个条件表达式,它其实是作为一个 event handler 执行。你创建 的 event handler 有一个类型为 ConditionalEventArgs 的参数,它有一个 Boolean 类型的 (名称为)Result 属性。你可对其进行 set,以指明该条件表达式的结果。 IfElse 活动根据 Result 的值来指挥工作流到底该执行两个分支中的哪一个。在 Microsoft Visual Studio 的工作流视图设计器中,true 执行的是显示在左边的路径,而 false 执行的是右边的路径。两个分支都可作为其它活动的容器,允许你插入任何一个你需 要的工作流活动。 备注:通过本节的学习,你可能会认为,IfElse 活动可能不是构建下面的工作流的最 合适的活动。你在本章的后面部分将找到更加适合下面特定的工作流的活动。 使用 IfElse 活动创建 QuestionFlow 工作流 1.下载本章源代码,打开 IfElse Questioner 文件夹中的解决方案。 2.看看 Visual Studio 解决方案资源管理器,你 会 看到解决方案的层次结构和前一章中 的相似。主应用程序的文件位置在 Questioner 项目中,而宿主通信服务文件的位置则在 QuestionService 项目中。为使你把注意力放到工作流上,我已经创建了服务接口(具体过 程参见前一章):IQuestionService,并且使用 wca.exe 工具(使用方法参见前一章)生成 了一个必需的通信活动:SendReponseDataToHost。现在,找到 QuestionFlow 项目的 Workflow1.cs 文件并在视图设计器中打开它。 3.从工具箱中拖拽一个 IfElse 活动到设计器界面上。 4.你会看到一个内含感叹号(!)标记的红色圆圈,这是提醒你还需要更多的信息才能 编译你的工作流。其实,缺少的就是条件表达式!选中 ifElseActivity1 的左边分支以便在 Visual Studio 的属性面板上呈现该活动的属性。选中它的 Condition 属性以激活它的下拉 列表框,然后从列表中选择代码条件。 备注:你通常有两种方式来对条件表达式进行选择:code(代码)和 rules-based(基于规 则)。我们这里将使用基于代码的条件表达式,基于规则的技术将保留到第 12 章(策略活 动)进行学习。 5.展开显示的 Condition 属性,输入 AskQuestion1,然后按下回车键,Visual Studio 这就为你插入了 AskQuestion1 的事件处理程序并会切换到代码视图下。现在,重新回到工 作流视图设计器上,你还要把更多的活动添加到你的工作流中。 6.拖拽一个 Code 活动到设计器界面上,并把它放到 ifElseActivity1 的右边分支上。 7.指定它的 ExecuteCode 属性值为 NegateQ1。当 Visual Studio 插入了 NegateQ1 事件 出现程序后,重新回工作流视图设计器界面上。 8.重复步骤 6 和步骤 7,在 ifElseActivity1 的左边分支上也添加一个 Code 活动。 指定它的 ExecuteCode 属性值为 AffirmQ1,但是,当 Visual Studio 插入了 AffirmQ1 事件出现程序后,不要切换回工作流视图设计器界面上。相反,我们要添加一些代码。 9.我们现在需要为该工作流类添加一些属性,当我们起动工作流进程时可把它们作为参 数。在 Workflow1 的构造器的下面,添加下面的代码,它 们 包含有工作流将问到的三个问题: private string[] _questions = null; public string[] Questions { get { return _questions; } set { _questions = value; } } 10.我们也需要添加一个 Dependent 属性,它用来告知这些问题彼此是否是相关的。在 上面所添加的代码下,添加如下代码: private bool _dependent = true; public bool Dependent { get { return _dependent; } set { _dependent = value; } } 11.问题回答的结果是一些 Boolean 值,在 传 回 给 宿 主应用程序前需要保存到某些地方。 因此,在上面所插入的代码下添加该字段: private bool[] _response = null; 12.该_response 字段没有被初始化,因此找到 Workflow1 的构造器,在里面的 InitializeComponent 方法下面添加如下的代码: // Initialize return vector. _response = new bool[3]; _response[0] = false; _response[1] = false; _response[2] = false; 13.现在找到 Visual Studio 为你添加的 AskQuestion1 事件处理程序(event handler)。 在该事件处理程序中添加下面的代码: // Ask the question! DialogResult result = MessageBox.Show(Questions[0], "Questioner:", MessageBoxButtons.YesNo, MessageBoxIcon.Question); e.Result = (result == DialogResult.Yes); 14.对于 NegateQ1 事件处理程序,则添加下面的代码: // Negate answer. _response[0] = false; if (Dependent) { // Negate remaining answers. _response[1] = false; _response[2] = false; } 15.接下来,在 AffirmQ1 事件处理程序中添加下面的代码: // Affirm answer. _response[0] = true; 16.你现在就已为第一个问题的提问添加了对应的工作流组成部分,但是这还有两个问 题。对于第二个问题,重复步骤 3 至步骤 8 来新添加一个 IfElse 活动添加到工作流中,把 和问题 1 相关的地方用问题 2 替换掉,例如插入的事件处理程序就应该是 AskQuestion2、 NegateQ2 和 AffirmQ2。工作流视图设计器的界面现在如下所示: 17.现在找到 AskQuestion2 事件处理程序并添加下面的代码: if (_response[0] == false && Dependent) { // No need to ask! e.Result = false; } else { // Ask the question! DialogResult result = MessageBox.Show(Questions[1], "Questioner:", MessageBoxButtons.YesNo, MessageBoxIcon.Question); e.Result = (result == DialogResult.Yes); } 18.对于 NegateQ2 事件出现程序,添加下面的代码: // Negate answer _response[1] = false; if (Dependent) { // Negate remaining answer _response[2] = false; } 19.对于 AffirmQ2 事件处理程序,添加下面的代码: // Affirm answer. _response[1] = true; 20.在一次重复步骤 3 至步骤 8,添加第三个问题,把和问题 1 相关的内容替换掉(方 法和添加第二个问题时一样)。此时,工作流视图设计器的界面如下所示: 21.找到 AskQuestion3 事件处理程序,插入下面的代码: if (_response[1] == false && Dependent) { // No need to ask! e.Result = false; } else { // Ask the question! DialogResult result = MessageBox.Show(Questions[2], "Questioner:", MessageBoxButtons.YesNo, MessageBoxIcon.Question); e.Result = (result == DialogResult.Yes); } 22.对于 NegateQ3 事件处理程序,添加下面的代码: // Negate answer. _response[2] = false; 23.对于 AffirmQ3 事件处理程序则添加下面的代码: // Affirm answer _response[2] = true; 24.现在回到工作流视图设计器。你会在工具箱中找到一个名称为 SendResponseDataToHost 的自定义活动。(注意:假如工具箱中没有这个 SendResponseDataToHost 活动,则编译该项目再重新看看。) 25.拖拽一个 SendResponseDataToHost 活动到你的工作流视图设计器界面上,把它放到 第三个 IfElse 活动(ifElseActivity3)的下边。 26.因为返回的数据是一个简单的 Boolean 类型的数组,因此这里的处理方式和前一章 有点点区别。和你去添加一个容纳该 Boolean 类型的数组的依赖属性不同, SendResponseDataToHost 活动使用一个字段来容纳该数据,创建该字段的用户界面也就和 你在第七章中看到的不同。在 Visual Studio 属性面板上选中 responses 属性,然后点击浏 览(...)按钮。 这打开了如下面的 Boolean 集合编器对话框。 27.点击添加按钮,共重复三次,保留这些默认的 False 值,然后点击确定按钮。Visual Studio 就为你把包含三个 Boolean 元素的数组添加进了你的 Workflow1.designer.cs 文件 的代码中。 提示:在下面的第 28 步,你将添加一个 CodeActivity,用它来把你在第 11 步添加的 _response 字段分配到我们刚刚为 SendResponseDataToHost 创建的 Boolean 数组中。但是, 你也可直接使用 SendResponseDataToHost 的一个我们已经创建的 response 属性( 来对它进 行访问)。我选择这样做仅仅是因为(从阐述的角度来说)这样更有意义,这可展示出在涉 及到宿主通信活动前该 Ifelse 活动是怎样添加和工作的。 28.现在我们需要把保存了问题回答结果的数组的值和将要使用这些值的 SendResponseDataToHost 活动联系起来。因此,我们现在就拖拽一个 Code 活动到工作流视 图设计器的界面上,把它放到第三个 IfElse 活动(ifElseActivity3)和 SendResponseDataToHost 活动(sendResponseDataToHost1)之间。 29.设置该 Code 活动的 ExecuteCode 属性为 CopyResponse,然后按下回车键。 30.在 Visual Studio 插入的 CopyResponse 事件处理程序中添加下面的代码: // Assign outgoing data. sendResponseDataToHost1.responses = _response; 31.编译并运行该应用程序。改变问题的 Dependency 属性,看看在回答这些问题时,作 出 否 定 的回答其结果一样吗? 使用 While 活动 假如你回头看看前一节,你会至少注意到两件事。首先,毫无疑问你体验了 IfElse 活 动;第二,它用了 31 个独立的步骤来创建了该工作流。有些程序结构使用 if-then-else 来进行处理很合适,但这个特殊的应用程序使用循环结构来进行问题的提问会更合适些。这 些 将在接下来演示。你将使用另一个使用了 while 循环的工作流来替换你刚刚创建好了的工 作流。 WF 的 While 活动处理条件表达式时的过程和 IfElse 活动相似。它 触 发 了一个对循环是 否继续进行验证的事件,它使用 ConditionalEventArgs 来返回你的判断结果(也要使用 Result 属性)。 但是,和 IfElse 活动不同的是,你在使用 While 活动的时候,假如设置 Result 为 true 将 导致继续进行循环,设置 Result 为 false 则终止循环。我们就来看看怎样使用 while 循环 来替换 if-then-else 进行条件处理,以简化我们的工作流。 使用 While 活动创建 QuesionFlow 工作流 1.从下载的本章源代码中使用 Visual Studio 打开 While Questioner 文件夹内的解决 方案。 2.和前一节一样,该应用程序本质上是完整的,它包含了已创建好了的 SendResponseDataToHost 活动。剩下要去完成的工作是完善工作流的处理过程。在解决方 案管理器面板上找到 QuesionFlow 项目中 Workflow1.cs 文件,然后在工作流的视图设计器 中打开它。 3.从工具箱中拖拽一个 While 活动到视图设计器界面上。 4.和 IfElse 活动相似,选中 whileActivity1 活动的 Condition 属性以激活它的下拉列 表框。从这个下拉列表框中选择代码条件。 5.展开该 Condition 属性,输入 TestComplete,然后按下回车键。Visual Studio 这就 为你添加了 TestComplete 事件程序程序,然后回到工作流的视图设计器界面上。 6.拖拽一个 Code 活动到工作流视图设计器界面上,把它放到 whileActivity1 的里面。 指定它的 ExecuteCode 的属性值为 AskQuestion。同样,在生成了 AskQuestion 事件处理程 序后重新回到工作流视图设计器界面上来。 7.为了使我们能把保存有问题回答结果的 Boolean 数组返回给宿主应用程序,我们需要 重复前面一节的第 24、25 步,以把一个 SendResponseDataToHost 活动插入到我们的工作流 中。(在这之前,需要编译该应用程序,否则 SendResponseDataToHost 活动不会在工具箱 中显示。)我们把该 SendResponseDataToHost 活动放到 whileActivity1 的下面,以 便它在 while 循环后被执行。 8.我们同样需要重复前一节的第 9 步至第 12 步,以便插入 Questions 和 Dependent 属 性,并且对_response 数组进行创建和初始化。 9.在_response 数组的声明语句下面,添加下面的代码: private Int32 _qNum = 0; 10.找到 TestComplete 事件处理程序,添加下面的代码: // Check for completion. if (_qNum >= Questions.Length) { // Assign outgoing data. sendResponseDataToHost1.responses = _response; // Done, so exit loop. e.Result = false; } else { // Not done, so continue loop. e.Result = true; } 11.我们需要完成的最后一点代码实际上就是问题的回答。在 Workflow1.cs 文件中,你 会 找 到 AskQuestion 事件处理程序,为 该 事 件 处 理程序添加下面的代码。假如问题的回答是 否定的并且 Dependent 属性是 true(表示各个问题是相关的),则所有余下的问题的回答 就都是否定的。 // Ask the question! DialogResult result = MessageBox.Show(Questions[_qNum], "Questioner:", MessageBoxButtons.YesNo, MessageBoxIcon.Question); _response[_qNum] = (result == DialogResult.Yes); // Check response versus dependency if (!_response[_qNum] && Dependent) { // Negate remaining questions while (_qNum < Questions.Length) { // Negate this question _response[_qNum] = false; // Next question ++_qNum; } } // if else { // Set up for next iteration ++_qNum; } 12.重复前一节的步骤 28 至步骤 30,我们将使用 Code 活动来把保存了问题回答结果的 数组传给 SendResponseDataToHost 活动。(该 Code 活动放到 While1 活动和 SendResponseDataToHost1 活动中间。) 13.编译并执行该应用程序。 假如你花一些时间来比较一下本节和上一节的最终的工作流视图设计器界面,你很容易 发现使用 While 活动大大简化了工作流的处理。整个工作流视图设计器界面简单多了! 既然有和 while 循环等价的工作流活动,是否也有和 for 循环等价的工作流活动呢?答 案是肯定的,它就是 Replicator 活动。 使用 Replicator 活动 说 Replicator 活动和 C#术语中的 for 循环是等价的可能并不正确。C#语言规范 1.2 中 告诉我们,C#中的 for 循环看起来和下面的类似: for(for-initializer;for-condition;for-iterator) embedded-statement embedded-statement(内嵌语句)在 for-condition(条件判断)的值为 true 时执行 (如果被省略,则假定为 true),循环由 for-initializer 开始,每一次循环都要执行 for-iterator。这当中没有涉及到任何的 C#声明组件。对于 replication 来说,我们可以 设想它是一个能重复地对源代码进行准确复制的软件工厂。C#的 for 循环则不是以这种方式 进行操作的。 事实上,“重复”的概念在我们看到 WF 中和 for 循环等价的活动这一说明后就不会感 到可怕了。假如你熟悉 ASP.NET 的话,你或许使用过 Repeater 控件。这个 ASP.NET的 Repeater 控件有一个项模板(同时也有一个项替换模板),它 能 根 据所绑定的数据项的数目进行重复 处理。 Replicator 活动和 ASP.NET 的绑定到基于 IList 数据源的 Repeater 控件相似,它 重 复 它 所 包含的子活动,基于 IList 的数据源中的每个元素就相当于一个子活动。但是 Replicator 活动和 C#中的 for 语句在有些方面是相似的,因为它也有一个循环初始化事件 (这就像是 for-initializer)、一个循环完成事件(这和用 for-iterator 和 for-condition 做比较类似)以及一个循环继续事件(和 for-condition 类似)。它提供了这些事件来指明 重复性的(和嵌入语句相似)子活动的创建工作,以便你能个性化地进行数据绑定,子活动 完成了它就触发一个事件,以便你能为每个子活动实例执行一些清理和管理的任务。 Replicator 活动必须接受也只接受一个唯一的活动,它能作为其它活动的容器(和 Sequence 活动类似),它触发一个开始执行的初始化事件。在初始化事件的执行期间,你 可把一个基于 IList 的集合绑定到 Replicator 活动的 InitialChildData 属性中。 该 Replicator 活动然后会重复你所提供的子活动,它们的次数和基于 IList 集合中的 项的数目相等。这些子活动实例能够以依次按顺序的方式或以并行的方式执行(这可通过 ExecutionType 属性进行设置)。UntilCondition 事件在每一个子活动执行前触发,在处 理 UntilCondition 事件时,你可通过设置 ConditionalEventArgs 的 Result 属性为 false 来 通知 Replicator 活动继续执行(为 true 则终止循环)。表 9-1 简要地列出了我们需要关注 的 Replicator 活动的一些属性,而表 9-2 列出了在我们的工作流中使用 Replicator 活动时 需要进行处理的一些事件。表 9-1 Replicator 活动的属性 属性 功能 ExecutionType 获取或设置 Replicator 活动的 ExecutionType(一个枚举值)。该 ExecutionType 的枚举值包含 Parallel 和 Sequence。 InitialChildData 获取或设置子活动数据的一个 IList 集合。该属性和其它.NET 技术中的 data-binging(数据绑定)属性相似,事实上要分配给该属性的对象必 须是一个 IList 对象。Replicator 活动会为分配给该属性的基于 IList 集合中的每一项创建一个子活动实例。 表 9-2 Replicator 活动的事件 事件 功能 ChildCompleteEvent 在 Replicator 活动中的子活动实例已完成后触发。对于每一个循 环触发一次。 ChildInitializedEvent 在 Replicator 活动中的子活动实例初始化后触发。对于每一个循 环触发一次。 CompleteEvent 在 Replicator 活动已完成后触发(也就是说,在所有循环中的子 活动实例都已执行完成)。 InitializedEvent 在 Replicator 活动开始执行时触发。该事件只触发一次,在所有 的子活动执行前触发。 UntilCondition UntilCondition 在许多的 WF 文档中都是以属性的方式列出的,它 其实表示的是一个事件处理程序,这和 Code 活动的 ExecuteCode 属性的作用一样(通过 ExecuteCode 属性就把一个去执行相应代码 的事件处理程序和相应的 Code 活动联系了起来)。这个事件在每 一个子活动实例执行前触发。它的 ConditionalEventArgs 事件参 数控制了循环是否继续进行执行。指定 Result 的值为 false 则允 许子活动继续执行。而指定 Result 的值为 true 则导致 Replicator 活动停止所有子活动的执行。 图 9-3 为你提供了一个基本的流程图,它显示了在什么地方触发什么事件。 图 9-3 Replicator 活动的事件触发顺序流程图 你在图 9-3 中看到的基于 IList 的集合是通过 InitialChildData 属性指定的,你也可 在 Initialized 事件处理前或处理期间进行指定。该工作流也没有说明所复制(生成)的子 活动能以顺序或者以并行的方式执行,这取决于 ExecutionType 属性的设置。 在现实情形中怎样使用 Replicator 活动呢?从目前的描述来看,它比真实的使用情形 要复杂得多。事实上,它的机制和其它活动相比并没有多大的区别。拖拽该活动到工作流视 图设计器界面上,为各中事件处理程序指定值,再拖拽一个唯一的子活动到 Replicator 活 动当中。该唯一的子活动和 Replicator 活动本身一样,也能作为一个(其它活动的)容器 (就像 Sequence 活动一样),因此事实上多于一个的活动也能执行。在我们的头脑里有了 这些表和这些图,我们就可使用 Replicator 活动来重新编写 Questioner 应用程序了。 使用 Replicator 活动创建 QuestionFlow 工作流 1.下载本章源代码,打开 Replicator Questioner 文件夹内的解决方案。 2.和前两节一样,宿主应用程序本质上已经完成,这可方便你把焦点放到工作流方面。 选中 Workflow1.cs 文件并在 Visual Studio 的工作流视图设计器中打开它。 3.从工具箱中拖拽一个 Replicator 活动到视图设计器界面上,界面如下所示: 4.在 Visual Studio 属性面板上,选中 Initialized 属性并输入 InitializeLoop,然 后按下回车键。这样 Visual Studio 就在你的代码中插入了相应的事件处理程序并把界面切 换到代码编辑视图界面中。我们回到工作流视图设计器界面上来,你还要继续设置属性。 5.对于 Completed 属性,输入 LoopCompleted 并按下回车键,在添加了 LoopCompleted 事件处理程序后,和前一步骤一样,我们重新回到工作流视图设计器界面上。 6.在 ChildInitialized 属性中,输入 PrepareQuestion。在插入了 PrepareQuestion 事件处理程序后,我们再次回到工作流视图设计器界面上。 7.接着,在 ChildCompleted 属性中输入 QuestionAsked,在创建了 ChildCompleted 事 件处理程序后,我们同样回到工作流视图设计器界面上。 8.为了在问完所有的问题后(或者在问题是相关的,同时用户的回答是否定的情形下) 终止循环,我们需要添加一个事件处理程序。因此,我们需要选中 UntilCondition 属性, 从它的下拉列表框中选择“代码条件”选项。 9.对于该 UntilCondition 的 Condition 属性,我们输入 TestContinue,按下回车键, 在插入相应的事件处理程序后再次回到工作流的视图设计器界面上来。 10.对于 Replicator 活动的实例 replicatorActivity1 来说,需要一个唯一的子活动。 因此,从工具箱中拖拽一个 Code 活动到 replicatorActivity1 中。指定它的 ExecuteCode 属性为 AskQuestion。 11.在工作流视图设计器上需要完成的最后工作是把一个 SendResponseDataToHost 活 动拖拽到设计器界面上,把它放到 replicatorActivity1 活动的下面。然后重复“使用 IfElse 活动创建 QuestionFlow 工作流”这一节中的步骤 24 至步骤 30。(在这之前,你或许需要 编译该应用程序以让 SendReponseDataToHost 活动显示在工具箱中。) 12.现在我们就来为 Workflow1 添加相应的代码,因此我们进入它的代码视图界面。 13.因为我们修改的 replicatorActivity1 活动的各个属性,Visual Studio 都为我们 添加了对应的事件处理程序,现在我们就来完成这些事件处理程序并为工作流添加其它一些 所需要的代码。我们重复“使用 IfElse 活动创建 QuestionFlow 工作流”这一节中的步骤 9 至步骤 12,这些过程为工作流添加了为进行问题处理所必须的一些属性。 14.Replicator 活动需要一个基于 IList 的集合以便复制(生成)出它的子活动。我们 有一个容纳问题的数组可以使用,因为基本的数组类型就是基于 IList 的。但是,我们怎样 返回结果呢?在问题描述和问题编号之间并没有直接联系在一起。除了这些,我们还不能在 所返回的数组的值中指定 Boolean 返回值。因此,我们将作出一些轻微的修改,我们需要创 建一个新的数组——一个整形数组。它用来表示在问题描述数组中的元素的偏移量。对于生 成的子活动,它将对所要提问的问题编号进行访问,给定它的一个索引,就可把问题描述数 组和回答的 Boolean 类型数组联系起来。因此,我们需要在_respone 数组的声明代码下面 添加这样一个数组。 private Int32[] _qNums = null; 15._qNums 数组还没有初始化,因此必须初始化才能使用。初始化最好的位置是在 Question 属性中,因此对它的 set 访问器进行修改,修改后的代码如下: public string[] Questions { get { return _questions; } set { // Save question values _questions = value; // Create new question number array _qNums = new Int32[_questions.Length]; for (Int32 i = 0; i < _questions.Length; i++) { // Assign this question number to the array _qNums[i] = i; } // for } } 16.为了对 Replicator 活动的所有事件都会被使用到进行证明,我们需要在_qNums 数 组的声明语句下添加下面的代码: private Int32 _currentQuestion = -1; private bool _currentQuestionResponse = false; 17.在 InitializeLoop 事件处理程序中添加下面的代码来对 InitialChildData 进行初 始化: replicatorActivity1.InitialChildData = _qNums; 备注:假如 Workflow1 有可绑定的属性的话,你可通过工作流视图设计器来直接对 InitialChildData 的值进行指定。但是,因为该 Replicator 活动正使用一个内部生成的数 组(_qNums),因此和上面所展示的一样,你必须在 InitializeLoop 事件处理程序中对 InitialChildData 的值进行指定。 18.对于 LoopCompleted 事件处理程序,添加下面的代码来返回问题的回答结果: replicatorActivity1.InitialChildData = _qNums; 19.现在我们的子活动将执行许多次来进行问题的提问。在每一个问题提问之前, Replicator 活动都将触发 ChildInitialized 事件。我们将处理该事件并从事件参数中获取 我们将要提问的问题编号。稍后,当 Code 活动执行时,将会对和该问题编号对应的问题进 行提问。因此,把下面的代码添加到 PrepareQuestion 方法中(该方法是 ChildInitialized 事件的处理程序): _currentQuestion = (Int32)e.InstanceData; 20.当对 Code 活动的回答结果进行保存时,我们需要做的过程都是相似的。定位到 QuestionAsked 事件处理程序(它用来处理 Replicator 活动的 ChildCompleted 事件),添 加下面的代码: _response[_currentQuestion] = _currentQuestionResponse; 21.紧接着是要对 Replicator 活动的 UntilCondition 进行编辑。找到 TestContinue 方法,添加下面的代码。这 些 在 TestContinue 方法中的代码将对 Dependent 属性进行检查。 假如不再有问题,循环将被终止,同时,假如这些问题被指明是相关的,并且最近的一次回 答是否定的,则所有余下未答问题的回答也被标明为否定的并终止循环。 if (_currentQuestion >= 0) { // Check dependency. if (!_response[_currentQuestion] && Dependent) { // Negate remaining questions. for (Int32 i = _currentQuestion + 1; i < Questions.Length; i++) { // Negate this question. _response[i] = false; } // Stop processing. e.Result = true; } else { // Check for complete loop. if (_currentQuestion == _qNums[Questions.Length - 1]) { // Done. e.Result = true; } else { // Continue processing. e.Result = false; } } } 22.找到 Visual Studio 已经为你添加了的 AskQuestion 方法,添加下面的代码。该方 法使你有机会去进行问题的回答。 // Ask the question! DialogResult result = MessageBox.Show(Questions[_currentQuestion], "Questioner:", MessageBoxButtons.YesNo, MessageBoxIcon.Question); _currentQuestionResponse = (result == DialogResult.Yes); 23.编译并执行该应用程序。把它执行的结果和前面的两个示例做比较,你会发现它的 功能和前面的示例完全一样。 第十章:事件活动 学习完本章,你将掌握: 1.使用 HandleExtenalEvent 活动创建特定的事件处理程序 2.在你的工作流中使用 Delay 活动 3.在你的工作流中使用 EventDriven 活动 4.在你的工作流中使用 Listen 活动 5.理解 EventHandlingScope 活动在活动并发执行的情况下是怎样监听事件的 在第八章(“调用外部方法和工作流”)中,你 看过工作流怎样使用 CallExternalMethod 活动来和宿主应用程序进行通信。当工作流调用一个外部方法时,使用一个你提供的本地通 信服务,该宿主应用程序会收到一个事件,然后宿主对数据进行处理并产生一些相应的动作。 相反的调用过程是宿主应用程序触发的事件被工作流捕获进行处理(尽管工作流事件处 理可被用在更广泛的任务中,而不仅仅是和宿主进行通信)。在第八章中,我提到过在对工 作流用来处理事件的活动进行叙述后,我们还将重温宿主/工作流之间的通信,在本章中我 们将完成这件事。 在目前为止的其它章节中,我都是单独地对某个工作流活动进行描述,然后提供一个小 应用程序来演示该活动的操作过程。和这些章节不同,本章将在一个示例应用程序中对多个 活动进行描述和演示。为什么这样做呢?因为我在这里要描述的这些活动都是相互关联互相 依赖的。我不能演示其中一个活动而对其它的活动不进行演示。Listen 活动可作为 EventDriven 活动的容器。在 EventDriven 活动的内部,你还会不出所料找到唯一的一个 HandleExternalEvent 活动等等。因此在本章中我将从始至终只创建一个应用程序来对这些 活动进行描述和演示。“宿主到工作流”这一节是本章的主线。我们首先从 HandleExternalEvent 活动开始。 使用 HandleExternalEvent 活动 不管在你的工作流中在何处处理事件,也不管你的工作流正处于执行状态时所发现要执 行的是什么样的活动组合,只要当一个事件来到了你的工作流路径当中, HandleExternalEvent 活动就是最终去处理该事件的工作流活动。对我来说,.NET 的强大的 功能特性很多,它的触发和处理事件的能力就是这些最强大的功能中的一个。包括工作流事 件的处理也同样强大。 HandleExternalEvent 活动的作用是响应一个基于 IEventActivity 接口的事件,它有 三个主要的成员:QueueName 属性、Subscribe 和 Unsubscribe 方法。QueueName 表示正等 待该事件的工作流队列,而 Subscribe 和 Unsubscribe 方法用来把你的事件处理程序将要接 收(或者不将进行接收)的特定事件实例告知工作流运行时。 HandleExternalEvent 活动本身也可和 CallExternalMethod 活动一起使用(我们在第 8 章中看到过)。工作流使用 CallExternalMethod 活动来把数据发送给宿主应用程序,但是 在工作流执行时,工作流使用 HandleExternalEvent 来接收从宿主中发送过来的数据。 备注:牢记:使用外部数据交换的时机并不仅仅是在把数据从你的宿主应用程序发送到 工作流的时候。当你创建你的工作流实例的时候,你可总是为其提供初始数据。但是,一旦 工作流正在执行时,对于直接和你的宿主应用程序进行本地通信来说,它是唯一可使用的机 制(当然也可使用更加间接的方式替代,例如使用 FTP 协议或者 Web 服务调用这些手段)。 表 10-1 和表 10-2 列出了使用 HandleExternalEvent 活动时经常用到的一些主要的属性 和方法。注意有些方法和属性是所有活动共有的(如在第四章“活动和工作流类型介绍”中 表 4-1 和表 4-2 展示的一样)。我在此展示的属性和方法无疑不是所有可使用的属性和方法, 但他们却是经常要被用到的。 表 10-1 经常用到的 HandleExternalEvent 活动的属性 属性 功能 CorrelationToken 获取或设置一个到关联标记(correlation token)的绑定。我们将在第 17 章(“关联和本地宿主通信”)中处理关联。 EventName 活动将要处理的事件。注意如果没有对其进行设置,该活动将不会对事 件进行监听并且和宿主通信也就不可能进行。奇怪的是,忽略该属性值 你不会收到任何错误验证信息。 InterfaceType 获取或设置进行通信所要使用的接口类型。该接口必须使用 ExternalDataExchange 特性进行装饰(标记)。(你或许可回忆一下第 8 章,你为 CallExternalMethod 方法提供了一个相同的接口。) 表 10-2 经常用到的 HandleExternalEvent 活动的方法 方法 功能 OnInvoked 这是一个有很用的保护型(protected)方法,它用来把本事件参数中的 值和你工作流中的字段或依赖属性进行绑定。重写该方法(或者处理它 所触发的事件)是检索来自于宿主并被保存到事件参数中的数据一个主 要的机制,通常,你会创建一个自定义的事件参数来把数据嵌入进参数 对象自身中。 尽管你能直接从 Visual Studio 的工具箱中使用 HandleExternalEvent 活动,但更普遍 的情形是使用你在第 8 章中看过的 wca.exe 工具来为你正使用的通信接口创建一个派生自 HandleExternalEvent 的自定义类。例如,假如在你的接口中定义了一个名称为 SendDataToHost 的事件,wca.exe 将会生成一个称作 SendDataToHost 的新活动(它派生自 HandleExternalEvent),并为你指定了 EventName 和 InterfaceType,而且通过你创建的 事件参数也为你和 SendDataToHost 事件进行了数据绑定。在本章晚些时候我将提供一个例 子。 使用 HandleExternalEvent 很容易,只需简单地在你的工作流中放入该活动,指定接口 和事件名。假如你需要的话,还可为 Invoked 事件提供一个 event handler,然后就可执行 你的工作流了。假如你使用 wca.exe,就可为你提供一个派生自 HandleExternalEvent 的活 动,你 可 直 接 把它拖拽到你的工作流中,在 属性窗口中添加绑定,把事件参数中的数据和一 个局部字段或者依赖属性绑定在一起。 在你的工作流中有了 HandleExternalEvent 活动后,在 等待事件发生时所有通过该顺序 流的处理过程都会停止。在一定程度上,在你的工作流中放入这个活动的行为就像.NET Framework 编程术语中的 AutoResetEvent。和 AutoResetEvent 不同的是,该处理过程的线 程不是暂停。它 就 像 是一扇门或通道,只有在该事件被触发时才允许工作流处理过程沿着它 的路径次序继续前进。 使用 Delay 活动 在本书中我们目前为止已经几次看到并使用过 Delay 活动,但现在我将对它进行更加正 式的叙述。为什么呢?很巧,Delay 活动实现了 IEventActivity 接口,因此,它同样也被 归类为 Windows Workflow Foundation(WF)的基于事件的活动。 传给 Delay 的是一个 TimeSpan 对象,它将延时指定的时间间隔。在延时时间过期后, 它将触发一个事件。你可通过在 Visual Studio 的工作流视图设计器上,或者以编程的方式 设置一个属性( TimeoutDuration)来初始化该延时的时间间隔。它 也 为你提供了一个 event handler(InitializeTimeoutDuration),当 Delay 活动被初始化并获取所需的时间间隔信 息时将调用该事件处理程序。 提示:延时事件和定时器(timer)事件是密切相关的。WF 没有 timer 活动,但你能通 过用 While 活动组合该 Delay 活动来创建一个 timer,本章的示例应用程序就使用了这种方 式。 HandleExternalEvent 和 Delay 相对于组合(composite)活动而言,它们都是 basic (基本)活动。也就是说,HandleExternalEvent 和 Delay 都只执行一个单一的功能,它们 不能作为其它活动的容器。正如你可能预料到的,这 些活动的普遍用法是基于一个单一的事 件来触发一系列的活动。你又如何支配这些事件序列会是怎么样的呢?答案是使用另一个 WF 活动:EventDriven 活动。 使用 EventDriven 活动 EventDriven 的行为就像是一个组合活动,这个组合活动以顺序执行的方式执行它所包 含的一系列活动。这并不是说你不能在这个容器中插入一个 Parallel(并行)活动,但在 并行活动之前插入的活动和之后插入的活动都将依顺序进行执行。对于它容纳的活动的唯一 限制是在执行路径上的第一个活动必须是对 IEventActivity 进行了处理的活动。 (HandleExternalEvent 和 Delay 就是这种类型的两个活动。)除了从基类继承而来的属性 和方法外,该 EventDriven 再没有其它可使用的属性和方法。(它仅仅是一个容器。) 和顺序活动不同的是,EventDriven 在有事件触发并被第一个活动处理前是不会允许所 容纳的活动执行的。(记住,第一个活动必须处理 IEventActivity。)。 EventDriven 的使用还有第二个限制。它的父活动必须是 Listen、State 或者 StateMachineWorkflow 之中的一个,在有些地方你是不能把 EventDriven 拖到你的工作流 中的,它只能拖到上述三种容器中。我们将在第 14 章(“基于状态的工作流”)中全面介 绍 State 和 StateMachineWorkflow 活动。但现在还是来看看 Listen 活动吧。 使用 Listen 活动 假如说 EventDriven 的行为像是一个顺序活动的话,那 Listen 活动的行为就像是一个 并行(parallel)活动。Listen 可作为两个或更多的 EventDriven 活动的容器。其中的这 些 EventDriven 活动选定的路径完全取决于它们中谁第一个收到事件。但是,一 旦 其中的一 个对某个事件进行了处理,其它的和它并行的 EventDriven 活动的执行路径都会被忽略而不 会被执行,它 们 不 会 再继续等待它们各自的事件,在 EventDriven 活动处理了相应的事件后, 又将按顺序继续执行接下来的路径。在它的 Activity 基类所暴露出的属性和方法外,再没 有我们感兴趣的属性和方法。 需注意的是在 Listen 活动内必须至少包含两个及以上的 EventDriven 活动对象,并且 仅仅只有 EventDriven 类型的活动能直接放到 Listen 活动中。此外,Listen 不能用到基于 状态机的工作流中。为什么这里有这些规则和限制呢? 假如 WF 允许少于两个的 EventDriven 子活动的话,Listen 活动的作用就值得怀疑。你 更好的做法是直接使用一个 EventDriven 活动。假如子活动中没有 EventDriven 活动的话, 你也就没有要去处理的事件。 在基于状态机的工作流中禁止使用 Listen 看起来或许是一个很奇怪的限制,其实这是 出于可能产生循环的考虑。状态机中循环这一术语指的是一系列事件的触发彼此相互依赖。 在一定程度上,这和多线程编程中的死锁概念相似。假如事件 A 依赖于事件 B 触发,但事件 B 又在等待事件 A 触发才能执行,我们就说产生了循环。在基于状态机的工作流中禁用并行 事件处理是 WF 设计器用来减少产生潜在的这种循环的一种措施。 使用 EventHandlingScope 活动 回顾目前为止我们看到过的活动中,有处理事件的基本活动、触发事件的 delay 活动、 能够组合顺序流的组合活动和组合并行流的组合活动。你 相 信 会 有 结 合 了 顺序化和并行化行 为特点的和事件有关的活动吗?这就是 EventHandlingScope 活动。 EventHandlingScope 是一个组合活动,它的作用是去容纳一组 EventHandler 活动(它 本身就是 IEventActivity 类型的对象的容器),以及唯一一个其它的非基于事件的如 Sequence 或 Parallel 之类的组合活动。非基于事件的组合活动在 EventHandler 活动中所 容纳的全部事件都已处理完毕前会一直执行。在 所 有这些事件都已触发并被处理完后,在 该 工作流的 EventHandlingScope 活动外面的下一个活动才继续执行。 宿主到工作流的通信 在介绍了 WF 中涉及事件的这些活动后,我现在要展示前面未完成的工作流和宿主之间 的通信体系的另一半。你可以回忆一下第 8 章,我们通过在工作流实例中使用 CallExternalMethod 活动来把信息发送到宿主进程中。这个被调用的“external method” 其实是一个你所提供的方法,它 由 一个你所写的本地通信服务暴露出来。该服务能把预定的 数据传给宿主并触发一个事件,这个事件发送一个数据到达的信号,然后宿主采取措施把数 据从该服务中读出(从工作流中接收到了数据后,该服务对数据进行了缓存)。 对于相反的过程,即宿主把数据发送给一个已经执行的工作流来说,也涉及到本地通信 服务、事件以及为处理这些事件的事件处理程序。当你为宿主和工作流之间设计好了进行通 信所要使用的接口时(就像是第 8 章中“创建服务接口”这一节所展示的一样),你在接口 中添加的方法就是被工作流用来把数据发送到宿主所使用的方法。在 该 接口中添加事件能使 宿主把数据发送给已经开始执行的工作流。 本章的示例应用程序将会用到我所描述过的每一个活动。一个 EventHandlingScope 活 动将处理“stop processing(停止处理)”事件。一个 Sequence 活动将包含一个对股票行 情更新进行模拟的工作流处理过程。当股价被更新时,新价将会被传到宿主中并在用户界面 上显示出来(如图 10-1 所示)。本 eBroker 应用程序并不是真实地对每一只股票代码的当 前股价进行检查,它使用一个简单的蒙特卡罗模拟法来计算最新的股价。蒙特卡罗模拟是使 用了随机数字的模拟方法,它和通过掷骰子来获取相应结果的过程类似。我们这样做的目的 只是为了去看看工作流和宿主之间是怎样进行通信的。 图 10-1 eBroker 的主用户界面 该 eBroker 应用程序应能让工作流知道,添加的新的当前并未被监视的股票应该要被监 视到,而添加时如该股票本已存在则不予考虑(目的是为了简化处理)。你可使用 Add 和 Remove 按钮来模拟股票的添加和删除。点击 Add 将弹出如图 10-2 所示的对话框。当你输完 后点击 OK,这个新的要被监视的股票就被添加进被监视股票的列表中了。 图 10-2 添加一个新的要被监视的股票 在“Ticker values”列表中选择一条记录,这会激活 Remove 按钮。点击该 Remove 按 钮就可把该项从被监视的股票列表中移除。该移除动作产生的结果如图 10-3。你正监视的 股票被保存在应用程序的 Settings 文件(XML 格式的配置文件)中。下一次你执行 eBroker 时,它将“记起”你的这些股票并重新开始进行监视。 图 10-3 移除一个已存在的被监视的股票 在图 10-2 中,你看到了应用程序需要知道你当前有多少股份以便能计算你所拥有的股 份的总价值,这些 数 字 可被 用 来计算当前的市值。假如你后来想要修正股份的数量(通过买 卖股票),你可选中市值(market value)列表中的股票然后点击 Buy!或者 Sell!该对话 框如图 10-4 所示。 图 10-4 需要去买或卖的股份数对话框 图 10-2 中的这个 Add 添加对话框也需要输入买或卖的“触发”条件值,当你不应该买 进或卖出你目前所监视的任何公司的股票时,工作流中包含的使用了这些值的业务逻辑会通 告你。假如股票价格超过了预定的触发卖价的值,则在市值列表中将显示一个红色的标记。 假如股票价格低于预定的触发买价的值,将显示一个绿色的标记。你能在任何时候进行买 卖...这些标记只是起提示作用,在图 10-5 中你可看到这组标记。 图 10-5 指出了买卖建议的 eBroker 用户界面 这四个按钮(Add、Remove、Buy!和 Sell!)中的每一个都会触发一个到工作流的事 件。还有第 5 个事件,就是 Stop,它用来停止模拟的执行过程,这个事件由 Quit 按钮触发。 该应用程序的许多地方其实我已经为你写完了,这使你能把注意力放到和工作流相关的 地方。首先,你 要 完成 工作流和宿主将用来进行通信的接口,然后你要使用 wca.exe 工具来 创建继承自 CallExternalMethod 和 HandleExternalEvent 的一组活动。准备好了这些,你 就可使用本章中看到过的每一个活动来布置该工作流。你将看到本地通信服务是怎样把宿主 应用程序和工作流通信处理进程粘合到一起的。最后,你将简要地进行检查并添加一些代码 到 eBroker 用户界面源文件中,以指引它和工作流进行交互。我们就开始吧! 创建通信接口 我们只需要一个方法:MarketUpdate,它把市场价格信息返回到用户界面上,另外还需 要五个事件,这些事件分别是 AddTicker、RemoveTicker、BuyStock、SellStock 和 Stop, 它们用来驱动工作流。这 唯 一的一个方法和五个事件都要添加到一个接口中,我们将首先创 建这个接口。任何和本地通信服务相关的事情都依赖于这个接口。 创建一个工作流数据通信接口 1.下载本章源代码,从 Visual Studio 中打开 eBroker 应用程序解决方案。 备注:和本书中绝大部分示例应用程序一样,本 eBroker 示例应用程序也有两个版本: 完整版本和非完整版本。非完整版是学习本示例应用程序的版本,你在此需要打开该版本以 便进行本示例应用程序的练习和学习。 2.在打开的本解决方案中你将找到三个项目。展开 eBrokerService 项目,然后打开 IWFBroker.cs 文件准备进行修改。 3.定位到 eBrokerService 名称空间,在该名称空间中添加下面的代码并保存: [ExternalDataExchange] public interface IWFBroker { void MarketUpdate(string xmlMarketValues); event EventHandler AddTicker; event EventHandler RemoveTicker; event EventHandler BuyStock; event EventHandler SellStock; event EventHandler Stop; } 4.对本项目进行编译,假如存在编译错误,修正所有错误。 不要忘记 ExternalDataExchange 属性。没有它你就不能使用我在这里所描述的数据传 送机制来成功地在工作流和宿主之间进行信息的传送。 在你创建通信活动(使用 wca.exe 工具)之前,花点时间来看看 eBrokerService 项目 中的 event arguments。MarketUpdateEventArgs 实际上只不过是 System.Workflow.ExternalDataEventArgs 的强类型版本,StopActionEventArgs 也是。 System.Workflow.ExternalDataEventArgs 这个 event argument 类不传送数据,但是, TickerActionEventArgs 和 SharesActionEventArgs 都要传送数据给工作流。 TickerActionEventArgs 承载的是代表要添加和移除的股票的 XML 数据,而 SharesActionEventArgs 承载的是作为主键的股票代码以及要买或卖的股票数目。 提示:设计这些 event argumeents 是很重要的,因为这些 event arguments 把数据从 宿主传给工作流。此外,wca.exe 工具会检查这些 event arguments 并创建到派生类的绑定, 使你能从 event arguments 中访问到这些派生类中的数据,仿佛这些数据就是这些 event arguments 所固有的。换句话说,假如 event arugment 有一个命名为 OrderNumber 的属性, 则wca.exe创建的类就会有一个命名为OrderNumber的属性。它的值来自于事件的事件参数, 并会为你自动指定该值。 现在我们就使用 wca.exe 工具来创建通信活动 创建通信活动 1.点击“开始”菜单,然后点击“运行”按钮打开“运行”对话框。 2.输入 cmd,然后点击确定。 3.使用 cd 命令把起始目录定位到 eBrokerService 项目生成的程序集所对应的目录下, 如 cd "...\eBroker\eBrokerService\bin\Debug"。 4.就如第 8 章中做过的一样,在命令行提示符中输入下面的命令(包含有双引号): "<%Program Files%>\Microsoft SDKs\Windows\v6.0A\bin\wca.exe" /n:eBrokerFlow eBrokerService.dll。(注意该“<%Program Files%>”表示你的 Program Files 目录的位 置,通常是“C:\Program Files”。)然后按下回车键。 5.wca.exe 会加载它在 eBrokerService.dll 找到的程序集,然后扫描使用了 ExternalDataExchange 特性修饰的接口,在这个例子中这个接口是 IWFBroker。被解析出的 那个方法被转换成派生自 CallExternalMethod 活动的类并保存到名称为 IWFBroker.Invokes.cs 的文件中。那些事件也相似地被转换为派生自 HandleExternalEvent 活动的类并被放进 IWFBroker.Sinks.cs 文件中。在命令提示符的命令行中键入下面的命令 来对该“invokes”文件重命名:ren IWFBroker.Invokes.cs ExternalMethodActivities.cs。 6.通过在命令提示符的命令行中键入下面的命令来对该“sinks”文件重命名:ren IWFBroker.Sinks.cs ExternalEventHandlers.cs。 7.使用下面的命令把当前目录下的这两个文件移到工作流项目的目录中:move External*.cs ..\..\..\eBrokerFlow。 8.现在回到 Visual Studio 中,向 eBrokerFlow 工作流项目中添加这两个刚创建好的文 件。 9.编译 eBrokerFlow 项目,在 成 功 编 译后,在工作流的视图设计器界面下的工具箱中将 呈现出 AddTicker、ButStock 等自定义事件活动。 注意:作为提醒,假如编译了工作流解决方案后这些新活动没有在工具箱中呈现出来, 就请关闭 eBroker 解决方案再重新打开它,以 强 制对这些自定义活动进行加载。在下一节我 们将使用它们。 创建 broker 工作流 1.在 Visual Studio 的视图设计器中打开 eBrokerFlow 项目中的 Workflow1.cs 文件。 2.我们需要插入一个 Code 活动,它被用来为一个 Delay 活动(你稍后将会插入它)指 定预期的延迟时间,并初始化一些内部数据结构。因此,拖拽一个 Code 活动到工作流视图 设计器的界面上,然后在 ExecuteCode 属性中键入 Initialize 并按下回车键,以便在工作 流代码中创建该 Initialize 事件处理程序。然后,回到工作流的视图设计器界面上继续添 加活动。 备注:延时时间值保存在 Settings(配置文件)中。 3.接下来拖拽一个 EventHandlingScope 到工作流视图设计器界面上。 4.记住,你需要为 EventHandlingScope 提供一个事件处理程序以及一个子活动,以便 在它监听事件时执行。我们首先创建事件处理程序。为了存取这些事件处理程序,需要把鼠 标指针移到 eventHandlingScop1 下面的微小矩形图标上。(这个矩形就是一个“ 智 能 标 记 。”) 然后这个矩形变成了一个更大、更黑并带有向下箭头的矩形。 点击这个向下的箭头,这会激活一个带有图标的四个快捷菜单:查看 EventHandlingScope、查看取消处理程序、查看错误处理程序和查看事件处理程序。 点击最下面的一个菜单项、切换到事件处理程序视图。你 看到的这个用户界面和你在第 七章(“基本活动操作”)中看到的和错误处理程序相联系的用户界面很相似。 拖拽一个 EventDriven 活动到工作流视图设计器界面上,把它放到这个矩形的中间(在 这里你会看到“将 EventDrivenActivity 拖放至此”的文字说明)。 5.现在回到工具箱中,在 eBrokerFlow 组件区域中找到 Stop 活动。拖拽一个该活动到 工作流视图设计器界面上,把它放进你在前一个步骤所添加的 EventDriven 活动中。假如你 想对多个事件进行监听的话,在这时你还可把它们都添加进去。在 我们的例子中,这 里 只 有 Stop 事件是我们需要的。 6.你刚才就添加好了 EventHandlingScope 活动将对停止执行进行监听的事件。下面, 你需要为 EventHandlingScope 添加子活动,当监听到 Stop 活动触发时 EventHandlingScope 将执行这个子活动。因此,我们需要通过第 4 步中的第一个子步骤回到 eventHandlingScopeActivity1 的查看 EventHandlingScope 界面上,但你需要选择最上面 的菜单项,而不是最下面的一个。 7.拖拽一个 While 活动到工作流视图设计器界面上,把它放到 EventHandlingScope 活 动内。 8.指定它的 Condition 属性为代码条件而不是声明性规则条件,指定该事件处理程序的 名称为 TestContinue。一旦 Visual Studio 添加了该 TestContinue 事件处理程序后,需要 回到工作流视图设计器上,还有更多的活动要进行添加。 9.While 活动只能接受唯一的一个子活动,因此拖拽一个 Sequence 活动到该 While 活 动中。 10.在这里你需要一个 Code 活动来对股票价值进行蒙特卡罗模拟,因此拖拽一个 Code 活动到视图设计器界面上,把它放进你在前一步骤所添加的 Sequence 活动中。在属性窗口 中把它重命名为 updateMarket。 11.指定 updateMarket 这个 Code 活动的 ExecuteCode 属性为 UpdateMarketValues。在 Visual Studio 添加了相应的事件处理程序后回到工作流视图设计器界面上来,以便继续布 置你的工作流。 12.在模拟完成后(你将添加的代码实际上就是进行模拟),你需要把这些潜在的进行 了修改的值传给宿主应用程序。为此 ,把 鼠 标 指针移到工具箱上,找到你在 IWFBroker 中创 建的 MarketUpdate 活动,把它拖到视图设计器界面上并放到 Sequence 活动中的你在前一步 骤中所添加的 Code 活动的下面。 13.MarketUpdate 活动需要把一小段 XML 放送给宿主,要做到这一点,它必须绑定到容 纳有此时将发送的 XML 的字段属性。为此,在 Visual Studio 的属性面板中选择 xmlMarketValues 属性,然后点击浏览(...)按钮,打开一个“将‘xmlMarketValues’绑 定到活动的属性”的对话框。然后点击绑定到新成员选项卡,点击创建属性,在新成员名称 中输入 Updates。最后点击确定。Visual Studio 就添加了 Updates 这个依赖属性。 14.为了让你能处理来自于宿主的事件,拖拽一个 Listen 活动到设计器界面上,把它放 进 Sequence 活动中。 15.假如你回忆一下,你会记起 IWFBroker 接口声明了五个事件,它们中的一个是我们 已经用过的 Stop,还有四个事件要去处理。Listen 活动目前仅仅容纳了两个 EventDriven 活动,但添加更多的 EventDriven 活动也很容易。你 需 要简单地拖拽多达三个的 EventDriven 活动进 Listen 活动中。为什么要添加三个而不是正好的两个呢?因为第五个 EventDriven 活动要包含一个行为像是定时器的 Delay 活动,当延时过期后,Listen 活动会结束该工作 流线程。然后 While 活动对执行条件进行检测判断,而返回的这个条件总被设置为 true, 于是使 While 活动不停地循环。在股票价值被更新并发送给宿主后,Listen 活动又对新一 轮来自宿主的事件进行监听。 16.在最右边的 EventDriven 活动中,拖拽并放入一个 Delay 活动,在属性面板中把它 命名为 updateDelay。 17.接下来从 eBrokerFlow 中拖拽一个 SellStock 活动到工作流视图设计器界面上,把 它放到从右边数起的第二个 EventDriven 活动中。 18.在 Visual Studio 的属性面板中选择 NumberOfShares 属性,点击浏览(...)按钮, 这会又一次打开一个“将‘NumberOfShares’绑定到活动的属性”的对话框。点击绑定到新 成员选项卡,然后再点击创建字段,并在新成员名称中输入_sharesToSell,最后点击确定。 Visual Studio 就添加了这个_sharesToSell 字段。 备注:我在这里选择创建_sharesToSell 依赖属性而不是字段是因为字段从来不会被 Workflow1 类的外部访问到。它提供的基于 XML 格式的市场价值信息要传给宿主,因此应当 把外部访问权限暴露出来。 19.Symbol 属性也必须进行绑定。下面的步骤和上一步骤是一样的,只是字段名称要命 名为_tickerToSell。 20.为了卖出股票,要拖拽一个 Code 活动放到 SellStock 事件处理程序的下面。在它的 ExecuteCode 属性中输入 SellStock,在插入了对应的事件处理程序后请回到工作流视图设 计器界面上来。 21.我们现在要对买股票的逻辑进行添加。拖拽一个 BuyStock 事件处理活动(也来自于 eBrokerFlow)到设计器界面上,把它放到正中间的 EventDriven 活动中。 22.使用第 18 步的步骤,把 BuyStock 活动的 NumberOfShares 属性绑定到一个新的字段, 名称为_sharesToBuy。同样,使用第 19 步的步骤,把它的 Symbol 属性也绑定到一个新的字 段,名称为_tickerToBuy。 23.和你需要一个 Code 活动去卖股票一样,你也需要一个 Code 活动去买股票。重复第 12 步添加一个新的 Code 活动,设置它的 ExecuteCode 属性为 BuyStock。 24.重复第 17 步至第 20 步两次,把 RemoveTicker 和 AddTicker 事件也添加到 Listen 活动中。RemoveTicker 活动的 TickerXML 属性要绑定到一个新的名称为_tickerToRemove 的字段,而为该 RemoveTicker 事件添加的 Code 活动的 ExecuteCode 属性指定为 RemoveTicker。同样地,AddTicker 活动的 TickerXML 属性要绑定到_tickerToAdd,和它相 联系的 Code 活动的 ExecuteCode 属性指定为 AddTicker。完成这些后,Listen 活动的外观 如下所示: 25.编译你的这个工作流,纠正任何出现的编译错误。 26.在 Visual Studio 中打开 Workflow1.cs 的源文件准备进行编辑。 27.Visual Studio 为你添加了大量的代码,因此你首先定位到 Workflow1 的构造器并 在该构造器下添加如下的代码。你 插 入的这些代码可被认为是初始化代码。当工作流启动时, 你将把一个数据字典传给该工作流,这个数据字典包含有以股票代码(如“CONT”)作为关 键字的要监视的若干股票信息的集合。你 也需要指定一个轮询间隔,它是再一次对股票市值 进行检测前工作流所要等待的时间值。 private Dictionary _items = new Dictionary(); private string _tickersXML = null; public string TickersXML { get { return _tickersXML; } set { _tickersXML = value; } } private TimeSpan _interval = TimeSpan.FromSeconds(7); public TimeSpan PollInterval { get { return _interval; } set { _interval = value; } } 28.下面定位到你在步骤 2 中为你的第一个 Code 活动添加的 Initialize 事件处理程序。 插入下面的代码: // Establish the market update timeout updateDelay.TimeoutDuration = PollInterval; // Stuff the known ticker values into the dictionary // for later recall when updating market conditions. eBrokerService.Tickers tickers = null; using (StringReader rdr = new StringReader(TickersXML)) { XmlSerializer serializer = new XmlSerializer(typeof(eBrokerService.Tickers)); tickers = (eBrokerService.Tickers)serializer.Deserialize(rdr); } foreach (eBrokerService.Ticker ticker in tickers.Items) { // Add the ticker to the dictionary _items.Add(ticker.Symbol, ticker); } 提示:为了方便,我在这个初始化方法中对该 Delay 活动的 TimeoutDuration 进行了指 定。但是不要忘了,你 也能使用 Delay 活动的 InitializeTimeoutDuration 方法来做同样的 工作。 29.找到 TestContinue 事件处理程序,While 活动使用它来对是否继续进行循环进行判 断。插入下面的代码让 While 活动不停循环(不用担心...实际上它最终会停止循环的!): // Continue forever e.Result = true; 30.下面要插入的代码块很长,它使用了蒙特卡罗模拟来对股票市场价进行更新。找到 和名称为 updateMarket 的 Code 活动(参见第 10 步)相 对应的 UpdateMarketValues 事件处 理程序,插入下面的代码: // Iterate over each item in the dictionary and decide // what it's current value should be. Normally we'd call // some external service with each of our watch values, // but for demo purposes we'll just use random values. Random rand = new Random(DateTime.Now.Millisecond); eBrokerService.UpdateCollection updates = new eBrokerService.UpdateCollection(); foreach (string key in _items.Keys) { // Locate the item eBrokerService.Ticker item = _items[key]; // If we're starting out, we have no current value, // so place the value at half the distance between the // buy and sell triggers. if (item.LastPrice <= 0.0m) { // Assign a price decimal delta = (item.SellTrigger - item.BuyTrigger) / 2.0m; // The last price must be a positive value, so add // the delta to the smaller value. if (delta >= 0.0m) { // Add delta to buy trigger value item.LastPrice = item.BuyTrigger + delta; } // if else { // Reverse it and add to the sell trigger // value item.LastPrice = item.SellTrigger + delta; } // else } // if // Set up the simulation decimal newPrice = item.LastPrice; decimal onePercent = item.LastPrice * 0.1m; Int32 multiplier = 0; // no change // We'll now roll some dice. First roll: does the // market value change? 0-79, no. 80-99, yes. if (rand.Next(0, 99) >= 80) { // Yes, update the price. Next roll: will the // value increase or decrease? 0-49, increase. // 50-99, decrease multiplier = 1; if (rand.Next(0, 99) >= 50) { // Decrease the price. multiplier = -1; } // if // Next roll, by how much? We'll calculate it // as a percentage of the current share value. // 0-74, .1% change. 75-89, .2% change. 90-97, // .3% change. And 98-99, .4% change. Int32 roll = rand.Next(0, 99); if (roll < 75) { // 1% change newPrice = item.LastPrice + (onePercent * multiplier * 0.1m); } // if else if (roll < 90) { // 2% change newPrice = item.LastPrice + (onePercent * multiplier * 0.2m); } // else if else if (roll < 98) { // 3% change newPrice = item.LastPrice + (onePercent * multiplier * 0.3m); } // else if else { // 4% change newPrice = item.LastPrice + (onePercent * multiplier * 0.4m); } // else if } // if else { // No change in price newPrice = item.LastPrice; } // else // Now create the update for this ticker eBrokerService.Update update = new eBrokerService.Update(); update.Symbol = item.Symbol; update.LastPrice = item.LastPrice; update.NewPrice = newPrice; update.Trend = multiplier > 0 ? "Up" : (multiplier == 0 ? "Firm" : "Down" ); update.Action = newPrice > item.SellTrigger ? "Sell" : (newPrice < item.B uyTrigger ? "Buy" : "Hold"); update.TotalValue = newPrice * item.NumberOfShares; updates.Add(update); // Update the data store item.LastPrice = newPrice; } // foreach // Serialize the data StringBuilder sb = new StringBuilder(); using (StringWriter wtr = new StringWriter(sb)) { XmlSerializer serializer = new XmlSerializer(typeof(eBrokerService.Update Collection)); serializer.Serialize(wtr, updates); } // using // Ship the data back Updates = sb.ToString(); 基本上,每一次更新循环,对于每一只股票将有 20%的几率被修改。假如该股票的价格 将被修改,它有一半的几率会上升,有一半的几率会下降。将改变的值是:有 75%的几率是 当前每股价格的 1%,有 15%的几率是当前每股价格的 2%,有 7%的几率是当前每股价格的 3%, 有 3%的几率是当前每股价格的 4%。对于每一次循环,所有被监视的股票都会被更新,即使 它的价格没有变化。将 被 发 送 回 宿 主 进行显示的数据是一个 XML 字符串,它 包含有各只的股 票代码、当前价格、根据所买的该只股票数计算出来的总市值、趋势(上升还是下降)以及 是否有要进行买或卖的建议。买卖建议会显示出一个醒目的标志(红或绿),你已经在图 10-5 中见过。 31.现在向外部事件处理程序中添加代码。首先定位到 SellStock 事件处理程序,添加 下面的代码: // Reduce the number of shares for the given ticker. try { // Find this ticker. eBrokerService.Ticker item = _items[_tickerToSell]; if (item != null) { // Reduce the number of shares. item.NumberOfShares = item.NumberOfShares - _sharesToSell >= 0 ? item.NumberOfShares - _sharesToSell : 0; } } catch { // Do nothing we just won't have sold any. } 32.找到 BuyStock 事件处理程序,添加下面的代码: // Increase the number of shares for the given ticker. try { // Find this ticker. eBrokerService.Ticker item = _items[_tickerToBuy]; if (item != null) { // Increase the number of shares. item.NumberOfShares += _sharesToBuy; } } catch { // Do nothing we just won't have purchased any. } 33.接下来是 RemoveTicker,找到它并插入下面的代码: // Remove the given ticker from the watch. try { // Deserialize eBrokerService.Ticker ticker = null; using (StringReader rdr = new StringReader(_tickerToRemove)) { XmlSerializer serializer = new XmlSerializer(typeof(eBrokerService.Ti cker)); ticker = (eBrokerService.Ticker)serializer.Deserialize(rdr); } // Find this ticker. if (_items.ContainsKey(ticker.Symbol)) { // Remove it. _items.Remove(ticker.Symbol); } } catch { // Do nothing we just won't have removed it. } 34.最后是 AddTicker,插入下面的代码: try { // Deserialize eBrokerService.Ticker ticker = null; using (StringReader rdr = new StringReader(_tickerToAdd)) { XmlSerializer serializer = new XmlSerializer(typeof(eBrokerService.Ti cker)); ticker = (eBrokerService.Ticker)serializer.Deserialize(rdr); } // Add the item if not already existing. if (!_items.ContainsKey(ticker.Symbol)) { // Add it. _items.Add(ticker.Symbol, ticker); } } catch { // Do nothing we just won't have added it. } 35.假如你现在对本工作流进行编译,不会出现编译错误。 现在,工作流完成了,我们需要回到我们关注的本地通信服务和宿主的结合上来。因为 我们已经在第 8 章详细介绍过这方面的内容,因此我在这里不再整个进行重新介绍。假如你 打开本例中相关的文件,你会看到这些代码和第 8 章中看过的很相似。 注意:我在第 8 章中提到过下面的内容,但它是一个重要的问题,您对这个问题的认识 应该得到加强:如果你在工作流和宿主应用程序中对对象或者对象的集合进行了共用的话, 运行中就会有风险,这 牵 涉 到多线程数据访问的问题,因为工作流和宿主将共享对同一对象 的引用。假如你的应用程序存在这个问题,当在工作流和宿主之间传递它们时,你 就可考虑 对这些对象进行克隆(在你的数据类中实现 ICloneable 接口),或者使用序列化技术。对 于本应用程序,我选择了 XML 序列化。 但我想谈谈连接器类 BrokerDataConnector 中的一些代码。IWFBroker 接口因为包含了 事件,因此和我们在第 8 章中看到的示例的接口不同。因为连接器类必须实现该接口(在本 例中,BrokerDataConnector 实现了 IWFBroker),因此该连接器也必须处理这些事件。但 是,事件的实现和清单 10-1 中看到的一样,没有特别之处。假如你对该清单从头一直看到 尾,你将看到通常的事件实现和你或许亲自写过的事件实现非常相像。 清单 10-1 BrokerDataConnector.cs 的完整代码 using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.Workflow.Activities; using System.Workflow.Runtime; using System.Data; namespace eBrokerService { public sealed class BrokerDataConnector : IWFBroker { private string _dataValue = null; private static WorkflowBrokerDataService _service = null; private static object _syncLock = new object(); public static WorkflowBrokerDataService BrokerDataService { get { return _service; } set { if (value != null) { lock (_syncLock) { // Re-verify the service isn't null // now that we're locked if (value != null) { _service = value; } // if else { throw new InvalidOperationException("You must pro vide a service instance."); } // else } // lock } // if else { throw new InvalidOperationException("You must provide a s ervice instance."); } // else } } public string MarketData { get { return _dataValue; } } // Workflow to host communication method public void MarketUpdate(string xmlMarketValues) { // Assign the field for later recall _dataValue = xmlMarketValues; // Raise the event to trigger host read _service.RaiseMarketUpdatedEvent(); } // Host to workflow events public event EventHandler AddTicker; public event EventHandler RemoveTicker; public event EventHandler BuyStock; public event EventHandler SellStock; public event EventHandler Stop; public void RaiseAddTicker(Guid instanceID, string tickerXML) { if (AddTicker != null) { // Fire event AddTicker(null, new TickerActionEventArgs(instanceID, tickerX ML)); } // if } public void RaiseRemoveTicker(Guid instanceID, string tickerXML) { if (RemoveTicker != null) { // Fire event RemoveTicker(null, new TickerActionEventArgs(instanceID, tick erXML)); } // if } public void RaiseBuyStock(Guid instanceID, string symbol, Int32 numSh ares) { if (BuyStock != null) { // Fire event BuyStock(null, new SharesActionEventArgs(instanceID, symbol, numShares)); } // if } public void RaiseSellStock(Guid instanceID, string symbol, Int32 numS hares) { if (SellStock != null) { // Fire event SellStock(null, new SharesActionEventArgs(instanceID, symbol, numShares)); } // if } public void RaiseStop(Guid instanceID) { if (Stop != null) { // Fire event Stop(null, new StopActionEventArgs(instanceID)); } // if } } } 当宿主执行上面这些“raise”方法来触发基于用户输入的各种事件时,工作流就会执 行连接器的 MarketUpdate 方法。第 8 章描述了该工作流用来调用 MarketUpdate 方法的机制。 为了看看宿主怎样调用一个用来和工作流进行交互的事件(在事件参数中可根据需要携带相 应的数据),我们来看看下面的代码段。这些代码用来在点击 Quit 按钮时退出应用程序。 private void cmdQuit_Click(object sender, EventArgs e) { // Stop the processing // Remove from workflow eBrokerService.BrokerDataConnector dataConnector = (eBrokerService.BrokerDataConnector)_workflowRuntime.GetService( typeof(eBrokerService.BrokerDataConnector)); dataConnector.RaiseStop(_workflowInstance.InstanceId); // Just quit Application.Exit(); } 为了触发传送数据到工作流中的这些事件,你首先需要使用工作流运行时的 GetService 方法获取连接器。注意该服务需要为它指明恰当的连接器类型,这样才能去使 用它的那些“raise”方法。一旦得到该服务后,你就可简单地调用对应的“raise”方法, 为它指定要传送的必要的数据信息去生成对应的 event arguments 就可以了 第十一章:并行活动 学习完本章,你将掌握: 1.理解在工作流环境中 Parallel 活动是怎样执行的,并且懂得如何使用它们 2.并行执行路径中的同步数据存取和临界代码区 3.使用 ConditionedActivityGroup 活动去执行根据条件表达式判断执行路径的并行活 动 在本书中截止目前为止,我们仅仅处理过顺序业务流程。如活动 A 执行后转到活动 B 的执行等等。我们还没看到过并行执行路径和由此通常伴随而来的错综复杂的情况。在本章 中,我们将看看并行活动的处理过程,以 及 看看怎样对横跨并行执行路径的共享信息进行同 步存取。 使用 Parallel 活动 当你用完某样东西需去杂货店买的时候,通常都可能只有一条结帐流水线。所有的顾客 都必须通过这条唯一的结帐线来付款。在 那 些 罕 见 的 情况下,当有两个或更多的结帐线开放 后,顾客和杂货结帐的速度会更快,因为他们是以并行的方式结帐的。 在某种意义上,工作流活动也是如此。有 时 ,你不能以混乱的方式甚至更糟糕的随机的 顺序来执行特定的活动。在这些情况下,你 必须选择一个 Sequence 活动来容纳你的工作流。 但在其它时候,你可能需要设计在同一时间(或者如我们将看到的,几乎是在同一时间)能 够执行多个处理过程的流程。对于这些情况,Parallel 活动是一个选择。 Parallel 活动是一个组合活动,但它只支持 Sequence 活动作为它的子活动。(当然, 你可自由地把你想使用的任何活动放到该 Sequence 活动中。)它至少需要两个 Sequence 活动。 子Sequence活动并没有在单独的线程上执行,因此Parallel活动不是一个多线程活动, 相反,那些子 Sequence 活动在单一的一个线程上执行。WF 会只对一个 Parallel 活动执行 路径中的某一单独的活动进行处理,直到该活动完成才会切换到另一个并行执行路径中的一 个单独的活动。也就是说,在某 个 分 支内 的 某 个单 独的 子 活动完成后,才能安排其它分支中 的另一个单独的子活动去执行(译者注:每个单独的子活动是 Parallel 活动执行的最小单 位)。各个并行活动间真正的执行顺序是无法保证的。 这样的结果是并行执行路径不会同时被执行,因此它们并不是真正意义上的多线程中的 并行执行。但是,它们执行时就像是在并行操作,并且你也可这样看待它们。把 Parallel 活动看成是真正意义的并行过程是最明智的:你 可 像 对 待 任何多线程下的处理过程来对待并 行活动。 注意:假如你需要强制指定并行执行路径间的顺序,可考虑使用 SynchronizationScope 活动。本章晚些时候我将对它进行演示。 在此时有个值得关注的问题:“有 Delay 活动会怎么样呢?”正如你知道的,顺序工作 流中的 Delay 活动会停止执行指定的 TimeoutDuration 时间间隔。那这会停止 Parallel 活 动的处理过程吗?答案是不会。延时会导致特定的顺序工作流路径将被停止,但其它并行路 径会正常地继续进行处理。 考虑到我所发出过的所有这些多线程警告,你或许会认为使用 Parallel 活动是一个挑 战。事实上,它 非 常 容易使用。它在工作流视图设计器中呈现出来的样式和 Listen 活动(该 活动在第 10 章“事件活动”中讨论过)非常相像。和 EventDriven 活动不同的是,你将会 在它里面找到 Sequence 活动,除此之外,表现出来的可视化界面都是相似的。我们这就创 建一个简单的例子来演示一下 Parallel 活动。 创建一个带有并行执行过程的新工作流应用程序 1.为了快速地演示 Parallel 活动,本例子使用的是一个基于控制台的 Windows 应用程 序。我们需要下载本章源代码,源代码中包含有练习版和完整版两个版本的解决方案项目, 完整版中的项目可直接运行查看运行结果,我们在此使用 Visual Studio 打开练习版中的解 决方案项目文件。 2.在 Visual Studio 打开了 ParallelHelloWorld 解决方案后,找到 Workflow1 工作流 并在工作流视图设计器中打开它。 3.从工具箱中拖拽一个 Parallel 活动到设计器界面上。 4.在 Parallel 活动放到工作流视图设计器界面上后,它会自动地包含一对 Sequence 活动。在左边分支的 Sequence 活动中拖入一个 Code 活动,在属性面板上指定它的名称为 msg1,并在它的 ExecuteCode 属性中输入 Message1。 备注:尽管在 Parallel 活动中的并行执行路径不能少于两个,但它并不会阻止你添加 更多的并行执行路径。假如你需要三个或更多的并行执行路径,你 可简单地把多个 Sequence 活动拖拽到设计器界面上并把它们放到 Parallel 活动中。 5.在 Visual Studio 中切换到代码视图界面下,在 Message1 事件处理程序中添加下面 的代码,然后回到工作流视图设计器界面上来。 Console.WriteLine("Hello,"); 6.拖拽第二个 Code 活动到左边的 Sequence 活动中,把它放到 Code 活动 msg1 的下面。 该 Code 活动的名称命名为 msg2,并在它的 ExecuteCode 属性中输入 Message2。 7.当 Visual Studio 为你切换到代码视图界面后,在 Message2 事件处理程序中输入下 面的代码,然后回到工作流视图设计器界面上来。 Console.WriteLine(" World!"); 8.现在拖拽第三个 Code 活动到右边的 Sequence 活动中。它的名称命名为 msg3,在它 的 ExecuteCode 属性中输入 Message3。 9.在 Message3 的事件处理程序中添加下面的代码: Console.WriteLine("The quick brown fox"); 10.回到工作流视图设计器界面上来,拖拽第四个 Code 活动并把它放进右边的 Sequence 活动中,具体位置是在你前一步所添加的 Code 活动的下面。它的名称命名为 msg4,它的 ExecuteCode 属性的值设置为 Message4。 11.在 Message4 的事件处理程序中输入下面的代码: Console.WriteLine(" jumps over the lazy dog."); 12.现在工作流的设计工作就完成了,你 需 要 在 ParallelHelloWorld 应用程序中添加对 ParallelFlow 项目的项目级引用。 13.在 ParallelHelloWorld 项目中打开 Program.cs 文件,找到下面的一行代码: Console.WriteLine("Waiting for workflow completion."); 14.在你找到的上述代码下,添加下面的代码以便创建工作流实例: // Create the workflow instance. WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(ParallelFlow.Workflow1)); // Start the workflow instance. instance.Start(); 15.编译本解决方案,修正所有的编译错误。 16.按下 F5 或者 Ctrl+F5 运行本应用程序。注意,为了能看到输出结果,你应该在 Program.cs 中(在 Main 方法内)设置一个断点。 和你在上面的图片中看到的一样,输出的信息是杂乱的。假如左边 Sequence 活动无干 扰地一直运行到结束,则在输出的结果中,“World!”就在“Hello,”的下面。假如右边 的 Sequence 活动没有被干扰,则 会 输 出 下 面 由 Western Union 发明的包含了所有 26 个字母, 用来测试电传打字机是否正常的一句话:“ The quick brown fox jumps over the lazy dog”。 但 是,这是好事,因为从这些 Code 活动中输出信息的杂乱顺序正好指出了并行活动的执行 方式。 假如你看仔细些,你将会看到各个 Code 活动都要一直运行到完成后才会转去执行另一 个 Code 活动。你或许也会注意到左边的 Sequence 活动在右边的 Sequence 活动前面启动。 当前该 Parallel 活动的执行顺序和伪随机数的生成结果是类似的。假如你使用相同的随机 种子值,产生的随机数实际上并不是随机的,它 们 是以可预期的方式生成的,在本 Parallel 活动中,也总是以这样的方式来执行的,从左到右,从上到下。它的执行顺序也是可预期的。 但是,不要把这样的现象和你的业务逻辑联系起来。就如我前面谈到的,你应当认为 Parallel 活动的执行方式是真正并行的。你 必须假定并行执行路径是以随机的顺序执行的, 即使可能个别的活动总是在切换执行上下文前结束。假如 WF 打破该契约(规则),那并非 为多线程操作所设计的活动内部的代码也会被中断,这可不是什么好事。 这就自然而然地产生了一个问题:你怎么协调并行执行路径,为什么要这样做呢?这个 问题问得非常好,这把我们带入了下面的话题:同步。 使用 SynchronizationScope 活动 任何曾经写过多线程应用程序的人都知道线程同步是一个很关键的话题。现代 Windows 操作系统使用任务调度程序来控制 CPU 上线程的执行,任务调度程序在任何时候都可移走一 个正在执行的线程,假如你疏忽,甚至可能在一个关键操作过程中发生这种情况。 当你写基于 Windows 的应用程序时,你有许多的多线程手段可以利用:如互 斥 、内 核 事 件 、临 界 区 、信 号 量 等等。但最终有两件事必须得到控制:一是临界代码的完成过程中不能 进行线程的切换,还有就是共享内存的存取(例如包含有 volatile 信息的变量)。 备注:这里使用 volatile 是有一定含义的。它意味着数据改变,对于任意的时间段都 不能保证仍然还是某一特定值。 WF 通过提供 SynchronizationScope 活动的使用来解决上面提到的两种情况。和 传 统 的 多 线 程编程所要做的工作相比,你不需要去使用许多不同的手段(也就是说,你不需要去理 解每种手段所使用的场合及使用方法)。相反,这一个活动的作用就是处理上面提到的两种 情况:完成临界代码区的执行过程及 volatile 内存的存取。 当你把一个 SynchronizationScope 活动放到你的工作流中的时候,WF 会保证在该执行 上下文切换到其它的并行路径以前,该组合活动(指 SynchronizationScope 活动)内部的 所有活动都将全部运行完成。这意味着你能在 SynchronizationScope 内部访问所有的 volatile 内存和完成临界区代码的执行。 SynchronizationScope 使用的机制和互斥(mutex)相似。(事实上,它会作为一个临 界区或者加锁执行,因为同步的范围不会跨越应用程序域。)在传统的多线程编程中,mutex 是为互相排斥所提供的一个对象。在一定程度上,它就像是一个令牌或钥匙。换句话说,当 一个线程要求进行互斥,仅仅另一个线程并没有使用该互斥对象时才允许它去访问这个互斥 对象。假如另一个线程正在使用这个互斥对象,第二个线程就会阻塞(等待),直到第一个 线程已经完成了它的任务并且释放了该互斥对象。 互斥对象通常只不过是“named”这样一个字符串,你也可使用任何你喜欢的字符串。 但是,多个线程互斥访问同一个互斥体必须使用同一个字符串对象。 SynchronizationScope 也有相似的特点,这 通过它的 SynchronizationHandles 属性来 提供支持。SynchronizationHandles 其实是一个字符串集合,它们中的每一个(字符串) 的作用是和要进行同步处理的其它的 SynchronizationScope 对象建立关联。假如你没有为 该属性指定至少一个字符串,Visual Studio 也不会为你报错,但是 SynchronizationScope 不会工作。和使用互斥对象一样,所有要进行同步的 SynchronizationScope 活动都必须使 用相同的 SynchronizationScope 字符串。 在进入我们的例子之前,我们要回头去看看前一个示例应用程序的输出结果。看到了那 些杂乱的信息没有?我们把 SynchronizationScope 活动运用到前面的示例应用程序中,来 迫使这些信息以一个恰当的顺序输出。说得更明白一点,就是我们在执行上下文环境进行切 换前,强制让临界区内的代码一直运行到完成,但我也将引入 volatile 内存去演示它的工 作方式。 创建一个带有同步化并行执行方式的新工作流应用程序 1.在本实例中,你将再次使用基于控制台的 Windows 应用程序,该应用程序和前一个实 例非常相似。在你下载的本章源代码中,打开 SynchronizedHelloWorld 文件夹内的解决方 案。 2.在 Visual Studio 加载了该解决方案后,在工作流视图设计器中打开 SynchronizedFlow 项目中的 Workflow1 工作流,拖拽一个 Parallel 活动到设计器界面上。 3.现在拖拽一个 SynchronizationScope 活动到设计器界面上,把它放到左边的 Sequence 活动中。 4.设置你刚才在工作流中所添加的 SynchronizationScope 活动的 SynchronizationHandles 属性为 SyncLock。 备注:你在 SynchronizationHandles 属性中输入的文本字符串并不重要。重要的是所 有要被同步的 SynchronizationScope 活动都要使用相同的文本字符串。 5.拖拽一个 Code 活动到 SynchronizationScope 活动中,在 属性面板上指定它的名称为 msg1,它的 ExecuteCode 属性为 Message1。 6.Visual Studio 会为你切换到代码视图中。在 Message1 的事件处理程序中输入下面 的代码: _msg = "Hello,"; PrintMessage(); 7.但是你同时还需要添加_msg 字段和 PrintMessage 方法。在 Workflow1 源代码中找到 它的构造器,在它的构造器下面添加下面的代码: private string _msg = String.Empty; private void PrintMessage() { // Print the message to the screen Console.Write(_msg); } 8.拖拽第二个 Code 活动到 SynchronizationScope 活动中,把它放到 msg1 活动的下面。 它的名称命名为 msg2,它的 ExecuteCode 属性设置为 Message2。 9.当 Visual Studio 切换到代码视图后,在 Message2 事件处理程序中输入下面的代码: _msg = " World!\n"; PrintMessage(); 10.拖拽一个 SynchronizationScope 活动放到右边的 Sequence 活动中。 11.为了让这个 SynchronizationScope 活动和你在第 4 步中插入的 SynchronizationScope 活动进行同步,在当前这个 SynchronizationScope 活动的 SynchronizationHandles 属性中输入 SyncLock。 12.现在拖拽一个 Code 活动放到你刚才插入的这个 SynchronizationScope 活动中。它 的名称命名为 msg3,它的 ExecuteCode 属性设置为 Message3。 13.在 Message3 事件处理程序中插入下面的代码: _msg = "The quick brown fox"; PrintMessage(); 14.拖拽第四个 Code 活动,把它放入右边 Sequence 活动中的 SynchronizationScope 活动中。它的名称命名为 msg4,它的 ExecuteCode 属性设置为 Message4。 15.在 Message4 事件处理程序中插入下面的代码: _msg = " jumps over the lazy dog.\n"; PrintMessage(); 16.工作流现在就完成了。从 SynchronizedHelloWorld 应用程序中添加对该工作流的项 目级引用。 17.打开 SynchronizedHelloWorld 项目中的 Program.cs 文件,找到下面一行代码: Console.WriteLine("Waiting for workflow completion."); 18.在你找到的上面一行代码的下面,添加下面的代码来创建一个工作流实例: // Create the workflow instance. WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(SynchronizedFlow.Workflow1)); // Start the workflow instance. instance.Start(); 19.编译该解决方案,纠正任何编译错误。 20.执行该应用程序。你 可 能需要在 Main 方法中设置一个断点才能看到输出结果。假如 输出结果中显示的这两条信息还是杂乱的,你需要确认你在两个 SynchronizationScope 活 动(步骤 4 和步骤 11 中添加)的 SynchronizationHandles 属性中输入的是完全相同的字符 串。 我们在本章中将介绍的最后一个活动和你看过或将看到的任何其它活动都大不一样,它 叫 做 ConditionedActivityGroup 活动。它具有并行和循环两方面的特征。让我们瞧瞧吧。 使用 ConditionedActivityGroup(CAG)活动 简要地说,CondtionedActivityGroup 活动(通常都称作 CAG)是一个组合活动,它为 你提供了一个角色,使你能对要执行的并行子活动进行调度。总的来看,它 会 运 行到 你 指 定 的一个条件为 true 时为止,假如你没有指定这个条件,它则会运行到所有的子活动都报告 它们没有更多要去执行的任务时为止。我提到的这个条件就是 CAG 的 until condition。 子活动并行执行,并且只有条件满足的子活动才被执行。这个条件也就是所谓的 when condition。假如没有任何子活动满足 when 条件,也就没有子活动去执行,并且 CAG 活动会 结束,除非你通过设置它的 until 条件强制它继续进行。假如一个或多个子活动满足了 when 条件,那这些子活动将并行执行。其它没有满足 when 条件的子活动将维持一个空闲状态。 通过设定要执行的子活动的 when 条件,你能决定哪些子活动将去执行。 CAG 开始执行时要判断它的 until 条件。假如判断结果指明要继续执行的话,则也要对 每一个子活动的 when 条件进行判断。判断结果如确定要去执行,则会导致相关的子活动如 期运行。假如超过一个活动要如期执行,则 执 行的顺序将取决于它们被放入父 CAG 活动的顺 序。 在每一个子活动执行结束后,CAG 将对 until 条件进行重新判定,同样的还有子活动的 when 条件。这是因为对于一个正执行的活动来说,一旦它结束,就可能影响到其它子活动 的执行顺序,甚至是整个 CAG 活动。 CAG 在工作流视图设计器中的使用方式也和其它活动大不一样。它的设计器用户界面和 其它活动的插入错误处理程序的界面相似(如图 11-1 所示)。假如你拖拽各个子活动到 CAG 的设计器界面上,并把它们放到两个箭头图标的中间,则在两个箭头中间的矩形略低的地方 将显示出如图 11-1 所示的文字:编辑。当你拖拽子活动并放到这个矩形中后,在下方的窗 口中将呈现出这些活动的图形。在图 11-1 中,你看到了一个名称为 setLevel1 的活动,它 来自于你即将创建的一个示例应用程序。 图 11-1 ConditionedActivityGroup 活动的设计器用户界面 子活动拖进 CAG 中后有两个显示模式:预览模式和编辑模式。图 11-1 显示的是编辑模 式。在编辑模式中,你能为子活动设置属性,例如设置它的 when 条件。当处于预览模式时, 你只能浏览子活动的设计器图片,这时在 Visual Studio 中显示出的属性都是 CAG 自己的。 点击图 11-1 中的“编辑”文字左边的小正方形按钮,或者双击主 CAG 窗口中的子活动,你 可在编辑和预览模式之间进行切换。 放到 CAG 中的子活动都只能是单一的活动。假如在你的工作流处理过程中其中一个子活 动需要执行超过一个以上的功能,就像处理一个事件的过程中,在事件响应时去执行一个 Code 活动一样,你应使用一个 Sequence 活动来作为容器,把它放入 CAG 中作为 CAG 活动直 接的子活动。 像 CAG 之类的活动的使用场合在什么地方呢?我认为它是那些很少使用的活动中的一 个,但当它适合你的处理模型时,它就会使事情变得非常的简单。 例如,想 象这样一个工作流程,需要对某些化学制品或材料的量进行监控。假如你往储 备箱中填入了过量的这些东西,则工作流会自动的把这些过多的制品或物料释放到一个溢出 箱中。当储备箱为空或者低于一个特定的下限值时,这个工作流会检测到这个情况并发送一 条警告信息。对于其它情况,工作流继续对储备箱中的量进行监控,但它不会产生任何动作。 把上面这些转换成 CAG 后,CAG 活动会一直运行到你决定不再需要进行监控时为止。假 如储备箱变得太空,一个子活动会发出一个警告,假如制品或物料的量超过了特定的上限值, 则另一个子活动会打开溢出箱。你可使用一个包含了 IfElse 活动的 while 活动来实现同样 的功能,但在这个例子中 CAG 是最合适的。(也可以说使用 CAG 活动是更加简洁的解决办法。) 为了对 CAG 进行说明示范,我已创建好了我提到的这个应用程序。TankMonitor 使用一 个工作流来监控储备箱中流体的量,它使用了一个简单的动画控件来模拟这个储备箱。图 11-2 展示了这个空储备箱。 图 11-2 储备箱为空时的 TankMonitor 用户界面 图 11-3 展示的是储备箱为半满时的情形。而图 11-4 展示出了储备箱中流体过量时的情 形。和你看到的一样,储备箱下面的 label 控件会为你提供任何状态提示信息。这个 label 控件的提示信息完全由工作流的反馈结果控制,而不是被储备箱的滑动条控件直接进行控 制。 图 11-3 储备箱为半满时的 TankMonitor 用户界面 图 11-4 储备箱中流体过量时的 TankMonitor 用户界面 在你的工作流中使用 ConditionedActivityGroup 活动 1.在下载的本章源代码文件夹目录中打开名称为 TankMonitor 的练习项目解决方案。 2.在 Visual Studio 打开了该解决方案后,在工作流视图设计器中打开 Workflow1.cs 文件。 3.拖拽一个 ConditionedActivityGroup 活动到工作流视图设计器界面上。 4.下图是 conditionedActivityGroup1 在 Visual Studio 的属性窗口中呈现的界面。选 择 UntilCondition 属性,这会显示出一个向下的箭头。点击这个向下的箭头,这会显示出 列表选择项,此处选择代码条件。 5.通过点击加号(+)展开 UntilCondition 属性,在 Condition 属性中输入 CheckContinue。在 Visual Studio 添加了对应的事件处理程序后,重新回到工作流视图设 计器上来。 6.生成该解决方案(按下 F6),这将使本项目中的这个自定义活动呈现在工具箱中。 现在,需向 CAG 中添加第一个子活动。从 Visual Studio 工具箱中,拖拽一个自定义 SetLevel 活动到工作流视图设计器界面上,把它放到 CAG 活动的矩形区域中,矩形区域的具体位置在 “<”按钮的右边。如下图所示: 备注:在 CAG 的主窗口内显示的锁状图标表示子活动处于预览模式。(CAG 主窗口上方 的文字也指明了当前所处的模式。)进入编辑模式后你能对 setLevel1 的属性进行编辑,通 过点击“<”按钮右方矩形窗口中的 setLevel1 活动的图标,你也能进入编辑模式并对它的 属性进行编辑。 7.我们现在就先进入 CAG 的编辑模式,点击“预览”文字旁边的微型正方形按钮。这是 一个开关型的按钮,如再点击一次这个按钮,将使 CAG 再次进入预览模式。 8.进入 CAG 的编辑模式后,你 就可通过在 CAG 的主窗口中选中它的子活动,然后就可对 它的属性进行设置。我们现在点击 setLevel1 活动以便激活它的属性。 9.选中 setLevel1 的 WhenCondition 属性,它将显示一个向下的箭头,然后从它的列表 选择项中选择代码条件。 10.展开这个 WhenCondition 属性,在 Condition 属性的文本框中输入 AlwaysExecute。 Visual Studio 也会自动为你添加一个对应的方法并会切换到代码视图下。我们需回到工作 流视图设计器界面下,因为你还需对 setLevel1 活动的多个属性进行设置。 11.在 setLevel1 的 Invoked 属性中输入 OnSetLevel 并按下回车键。在 Visual Studio 添加了对应的 OnSetLevel 事件处理程序后,重新回到工作流视图设计器界面下。 12.你需要为 setLevel1 设置的最后一个属性是 Level 属性。选择它的 Level 属性,这 将显示一个浏览(...)按钮,然后点击这个浏览按钮。这将打开一个“将‘Level’绑定到 活动的属性”对话框。然后点击绑定到新成员选项卡,点击创建属性,在新成员名称中输入 TankLevel,最后点击确定。 13.你现在需要向 CAG 中放入另外一个子活动。从工具箱中拖拽一个自定义 Stop 活动到 “<”按钮右边的矩形窗口中,并把它放到 setLevel1 活动的右边。 14.选择 stop1 的 WhenCondition 属性,这将显示一个向下的箭头,从它的列表选择项 中选择代码条件。 15.点击 WhenCondition 旁边的“+”,这将显示出 Condition 属性。在这个例子中,你 可和 setLevel1 活动共享一个 Condition 属性值,因此,选中这个 Condition 属性,这将显 示一个下拉列表框,然后从它显示出的列表项中选择 AlwaysExecute。 16.接下来选中 stop1 的 Invoked 属性,在它的文本框中输入 OnStop。同样,这也会添 加一个 OnStop 方法。在添加了这个方法后重新回到工作流视图设计器界面上来。 17.现在准备向 CAG 中添加第三个活动。这 次 ,拖 拽 一个自定义 UnderfillAlert 活动到 设计器界面上,把它放到 CAG 中 stop1 活动的右边。 18.点击 underfillAlert1 活动的 WhenCondition 属性,从显示出的下拉列表框中选择 代码条件。 19.点击“+”展开该 WhenConditon 属性,在 Condition 中输入 CheckEmpty。同样,在 添加了对应的 CheckEmpty 方法后回到工作流视图设计器界面上来。 20.接下来,你需要把 underfillAlert1 的 level 属性绑定到你在第 12 步中创建的 TankLevel 属性。为此,点击 level 属性激活它的浏览(...)按钮。然后点击该浏览按钮, 这就打开一个“将‘level’绑定到活动的属性”对话框。但这次,你将绑定到一个现有的 属性,因此只需从现有的属性中选中 TankLevel 即可,然后点击确定。 21.underfillAlert1 活动就完全配置好了,我们现在要把最后一个活动添加到本示例 应用程序的 CAG 中。拖拽一个自定义 OverfillRelease 活动到设计器界面上,把它放到 CAG 中其它现有活动的最右边。 22.同样,你需要设置它的 WhenCondition 属性,因此点击 overfillRelease1 的 WhenCondition 属性并从它的下拉列表中选择代码条件。 23.展开 WhenCondition 旁边的“+”,这就显示出它的 Condition 属性。在该属性中输 入 CheckOverfill。在 添 加了对应的 CheckOverfill 方法后重新回到工作流视图设计器界面 上来。 24.和第 20 步中你为 underfillAlert 所做的工作一样,现在把 overfillRelease1 的 level 属性绑定到 TankLevel 属性上。点击 level 属性,这就显示出一个浏览(...)按钮。 点击该浏览按钮,这将打开一个“将‘level’绑定到活动的属性”对话框。从现有属性列 表中选择 TankLevel,然后点击确定。 25.本工作流在视图设计器界面上的设计工作就完成了,现在需要切换到 Workflow1.cs 的代码视图下添加一些代码。 26.打开 Workflow1.cs 源文件,在 源文件中找到 Workflow1 构造器。在 构 造 器 下 面 添 加 如下的代码,主要作用是创建在启动工作流时需要用到的储备箱中容纳流体的下限值和上限 值的属性。 private bool _stop = false; private Int32 _min = -1; private Int32 _max = -1; private bool _notificationIssued = false; public Int32 TankMinimum { get { return _min; } set { _min = value; } } public Int32 TankMaximum { get { return _max; } set { _max = value; } } 27.接下来你将看到的是 CheckContinue 方法,这个方法是你在设置 CAG 的 UntilCondition 属性时自动添加的。这个方法实际上是一个事件处理程序, ConditionalEventArgs 包含了一个 Result 属性,你 可对其设置以决定是让 CAG 继续进行处 理还是让它停止,如 设 置 Result 为 true 将使其停止,而设置为 false 将使其继续进行处理。 向 CheckContinue 中添加下面一行代码(_stop 是一个标志,它在 OnStop 事件处理程序中 被设置): e.Result = _stop; 28.两个 CAG 活动,setLevel1 和 stop1,都应当始终运行。因此在 AlwaysExecute 中添 加下面一行代码: e.Result = true; 29.找到 OnSetLevel 方法,在 SetLevel 事件被响应时将调用该方法。实际上储备箱中 的量是由 WF 自动为你设置的,因为你把 setLevel1 的 Level 属性绑定到了 TrankLevel 这个 依赖属性上。下 面 添 加的代码的作用是对任何警告通知进行重置,以 便 在 储 备 箱 中 量 的 新 值 不 再 合 理 范围内时能让 overfill 活动或 underfill 活动发出它们的通知。 _notificationIssued = false; 30.Stop 事件的作用是当它触发时对 CAG 的 UntilCondition 进行设置以使它停止处理。 在 Workflow1.cs 文件中找到 OnStop 方法,并为它添加下面两行代码: // Set the stop flag _stop = true; 31.接下来定位到 underfillAlert1 的 WhenConditon 属性对应的 CheckEmpty 方法。尽 管你想让 CAG 每次都对它的子活动的 WhenCondition 进行判断,但你并不想让通知(低于下 限值或超过上限值时)不停地发送到用户界面上,因为这将消耗过多的 CPU 周期。实际上, 你只想让通知在储备箱的容量状态级别发生更改后只发出一次。下 面 的代码就为你做这个工 作,这些代码添加到 CheckEmpty 方法中。 // If too empty, execute e.Result = false; if (TankLevel <= TankMinimum) { e.Result = !_notificationIssued; _notificationIssued = e.Result; } // if 32.overfillRelease1也需要对它的WhenCondition进行判断,因此找到CheckOverfill 方法并添加和上面相似的代码: // If too full, execute e.Result = false; if (TankLevel >= TankMaximum) { e.Result = !_notificationIssued; _notificationIssued = e.Result; } // if 33.保存所有打开的文件,编译该解决方案。 34.执行该应用程序。向上或向下拉动滑动条可对储备箱进行补充或抽空进行模拟。当 储备箱中的流体超过或低于边界条件时注意观察它下面所显示的文本。 备注:我在创建这个 TankMonitor 应用程序的过程中,当添加事件处理程序时会感到它 非常像是一个基于状态机的工作流。假如在你创建顺序工作流的过程中发现,在一个特别的 业务流程中使用一点点基于状态机的处理流程会更有用的话,CAG 或许就是一个既快速又简 便的极好的选择,这不用重新创建并调用一个独立的基于状态机的工作流。 信不信由你,到现在你已经看到过足够多的用来解决许多和工作流任务相关的处理过 程,但这些章节讨论的话题还只是加深你对 WF 的理解。 第十二章:策略和规则 学习完本章,你将掌握: 1.知道在工作流处理过程中怎样进行策略和规则的处理 2.理解前向链接以及这是如何影响到基于规则的工作流处理过程的 3.为工作流处理过程创建规则 4.结合 Policy 活动来使用规则 我敢肯定,我们中的大多数人编写面向过程的代码(imperative code)都很轻松自在。 过程式代码指通过编程来实现业务处理过程的 C#代码,例如,读取一个数据库表,增加这 个表中某些列的值,然后把它们统统都写到另一个数据库的表中。 但在本章,我们将深入规则,规则是对工作流的执行进行控制的一种机制,但它被看作 是声明性的(declarative)。通常,声明性代码并不会被编译进程序集中,而是在应用程 序执行时被解释。ASP.NET 2.0 中有许多新的特征就是声明性的,这其中包括数据绑定和改 进了的模板控件。它们能够让你在写 ASP.NET 应用程序时不使用 C#代码就可去执行数据绑 定或者其它复杂的控件呈现任务。 Windows Workflow Foundation(WF)也具有声明性的能力,但它是对规则和策略进行 绑定而不是数据。你不能使用 HTML 或者 ASP.NET 的构造来声明你的规则,当然,涉及的概 念都是相似的。 但是什么是规则,什么又是策略呢? 策略和规则 当我写一个涉及到数据或业务过程的程序时,我都会对数据或业务过程进行理解并把它 转换成计算机去执行的代码。例如,考虑这样一个对帐目进行检查的处理逻辑:“假如在 AvailableBalance 列中的值少于要求的值,将抛出一个 OverdraftException 异常。”这似 乎很简单...下面是表达这个行为的一些伪代码: IF (requestedValue > AvailableBalance) THEN throw new OverdraftException("Insufficient funds.") 但是要是银行客户具有透支保障功能,假如主账户资金不足时能对次账户进行存取又会 怎么样呢?要是客户没有透支保障功能但是可自动设置透支范围的信贷业务又会怎么样 呢?要是客户两样都有呢……我们该使用哪一个呢? 就像你能预见到的,为了对各种情况都进行检查,代码就会变得既复杂又混乱。更糟糕 的是,它不能很方便地移植到其它业务处理过程中,并且它维护起来可能也很困难。 更进一步,我们看到了这些不只是去进行数据处理而且还有数据之间的关系。在代码中, 我们运用过程化的处理方式来对关系进行处理,这 些通常都会被翻译成许多嵌套的 if 语句, swith 语句和循环。假如以前你在处理过程中使用了大量的 if 语句去对所有可能的条件检 查,你或许应该问问自己是否已经没有更好的方式了。 至少在 WF中有更好的方式。我们可以创建声明性规则然后使用规则引擎(rules engine) 来处理它们。声明性规则对关系进行描述说明,它也适合应用到潜在要进行判断的地方。 WF 承载了一个规则引擎(rules engine)。该规则引擎可使用 XML 格式编码的规则, 并且能把这些规则应用到你的工作流的方法和字段中。在 WF 中,你能把面向过程的代码和 声明性规则两者结合在一起形成一个总的解决办法。 WF 中主要有两个地方会用到规则处理:条件处理和策略。你将发现条件处理是 IfElse、 While、Replicator 以及 ConditionedActivityGroup 这些活动的一部分。假如你回顾一下 第 9 章“逻辑流活动”和第 11 章“并行活动”的话,在那些地方介绍和示范的活动中,在 每种情况下我都使用一个代码条件来对处理流程进行判断。当然,代码条件的实现是你工作 流处理类中的一个事件处理程序(它通过 WF 所提供的一个 CodeCondition 类被绑定)。但 是,在本章中你将开始使用规则条件进行替换。直到目前在本书中还没有体验过策略的使用, 但在本章中当我介绍 Policy 活动时将对策略进行演示。 备注:对于 WF 和基于规则的处理可以写完整地一本甚至是一部系列丛书。我不可能在 本章覆盖到各个方方面面。但可以做到的是对几个关键的概念进行介绍,这 些 概念对于你来 说是全新的,并且也为你提供了一些基于 WF 的应用程序,它们用来对基于规则的处理过程 的某个特定方面进行演示。假如你对这些话题感兴趣,我强烈建议你花些宝贵时间到 Google 上(http://www.google.com/),大 量 的 网站 都 有 关于 在 基于工作流的系统中实现业务处理 流程方面的论文和资料。 在 WF 中,规则(rule)通过条件来表示,它返回一个 Boolean 值,并伴随着一个或多 个操作。WF 中规则风格的布局遵循 if-then-else 风格。规则引擎对条件进行判断,然后指 挥工作流按照条件处理的结果去执行。在一定程度上,规则类似于脚本代码,与 规则 引 擎 一 起充当脚本执行环境。在面向过程的代码之上使用规则的优点是规则能很容易地进行修改, 以让你部分的业务处理过程更容易地适应易变的环境。 在 WF 术语中的策略是指规则的集合,它被包含到一个规则集(RuleSet)中。这使有些 被称作前向链接(forward chaining)的事情变得更方便,这个假想的术语指的是在当前处 理规则发生改变导致状态变化后,能对规则重新进行判定。 实现规则 规则基于 XML,当在 Microsoft Visual Studio 中生成你的工作流时,这些 XML 被编 译成资源。许多基于 WF 的类都了解和规则相关的具体细节,它们都在 System.Workflow.Activities.Rules 中。这些类和 XML 协同工作去执行规则脚本,最终生 成一个以 true 或者 false 为结果的条件语句,你的工作流逻辑使用它来指挥处理流程。 在 Visual Studio 中通过两个主要的用户界面来让规则协同工作。对于简单的规则编 辑过程,就像在基于流的活动(在第 9 章和第 11 章讨论过)中的条件赋值一样,你可使用 一个用户界面来对规则进行编辑,该用户界面能让你生成规则文本。在你的规则中,你 可同 时使用脚本化的关系运算符(如表 12-1 所示),算术运算符(如表 12-2 所示)、逻辑运算 符(如表 12-3 所示)、关键字(如表 12-4 所示)以及字段、属性和方法,以在你的工作流 中为基于流的活动去判定该条件表达式。 表 12-1 规则关系运算符 运算符 功能 ==或= 测试是否相等 >或>= 测试是否大于(>)或者是否大于等于(>=) <或<= 测试是否小于(<)或者是否小于等于(<=) 表 12-2 规则算术运算符 运算符 功能 + 加 - 减 * 乘 / 除 MOD 模 表 12-3 规则逻辑运算符 运算符 功能 AND 或&& 逻辑与 OR 或|| 逻辑或 NOT 或! 逻辑非 & 位与 | 位或 表 12-4 规则关键字 运算符 功能 IF 开始条件测试 THEN 假如条件测试值为 true 时所执行的流路径 ELSE 假如条件测试值为 false 所执行的流路径 HALT 终止规则处理过程,把控制权返回给使用该规则的活动,但是这和使用 Terminate 活动是不同的。工作流处理过程并没有结束,仅仅是特定的条 件停止处理。 Update 通知规则引擎有一个特定的字段或属性值已经被修改了(这可方便地对前 向链接进行依赖检查,这将在本章的晚些时候进行论述)。 对于策略,你 可 使用一个专门的编辑器来编辑你的规则集(如图 12-1 所示)。在这里, 你能批量地编辑并组成规则集。你 可 指 定 规 则的优先级,假如条件改变时怎样对规则进行重 新计算,以 及 指 定 你想运用的前向链接(forward chaining)机制。当你通过 Visual Studio 的规则集编辑器用户界面来创建规则时,你可指定规则优先接收的值以及它的前向链接行 为。 图 12-1 规则集编辑器用户界面 规则属性 当通过规则调用你工作流的方法时,或许会有规则引擎不知道的依赖关系。当它们共 享工作流字段或属性时,规则就变成依赖的了。有 时 依赖关系并不明显,但有时却不。例如, 设想一个顾客购买了一定数量的东西后可允许免费送货,但仍然要对手续费进行确定。这 可 考虑这些规则: IF this.PurchasedQuantity >= this.DiscountQuantity THEN this.DiscountShipping(1 .0) AND IF this.HandlingCost > 0 THEN this.OrderCost = this.OrderCost + this.HandlingCost 第一条规则陈述了假如买的东西的数量超过了一个门槛值,就可不用收取运货费用(运 货费用的折扣率是 100%,注意我们调用了一个方法去设置这个值)。第二条规则在完全不 同的工作流部分中执行,它把手续费加到订单价格总额中。假如运费打了折扣,通常也就存 在手续费用,但这两条规则是独立的。假如调用了 DiscountShipping 把一个值写到 HandlingCost 属性中,并且写入后导致了第二条规则稍后会在处理中去执行(译者注:因 为此时 handlingCost > 0,会执行第二条规则),你就应当让规则引擎知道这里存在依赖 关系,方法是使用一个特殊的基于规则的工作流特性,它们都被列到了表 12-5 中。下面的 代码展示了其中的一个特性行为: [RuleWrite("HandlingCost")] public void DisountShipping(decimal percentageDiscount) { // 这里是更新手续费的代码 } 在处理前向链接(forward chaining)时这些特性将起作用。 表 12-5 基于规则的特性 特性 功能 RuleRead 这个特性是默认的。该特性通知规则引擎方法可读出工作流实例的属性和 字段,但不能更新它们的值。 RuleWrite 该特性通知规则引擎工作流方法可更新该潜在依赖的字段或属性的值。 RuleInvoke 该特性告知规则引擎被这个特性修饰的方法可调用一个或多个其它的方 法,这些方法或许也会对潜在依赖的字段或属性进行更新。 Update 语句 表 12-4 中列出了你可自由使用的基于规则的关键字。它们相对都具有自解释性,但 Update 例外。作为基于规则的特性,当我们接触前向链接时将对 Update 进行更多的讨论, 但核心思想是要通知规则引擎你的规则是在明确地对一个字段或属性进行更新,以 便 使其它 相依赖的规则知道这个更改。Update 实际上并不对字段或属性进行修改——它只是通知规 则引擎该字段或属性被改变了。 Update 需要一个单一的字符串,它表示字段或属性的名称,它的用途是通知规则引擎 相依赖的规则可能需要重新判定。尽管最佳做法的原则是使用基于规则的特性作为首选,但 有时使用 Update 更恰当。这有一个很好的例子:当你在一个工作流程序集上不能进行写入 操作但要修改某个属性时( 一是没有基于规则的属性,一是你不能更新源代码以让它包含必 要的特性)。 可能对于理解在工作流处理过程中能怎样去使用规则的最好方式是开始去写一些代码 进行试验。我们将从规则条件开始,它可和我们在第 9 章中使用过的代码条件相对比。 规则条件 对条件表达式进行判定的 WF 活动有 IfElse 活动、While 活动、Replicator 活动和 ConditionedActivityGroup 活动。这 些活动当中的每一个都会要求你做一个 true/false 的 判断。在第 9 章中,我们使用过“代码条件”属性设置,它使 Visual Studio 向我们的工作 流代码中添加进一个 event handler,该事件参数的类型是 ConditionalEventArgs,它包含 了一个 Result 属性,我们可设置它为 true 或者 false,这取决于我们的决定。 但是,对于这些每一个条件的判定,我们也可使用“规则条件”来进行替换。规则条 件是对 true 或者 false 进行判断的一组规则,例如:购买的物品数目超过了免费送货的界 限值。为了清楚地说明这一点,这里有一个使用了规则条件的示例应用程序。 创建一个使用了“规则条件”进行条件判定的新工作流应用程序 1.下载本章源代码,打开 RuleQuestioner 目录中的解决方案 2.创建一个顺序工作流库项目,步骤可参考第 3 章“工作流实例”中“ 向 WorkflowHost 解决方案中添加一个顺序工作流项目”一节中的步骤。把该工作流库的名称命名为 “RuleFlow”。 3.在 Visual Studio 添加了该 RuleFlow 项目后,打开工具箱,拖拽一个 Code 活动到 设计器界面上,在它的 ExecuteCode 属性值中输入 AskQuestion。 4.Visual Studio 会创建该 AskQuestion 方法并为你自动切换到代码视图界面下。在 该 AskQuestion 方法中输入下面的代码: // Ask a question DialogResult res = MessageBox.Show("Is today Tuesday?", "RuleFlow", MessageBoxButtons.YesNo, MessageBoxIcon.Question); _bAnswer = res == DialogResult.Yes; 5.找到 Workflow1 的构造器,在该构造器下面添加这些代码: private bool _bAnswer = false; 6.添加如下的名称空间: using System.Windows.Forms; 7.因为 MessageBox 由 System.Windows.Forms 支持,当创建一个顺序工作流项目时该 程序集不会自动地被 Visual Studio 所引用,你需要添加该引用。因此在解决方案资源管理 器中的 RuleFlow 项目内的引用树状结点上单击右键,然后从右键快捷菜单中选择“添加引 用”,点击.NET 选项卡,在列表中找到 System.Windows.Forms。选中它然后点击确定。 8.然后切换到工作流视图设计器界面上来。拖拽一个 IfElse 活动到工作流视图设计器 界面上,把它放到你刚刚所放入的 Code 活动的下面。红色的感叹号标记表明还需要完成额 外的工作,在本例中意味着我们还需要添加触发工作流去选择执行左边的路径(“true”) 还是右边的路径(“false”)的条件。 9.在工作流视图设计器中,选择左边的 ifElseBranchActivity1 分支,在 Visual Studio 的属性面板中将显示该活动的属性。 10.选中 Condition 属性,点击下拉箭头,这将显示可供使用的条件处理选项的选择列 表。选择声明性规则条件选项。 11.通过点击加号(+)展开 Condition 属性,点击 ConditionName 属性,这将激活浏 览(...)按钮,点击它。 12.这将打开“选择条件”对话框,点击新建按钮。 13.这会打开“规则条件编辑器”对话框。在“条件”域中输入 System.DateTime.Now.DayOfWeek == System.DayOfWeek.Tuesday,然后点击确定。 14.注意在“选择条件”对话框的条件列表中就有了一个名称为条件 1 的条件。 15.在这里,IfElse 活动就有了一个条件去进行处理,但是它并没有执行任何代码! 因此,拖拽一个 Code 活动到设计器界面上并把它放进左边的分支中。在它的 ExecuteCode 属性中输入 ShowTuesday。 16.Visual Studio 会为你自动切换到代码视图下,在该 ShowTuesday 事件处理程序中 输入下面的代码,然后重新切换到工作流视图设计器界面上来。 string msg = _bAnswer ? "The workflow agrees, it is Tuesday!" : "Sorry, but today IS Tuesday!"; MessageBox.Show(msg); 17.拖拽第二个 Code 活动到 IfElse 活动的右边分支中,在它的 ExecuteCode 属性中输 入 ShowNotTuesday。 18.在 Visual Studio 为你切换到代码视图后,在 ShowNotTuesday 事件处理程序中输 入下面的代码后重新回到工作流视图设计器界面上来: string msg = !_bAnswer ? "The workflow agrees, it is not Tuesday!" : "Sorry, but today is NOT Tuesday!"; MessageBox.Show(msg); 19.工作流现在就设计完成了,现在从 RuleQuestioner 应用程序中添加对该工作流的 项目级引用。 20.在 RuleQuestioner 项目中打开 Program.cs 文件,找到下面的代码: // Print banner. Console.WriteLine("Waiting for workflow completion."); 21.在上面找到的代码下面添加如下代码,这将创建一个工作流实例。 // Create the workflow instance. WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(RuleFlow.Workflow1)); // Start the workflow instance. instance.Start(); 22.编译该解决方案,纠正任何可能出现的编译错误。 23.按下 F5(或者 Ctrl+F5)执行该应用程序。 假如你仔细看看第 13 步,你 会发现我们添加的规则和用户是否通知了工作流今天是不 是星期二完全无关。规则对属于一周的哪一天进行了检查,它 也 应 该能把用户的输入考虑在 内。(我也可以向规则中添加 this._bAnswer 去访问该 Boolean 值。) 你可能也会对为什么这样做要比使用代码条件好而感到疑惑。其实,并不能说一个要 比另一个要好,效果都是一样的。对于基于规则的条件的过程来说,对判定做出改变的东西 是保存起来的规则,这 可 在 运 行的时候使用不同的规则去进行替换。这是一个很强大的概念。 当涉及超过一个以上的规则的时候,它甚至会变得更加强大,这就是使用策略(policy)的 情况。但在我们涉及策略之前,我们需要看看前向链接(forward chaining)。 前向链接 假如你曾经观看过轿车的组装过程的话,你绝对会感到惊奇。轿车本身实际上就非常 复杂,组装的过程甚至一定会更加复杂。组装过程中隐藏的是一个选项的概念,轿车有一些 可选的部件。一些或许有卫星收音机,其它或许要有 GPS(全球定位系统)以便驾驶员绝不 会迷路。并非组装线上的所有轿车都有每个组装选项。 因此当一辆轿车从线上下来的时侯,组装过程通常会改变。一些组装选项要求在组装 过程中很早的时候就布下不同的电气配线、或者更持久的电池、或者不同的引擎部件。 问题是组装过程以每辆车作为基础进行变化。在每个装配站,线上的工人(或者机器 人)都会被告知要组装什么部件。告知他们的过程可以容易地设想为一个使用了基于规则的 方式的工作流过程。此外,早期作出的判定结果也会影响到后期将怎样去进行判定。有些 选 项 和其它选项不能同时存在,因此在轿车从线上下来时组装过程必须进行改变。 这就是前向链接的本质。规则紧密地链接在一起,就像一个规则的判定结果会影响到 接下来的规则会怎样去进行判定。当我们有超过一个以上的规则要去处理时,就像是我们将 使用的策略的过程,我们需要去关注规则依赖以及想怎样去处理前向链接。 备注:术语“规则间的依赖关系”真正的意思是两个或更多的规则共享了相同的工作 流字段或属性。假如没有规则和其它的规则共享访问相同的工作流字段或属性,则这两个规 则之间也就没有依赖关系。假如存在依赖关系,则这个问题将通知规则引擎存在着依赖关系, 在有些情况下也有可能要掩盖这些依赖关系的存在。(我们将在这节看到这些内容。) 正如我在本章前面提到过的,规则被聚集到一个规则集(RuleSet)中。在规则集中的 规则能被指派优先级,在一个特定的时间点上你能指定它们是否处于激活状态(和 enabled 属性相似)。当正在处理一个以上的规则时,将以下面的方式来对这些规则进行处理。 1.派生出的处于激活状态的规则列表。 2.所找到的最高优先级的规则(或者规则集)。 3.对规则(或多条规则)进行判定,然后必须执行它的 then 或者 else 内的操作步骤。 4.假如规则更新了前面所提到的规则列表中具有更高优先级的规则所使用过的工作流 的字段或属性的话,前面的规则会被重新判定并执行它所必须要去执行的步骤。 5.继续进行处理过程,直到根据需要判定完(或者是重新判定)规 则 集 中的所有规则。 通常在三种情况下规则可以是前向链接的:隐式链接(implicit chaining)、特性声 明链接(attributed chaining)和显式链接(explicit chaining)。也就是说,规则能被 进行链接并且共享依赖,因为工作流运行时能(在某些条件下)弄清是否有这个必要(这是 隐式链接),你 也 可应用基于规则的特性中的某一个标记来声明某个方法( 这是特性声明链 接),或者使用一个 Update 语句(这是显式链接)。我们就来对每一个进行简要的看看。 隐式链接 当字段和属性被一条规则进行了更新,而这些字段或属性又显而易见地被其它规则所 读出的时候,就会产生隐式链接。例如,考虑这些规则: IF this.OrderQuantity > 500 THEN this.Discount = 0.1 And IF this.Discount > 0 && this.Customer == "Contoso" THEN this.ShippingCost = 0 第一条规则在订单数量超过 500 单位时将进行打折。第二条规则陈述了假如公司是 Contoso 并且也进行了打折,则对运费免费。假如第一条规则起作用的话,第二条规则可能 需要再次进行重新判定并执行。 特性声明链接 因为在你的工作流中的方法能对字段和属性进行修改,但是规则引擎可能对此却一无 所知,因此 WF 提供了我在本章前面提到过的基于规则的特性。结合先前的例子,对规则稍 微进行改写,特性声明链接可能看起来就像下面这样: IF this.OrderQuantity > 500 THEN this.SetDiscount(0.1) AND IF this.Discount > 0 && this.Customer == "Contoso" THEN this.ShippingCost = 0 这里,第一条规则调用了工作流类中的一个方法:SetDiscunt,它 对 Discount 属性进 行了更新。但规则引擎并不知道 SetDiscount 将改变 Discount 的值,因此当写 SetDiscount 方法时,你应当使用 RuleWrite(或者 RuleInvoke)特性: [RuleWrite("Discount")] private void SetDiscount(decimal discountValue) { } RuleWrite 特性将通知规则引擎对 SetDiscount 的调用将导致对 Discount 属性进行更 新。因为这些形成了一个依赖关系,因此假如 SetDiscount 方法被调用的时候这些规则将会 被重新进行判定。 显式链接 最后一种前向链接是显式的,也就是说你的规则中使用了 Update 语句来告知规则引擎 有一个字段或属性的值已经被修改了。Update 的作用等同于使用 RuleWrite 特性。但是正 如你所知道的,当调用一个工作流方法的时候,规则引擎并不知道该方法是否对某个规则依 赖到的字段或属性进行了更新。在这种情况下,你调用了工作流方法后接着通过使用一个 Update 语句来告知该规则引擎存在的依赖关系。 这些或许听起来有些古怪,但它还是有使用价值的。假如你写你自己的工作流的话, 你应该使用基于规则的特性。但是,当基于工作流的软件变得普遍,人们开始使用第三方的 工作流的时候,他们可能会发现基于规则的特性并没有应用到各个工作流方法中去。在这些 情况中,他们就应当使用 Update 语句来保持正确的工作流状态以及规则引擎的同步。基于 规则的特性是以声明的方式来指明更新,而 Update 语句则是在不可避免的时侯使用。当使 用了已预编译过的第三方软件的时候,你就需要这种不可避免的方案。 回到先前的例子,假设 SetDiscount 方法并没有应用 RuleWrite 特性。则这两条规则 就会和下面的这些看起来相像。 IF this.OrderQuantity > 500 THEN this.SetDiscount(0.1) Update(this.Discount) And IF this.Discount > 0 && this.Customer == "Contoso" THEN this.ShippingCost = 0 有了这些信息,规则引擎就知道了 Discount 属性已经本更新了,并且也因此将对规则 的适用范围进行重新判定。 控制前向链接 你可能会认为一旦你开始了基于规则的工作流的执行后,你就失去了对它的控制并允 许规则引擎能进行所有的判定。尽管大多数情况下这正是你想做的,但在处理规则依赖和前 向链接上也有一些控制权。 表 12-6 列出了你所具有的对前向链接进行控制的三种类型。 表 12-6 前向链接控制行为 行为 功能 Full Chaining 这是默认的,这个行为允许工作流引擎在它认为有必要的时候去对规则进 行处理和重新判断。 Explicit Chaining 当应用它时,该控制行为就把前向链接行为的应用范围限定在包含了 Update 语句的规则上。 Sequential 这实质上是把前向链接关闭。(指明)没有会被判定到的依赖关系,规则 依次、按顺序被使用。 完全链接(full chaining)行为能让规则引擎根据需要去对包括隐式的和特性的在内 的规则进行重新判定。 仅显式更新链接(explicit chaining)行为会使隐式的和特性标记的前向链接无效, 并且它应用在使用了显式前向链接,需要由你直接负责去通知规则引擎存在依赖关系的地 方。在使用了 Update 语句的地方,你在规则依赖和重新判定上有总的控制权。在省略了 Update 语句的地方,规则引擎不会做任何确定是否存在依赖的尝试,因此即使实际上存在 依赖关系,规则也将不会被重新进行判定。它的作用是在你的规则中增加了 Update 语句后, 你就在前向链接上掌握了完全的控制权。你 可 能会这样去做以提高性能( 因 为 规 则 引 擎 就 不 再 对 所 有 那 些 非必要的规则进行重新判定),你 也 可 能 必须去这样做以便消除你的规则中的 依赖循环。 顺序的链接(sequential chaining)行为实际上是把所有的前向链接关闭。规则从顶 到底地在单一的通道上被判定。假如存在依赖关系,这些依赖会被完全忽略。 提示:优先级的正确使用通常也能高效地对前向链接进行控制。高优先级的规则首先 执行,因此,高优先级的规则会在低优先级的规则执行以前对低优先级将使用的字段和属性 的值进行更新和确定。就像你想起的,在同一个 Visual Studio 用户界面中要根据你要使用 的优先级去创建规则。 控制规则的重新判定 在怎样对规则重新判定上你也有控制权。表 12-7 列出了这些方法。一个需要牢记的事 情是规则的重新判定模式只在个别规则等级上应用。在一个接一个的规则基础上,你 能 为特 定的规则指定重新判定的模式。 表 12-7 规则重新判定模式 模式 功能 Always 这是默认的。这个模式使工作引擎在必要时对规则进行重新判定。 Never 当应用该模式时,将指明规则只能被判定一次(绝不会被重新判定)。 通过总是(Always 模式)使规则进行重新判定,规则引擎作出的判定可能会改变那些 根据临时状态变化进行对应处理的规则的最终结果。在 依赖字段或属性值被修改时,规则引 擎能在必要时重新执行规则以把这些修改考虑在内。 但是,有时你可能不想这样,这时你可选择 Never 作为你的规则重新判定模式。你为 什么会选择这种重新判定模式呢?哦,其中一个例子可能包括以下内容: IF this.Handling < 5.0 && this.OrderQuantity > 500 THEN this.Handling = 0 这条规则的意思是:“假如手续费低于$5.0 并且订单数量超过了 500 个单位的话,那 么就不收取任何的手续费。”但 是 当 满 足 该规则判定标准并且把手术费设置为 0 时会发生什 么呢?哦,依赖属性 Handling 被更新了,因此该规则要重新判定!假如你猜到该规则会导 致一个无限循环的话,你猜中了。因此,应用一个 Never 类型的重新判定模式很有意义:手 续费用一旦为 0,为什么还需再次对规则进行判定呢?尽管在写这个特定的规则时可能使用 其它的方式来防止出现无限循环,但问题是在你的工作流创作工具包你有这样一种重新判定 模式来作为工具,你又为什么不利用它呢? 使用策略活动 当超过一个以上的规则要被处理的时候,前向链接这种情形就出现了。对于规则条件 (Rule Condition)的情形来说,这是不可能发生的情况:因为这种情况下只有一个规则。 事实上,它甚至不是一个完整的规则而只是一个布尔表达式。但是,Policy 活动改变了所 有这些。有了 Policy 活动,你就有机会把多个复杂的规则组合到一起,并且你可能会看到 (某些时候也可能看不到)前向链接的结果。 当你使用 Policy 活动的时候,规则被聚集进一个集合中,这个集合通过 WF 的规则集 (RuleSet)对象来维护。当你把 Policy 活动拖拽进你的工作流中后,你 需 要创建一个规则 集对象并插入到你的规则中,在必要时应用前向链接进行控制并使用规则重新判定模式。 Visual Studio 为帮助创建规则集合提供了一个用户设计界面,就像有一个为添加单一的规 则条件(Rule Condition)的用户界面一样。 为了演示 Policy 活动,让我们重新回味一下我在第 4 章中“ 选择一种工作流类型”这 一 节 中 所 概述 的 情 景 。我 不 会 实现前面提到过的所有规则,但我将实现足够多的规则以便对 Policy 活动的功能进行演示。这些规则集如下所示: 1.当你收到一份订单时,检查你帐目上目前还有的增塑剂的合计数目。假如你认为数 目足够的话,就可尝试填写一份完整的订单。否则的话,就准备填写一份部分出货类型的订 单。 2.假如你正填的是一份部分出货类型的订单,检查看看订货的公司是接受这种部分出 货类型的订单呢还是需要让你先等等,直到你能提供一份完整的订单时为止。 3.假如你正填的是一份完整的订单,就检查在储备罐中增塑剂的实际的量(有些可能 已经被蒸发了)。假如具有足够的增塑剂来履行这份完整的订单,就处理这份完整的订单。 4.假如没有足够的增塑剂来履行这份订单,就把它当部分出货的订单类型处理。(看 看第二条规则。) 我也知道任何有竞争力的塑胶公司都会知道在储备罐中储存的增塑剂的真实的量,但 这仍不失为一个好例子,因为这里实际上包含了许多的条件。假如订单一来并且我们知道我 们没有足够的量来满足它,我们就看看我们是否可以提供一份部分出货类型的订单(这些能 够根据和客户达成的协议作出选择)。我们也总是可以尝试对我们知道能完全满足的订单进 行处理,但当增塑剂的实际的量和我们先前认为的量有所区别的时候会发生什么呢?,这 会 导 致 部分出货吗?这种情形我很有兴趣进行演示,因为它显示出了在操作过程中的规则判定 处理。 假设我们是塑胶制造商,我们有两个主要的客户:Tailspin Toys 和 Wingtip Toys。 Tailspin Toys 已告知我们他们可以接受部分出货,但 Wingtip 需要订单要完整地发送。我 们的工作流将使用一个 Policy 活动来把这些规则应用到我概述的这些客户、他们的订单以 及我们手头的原料数量当中,这可能(或不能)足够完成他们的订单。我们在操作中来看看 这个活动。 创建一个使用了 Policy 活动的新工作流应用程序 1.该 PlasticPolicy 应用程序再次为你提供了两个版本:完整版本和非完整版本。你 需要下载本章源代码,打开 PlasticPolicy 文件夹中的解决方案。 2.在 Visual Studio 加载了 PlasticPolicy 解决方案后,新创建一个顺序工作流库的 项目,该工作流库的名称命名为 PlasticFlow。 3.在 Visual Studio 添加了该 PlasticFlow 项目后,Visual Studio 会打开 Workflow1 工作流以便在工作流视图设计器中进行编辑。打开工具箱,拖拽一个 Policy 活动到设计器 界面上。 4.在你真正创建规则以便把它们和你刚才插入进你的工作流中的 Policy 活动相配合 前,你需要添加一些初始化代码和方法。以代码视图的方式打开 Workflow1.cs 文件,在该 类的构造器前添加这些代码: private enum Shipping { Hold, Partial }; private decimal _plasticizer = 14592.7m; private decimal _plasticizerActual = 12879.2m; private decimal _plasticizerRatio = 27.4m; // plasticizer for one item private Dictionary _shipping = null; // Results storage private bool _shipPartial = false; private Int32 _shipQty = 0; // Order amount private Int32 _orderQty = 0; public Int32 OrderQuantity { get { return _orderQty; } set { // Can't be less than zero if (value < 0) _orderQty = 0; else _orderQty = value; } } // Customer private string _customer = String.Empty; public string Customer { get { return _customer; } set { _customer = value; } } 5.添加下面的名称空间: using System.Collections.Generic; 6.再次找到 Workflow1 的构造器,在该构造器内调用初始化组件 (InistializeComponent)的方法下添加下面这些代码: // Establish shipping for known customers this._shipping = new Dictionary(); this._shipping.Add("Tailspin", Shipping.Partial); this._shipping.Add("Tailspin Toys", Shipping.Partial); this._shipping.Add("Wingtip", Shipping.Hold); this._shipping.Add("Wingtip Toys", Shipping.Hold); 7.在构造器下添加下面这些方法: private bool CheckPlasticizer() { // Check to see that we have enough plasticizer return _plasticizer - (OrderQuantity * _plasticizerRatio) > 0.0m; } private bool CheckActualPlasticizer() { // Check to see that we have enough plasticizer return _plasticizerActual - (OrderQuantity * _plasticizerRatio) > 0.0m; } [RuleWrite("_shipQty")] private void ProcessFullOrder() { // Set shipping quantity equal to the ordered quantity _shipQty = OrderQuantity; } [RuleWrite("_shipQty")] private void ProcessPartialOrder() { // We can ship only as much as we can make _shipQty = (Int32)Math.Floor(_plasticizerActual / _plasticizerRatio); } 8.为了使你能在规则处理时看到输出结果,你需要激活属性面板。在属性面板中,点 击事件工具条按钮,然后在 Completed 事件中输入 ProcessingComplete。这会在你的工作 流代码中为 WorkflowComplete事件添加一个对应的 event handler并为你切换到 Workflow1 类的代码编辑界面下。 9.定位到 Visual Studio 刚刚添加的 ProcessingComplete 方法,插入下面这些代码: Console.WriteLine("Order for {0} {1} be completed.", _customer, OrderQuantity == _shipQty ? "can" : "cannot"); Console.WriteLine("Order will be {0}", OrderQuantity == _shipQty ? "processed and shipped" : _shipPartial ? "partially shipped" : "held"); 10.现在切换回工作流视图设计器来,我们将添加一些规则。首先,选中 policyActivity1,以 便 在属性面板中激活它。点击 RuleSetReference 属性,这将激活浏览 (...)按钮。 11.点击该浏览按钮,这将打开“选择规则集”对话框。一旦“选择规则集”对话框打 开后,点击新建按钮。 12.点击新建按钮将打开“规则集编辑器”对话框,然后点击添加规则。 13.你将添加三条规则中的第一条。你添 加的每个规则都由三个部分组成:条件、Then 操作和 Else 操作(最后一个是可选的)。在条件部分中输入 this.CheckPlasticizer()。 (注意它调用的是一个方法,因此括号是必须的。)在 Then 操作部分,输入 this.ProcessFullOrder()。最后在 Else 操作部分中输入 this.ProcessPartialOrder()。 14.再次点击添加规则,把 第 二 条规则添加到规则集中。在本条规则的条件部分中输入 this.CheckActualPlasticizer()。在 Then 操作部分输入 this.ProcessFullOrder()。在 Else 操作部分,输入 this.ProcessPartialOrder()。 15.再次点击添加规则以便插入第三条规则。在第三条规则的条件部分,输入 this._shipping[this._customer] == PlasticFlow.Workflow1.Shipping.Hold && this._shipQty != this.OrderQuantity。在 Then 操作部分,输入 this._shipPartial = False。在 Else 操作部分输入 this._shipPartial = True。 16.点击确定关闭“规则集编辑器”对话框。注意现在在规则列表中多了一个名称为规 则集 1 的规则。再点击确定关闭“选择规则集”对话框。 17.你的工作流现在就完成了,尽管它看起来有些古怪,因为整个工作流中就只有单一 的一个活动。其实,你 已 经 通过你提供的规则来通知你的工作流要做些什么。还有就是需要 在 PlasticPolicy 应用程序中添加对该工作流的项目级引用。 18.在 PlasticPolicy 项目中打开 Program.cs 文件,找到 Main 方法。在 Main 方法的 左大括号下面添加下面这些代码: // Parse the command line arguments string company = String.Empty; Int32 quantity = -1; try { // Try to parse the command line args GetArgs(ref company, ref quantity, args); } catch { // Just exit return; } 19.然后在 Main 方法中找到下面这行代码: // Print banner. Console.writeLine("Waiting for workflow completion."); 20.在上面的代码下添加如下这些代码: // Create the argument. Dictionary parms = new Dictionary(); parms.Add("Customer", company); parms.Add("OrderQuantity", quantity); // Create the workflow instance. WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(PlasticFlow.Workflow1), parms); // Start the workflow instance. instance.Start(); 21.在第 18 步,你添加的代码调用了一个对命令行参数进行处理的方法。现在你需要 添加该方法。在 Program.cs 源文件的底部添加这个方法: static void GetArgs(ref string company, ref Int32 quantity, string[] args) { // Pre-set quantity quantity = -1; try { // Parse the arguments we must have both a company // and a quantity. for (Int32 i = 0; i < args.Length; i++) { // Check this argument must have at least // two characters, "/c" or "/q" or even "/?". if (args[i].Length < 2) throw new Exception(); if (args[i].ToLower()[1] == 'c') { // Company The company name will be // located in character position 3 to // the end of the string. company = args[i].Substring(3); } // if else if (args[i].ToLower()[1] == 'q') { // Quantity The quantity will be // located in character position 3 to // the end of the string. Note Parse // will throw an exception if the user // didn't give us an integer. quantity = Int32.Parse(args[i].Substring(3)); } // else if else { // "/?" or unrecognized. throw new Exception(); } // else } // for // Make sure we have both a company and a // quantity value. if (String.IsNullOrEmpty(company) || quantity == -1) throw new Exception(); } // try catch { // Display usage Console.WriteLine("\nPlasticPolicy.exe -"); Console.WriteLine("\tTests Windows Workflow Foundation " + "rules-based processing\n"); Console.WriteLine("PlasticPolicy.exe /c: /q:\n"); Console.WriteLine("\t- Required Arguments -\n"); Console.WriteLine("/c:\n\tCompany placing order\n"); Console.WriteLine("/q:\n\tOrder quantity\n"); throw; } // catch } 22.编译该解决方案,修正任何出现的编译错误。 我们现在将使用这个示例应用程序来执行四个场景。第一个场景是规则引擎要处理的 最棘手的场景之一:Tailspin Toys 预订的量是 500 单位。这是一个很有用意的数字,因为 这样要消耗的增塑剂总计为 14,592.7(这是一个完全编造的数字),但是在储备罐中的增 塑剂的真实的量总共是 12879.2(我总共能凑足的数字!)。因为每生产一件成品需要消耗 27.4 个单位的增塑剂(这是我编造的另一个值,它通过 Workfow1 中的_plasticizerRatio 来表示),该订单正好处于这样一个范围:表面上该订单能全部满足,但实际上没有足够的 增塑剂。也就是说,我们认为我们的全部增塑剂能加工出 532 个成品(14,592.7 除以 27.4), 但是看到储备罐中真实的量后,我们只能加工出 470 个成品(12,879.2 除以 27.4)。最后, 我们只能进行部分出货。 甚至,假如你运行该应用程序时,提供的公司名称为“Tailspin Toys”,提供的数量 为“500”(对应的命令行内容为:PlasticPolicy.exe /c:"Tailspin Toys" /q:500),你 就可看到如图 12-2 所示的输出结果。此外,Tailspin 众所周知可接受部分出货,工作流也 指明了这一点。 备注:因为 PlasticPolicy 应用程序接受命令行参数,在 调 试 模 式 下你需要使用 Visual Studio 项目设置来提供这些参数然后运行该应用程序,或者打开一个命令提示符窗口,浏 览并定位到包含 PlasticPolicy.exe 的目录下,然后在命令提示符下执行该应用程序。 图 12-2 Tailspin Toys 的部分出货订单 但是假如 Tailspin 预订的数量是 200 个的话,该工作流还会正确地执行吗?我们就来 看看。再次在命令提示符下运行该程序:PlasticPolicy.exe /c:"Tailspin Toys" /q:200。 运行结果如下图 12-2 所示: 图 12-3 Tailspin 的完整出货订单 Tailspin 注册为能接受部分出货的类型。但是 Wingtip Toys 则希望订单继续有效, 直到整个订单都能满足时为止。该工作流也处理 Wingtip 吗?而且,假如 Wingtip 的订单处 在这样一个范围:我们认为我们有足够的增塑剂但实际上没有呢?为找出答案,试试这个命 令:PlasticPolicy.exe /c:"Wingtip Toys" /q:500。就像如图 12-4 中显示的,我们发现 我们只能部分完成 Wingtip 的订单。最重要的是,当我们访问需要优先进行处理的顾客的记 录时,选出被压下的 Wingtip 的订单是当务之急。 图 12-4 Wingtip Toys 的部分出货订单 为对最后一个场景进行测试,我们要能满足 Wingtip 的需求,而不用考虑增塑剂的真 实的量,因此在命令提示符下输入下面的命令:PlasticPolicy.exe /c:"Wingtip Toys" /q:200。Wingtip Toys 现在已经定了 200 个成品,事实上,图 12-5 也指明了我们能完整地 履行 Wingtip 的订单。 图 12-5 Wingtip Toys 的完整出货订单 基于规则方式的强大在于处理的处理方式。想像这个塑胶策略的例子,假设它被创建 成使用了几个嵌套的 IfElse 活动组成,或 许还要一个 ConditionedActivityGroup 活动,它 们通过使用工作流视图设计器以面向过程的方式创建。(当我们对储备罐中的增塑剂进行检 查时,ConditionedActivityGroup 活动也要对规则重新判定进行说明)。在这种情况下, 面向过程的模式不是一种好的工作方式,尤其对于要考虑使用许多嵌套的 IfElse 活动和优 先级来说更是如此。 但是,以规则为基础的方式使处理模式简化。许多嵌套的活动合而为一。而且,因为 规则是一种资源,你 能 很方 便 地 把它们抽取出来,然后用不同的规则对它们进行替换,这比 你通常地去部署一个新的程序集来说要更加容易。你 可 能会发现真实世界的工作流由过程化 的方式和基于规则的方式二者合并组成。正确的做法是保证你的工作流一定能工作的前提 下,根据实际情况的限制条件选择最合适的方式。 第十三章:打造自定义活动 学习完本章,你将掌握: 1.了解对于创建一个功能齐全的自定义工作流活动来说哪些组件是必须的 2.创建基本的自定义工作流活动 3.在基本的自定义工作流活动中应用验证规则 4.把基本的自定义工作流活动集成到 Microsoft Visual Studio 的工作流视图设计器和 工具箱中 WF 并不可能涵盖到你可能在你的工作流中想要实现的各个方方面面。即使 WF 对于开 发社区来说仍是非常新的技术,但目前已经可以获得许多免费发布的自定义活动,可以肯定 商业级的活动最终也会跟进。 在这章中,你将通过创建一个新的工作流活动来了解 WF 的个中奥妙,这个活动从远程 FTP 服务器中检索文件。你将看到在创建你自己的活动时哪些东西是必需的,以及其中哪些 部分挺不错。你也将更深入地了解活动是怎样和工作流运行时交互的。 备注:只在一章中对自定义活动开发的每一个细节进行探讨是不可能,这儿简化了太 多的细节。不 过好 消 息 是,对于得到一个完整功能的活动来说是容易的,这不用知道每一个 细节。 关于活动的更多知识 在第四章(活动及工作流类型介绍)中,我们初步了解了一下活动并讨论了像 ActivityExecutionContext 之类一些话题,ActivityExecutionContext 用来容纳一些和正 执行的活动相关的一些信息,工作流运行时需要不时对这些信息进行访问。我们这里将对 WF 活动进行更深入一些的了解。 活动的虚拟方法 在创建自定义活动时首先需要了解的是基类为你提供了哪些虚拟的方法和属性。表 13-1 显示了活动中被普遍使用的可重写的一些方法。(这里没有虚拟属性。) 表 13-1 Activity 中被普遍使用的可重写的虚拟方法 方法 功能 Cancel 在工作流被取消时被调用。 Compensate 这个方法实际上并不来自于 Activity 基类,它实际上需 要由 ICompensatableActivity 接口提供,许多活动都从 该接口派生。因此,不管出于什么目的和意图,都把它 当作 Activity 的方法。你将实现这个方法以便对失败的 事务进行补偿。 Execute 被用来执行活动要去完成的对应的工作。 HandleFault 在活动内部代码抛出一个未经处理的异常时被调用。注 意一旦该方法被调用将没有办法重启该活动。 Initialize 在活动被初始化时被调用。 OnActivityExecutionContextLoad 在活动完成了它的工作流程后被调用。当前执行上下文 (current execution context)正 在 转 移 到 另 一个活动。 Uninitialize 在活动要被反初始化时被调用。 在你的活动已经被加载到工作流运行时中但在执行之前的时候,假如你需要进行一些 特定的处理工作,一个极好的位置是在 Initializze 方法中做这些事情。你或许也会在 Uninitialize 方法中执行一些相似的处理工作之外的事情。 OnActivityExecutionContextLoad 和 OnActivityExecutionContextUnload 方法分别 表示活动正加载到工作流运行时中和活动正从工作流运行时中移走。在 OnActivityExecutionContextLoad 被调用之前以及 OnActivityExecutionContextUnload 被 调用之后,从 WF 的角度来看,该活动是处于卸载状态中。它或许是被序列化到一个队列中、 保存进一个数据库中或者甚至是在磁盘上等待被加载。但在这些方法 (OnActivityExecutionContextLoad 和 OnActivityExecutionContextUnload 方法)被调用 之前或之后它并不存在于工作流运行时之中。 Cancel、HandleFault 和 Compensate 都在显而易见的条件(指取消、失败和补偿条件) 激发的时候被调用。尽管 Compensate 真正用在执行你的事务补偿的地方(看看第 15 章:工 作流和事务),但它们主要的用途都是去执行一些你想去执行的额外的工作(例如日志)。 牢记这些方法被调用的时候都太晚了,因为到你的活动被要求对失败进行补偿的时候,你不 能对事务进行恢复;你也不能撤销一个未经处理的异常或者终止一个取消(cancle)的请求。 所有你能做的是去执行一些清理或者其它处理的请求,就 Compensate 来说,实际上是为失 败的事务提供补偿功能。 Execute 是最有可能被重写的 Activity 的虚拟方法,这 只 不 过 是 因 为这个方法需要你 重写以去执行活动应当要去执行的工作。 活动组件 尽管毫无疑问你需要亲自去写自定义活动代码,完 整 开 发 的 WF 活动都带有一些额外的 支持和工作流无关的行为的代码,但通常在工作流可视化设计器中都为开发者提供了更丰富 的开发体验。例如,你 可 能 想 要 提供一个验证器对象以便对不适当的活动配置进行检查并返 回错误信息;或者你可能需要提供一个 ToolboxItem 或者 ToolboxBitmap 以便更好地和 Visual Studio 工具箱集成。不管你是否相信,通过使用一个专门的设计器类来修改活动的 主题,你实际上能够调整你的活动放到工作流视图设计器中的呈现样式。在本章中的示例实 现了所有这些东西以对它们的功能和效果进行演示。 执行上下文(Execution Contexts) 你可能还记得,有两种类型的活动:基本(单一功能)活动和组合(容器)活动。你 可能会认为它们之间的主要区别是其中一个是单一的活动,而另一个能容纳可嵌入活动。这 毫 无 疑 问 是一个主要的区别。 但是还有其它重要的区别,尤其是活动在执行上下文(execution context)中怎样工 作这一点上。活动执行上下文在第 4 章中介绍过,它是 WF 去记载一些重要事情的一种简单 方法,就像是一个正在工作的活动来自于哪个工作流队列一样。但它也为活动控制提供了一 个机制,为 WF 在那些正执行的活动之间实施规则提供了一种手段。活动执行上下文的一个 有趣的地方是你的工作流实例启动的上下文可能并不是你的自定义活动中正被使用的上下 文。活动执行上下文能被克隆并传给子活动,对于迭代(iterative)类型的活动来说总会 发生这种情况。 但是对我们这里的目的而言,可能最重要的事情是要记住创建自定义活动的时候,至 少要记住活动执行上下文。活动执行上下文保存了当前的执行状态,并且当你重写了 System.Workflow.Activity 中的那些虚拟方法的时候,它 只 有 某 些 状态值是有效的。表 13-2 显示了哪些执行状态值能应用到 System.Workflow.Activity 中的方法的重写中。 Compensate 稍微有点例外,因为它不是 System.Workflow.Activity 的虚拟方法,它来自于 ICompensatableActivity,可它由活动实现,就返回状态值而言这条规则仍然适用于 Compensate。返回任何无效状态值(例如从 Execute 中返回 ActivityExecutionStatus.Faulting)其结果就是运行时抛出一个 InvalidOperationException。 表 13-2 有效的执行状态 可重写的方法 有效的返回执行状态 Cancel ActivityExecutionStatus.Canceling 和 ActivityExecutionStatus.Closed Compensate ActivityExecutionStatus.Compensating 和 ActivityExecutionStatus.Closed Execute ActivityExecutionStatus.Executing 和 ActivityExecutionStatus.Closed HandleFault ActivityExecutionStatus.Faulting 和 ActivityExecutionStatus.Closed Initialize ActivityExecutionStatus.Initialized。和其它状态值不一样,在此时 工作流活动被初始化,并没有任何东西去关闭它,因此 ActivityExecutionStatus.Closed 不是可选的。 通常,你要分别为这些虚拟方法的任务进行处理并返回 ActivityExecutionStatus.Closed。返回其它另外的有效值表明需要由工作流运行时或者一 个包含它的活动(指它的父活动)来采取更进一步的行动(操作)。例如,假如你的活动有 子活动,当你的主活动的 Execute 方法完成后还有子活动没有完成的话,主活动的 Execute 方法就应当返回 ActivityExecutionStatus.Executing。否则,它就应该返回 ActivityExecutionStatus.Closed。 活动生命周期 那么这些方法是在什么时候由工作流运行时执行呢?表 13-1 中的方法以下面的顺序 被执行: 1.OnActivityExecutionContextLoad 2.Initialize 3.Execute 4.Uninitialize 5.OnActivityExecutionContextUnload 6.Dispose 从工作流运行时的角度来看,OnActivityExecutionContextLoad 和 OnActivityExecutionContextUnload 界定了活动的生命周期。 OnActivityExecutionContextLoad 在一个活动刚刚被加载到运行时内存中的时候被调用, 而 OnActivityExecutionContextUnload 在一个活动从运行时中删除的前一刻被调用。 备注:活动通常从反序列化过程创建而不是由工作流运行时直接调用构造器创建。因 此,假如你需要在创建活动的时候为其分配资源的话,OnActivityContextLoad 是做这件事 情的最好位置,而不是在构造器中。 尽管从内存的角度来说 OnActivityExecutionContextLoad 和 OnActivityExecutionContextUnload 指示了活动的创建,但是 Initialize 和 Uninitialize 则表示活动在工作流运行时中执行的生命周期。当工作流运行时调用 Initialize 方法的时 候,你的活动就准备就绪了。当 Uninitialize 被执行的时候,从工作流运行时的角度来看 你的活动就已经完成了并准备从内存中移出。Dispose 这个.NET 对象的原型销毁方法对于释 放静态资源是很有用的。 当然,工作流并不能总是控制其中一些方法的执行。例如 Compensate,它仅在一个可 补偿的事务失败时才被调用。这 些 剩 下的方法实际上在 Execute 时会被不确定地调用(不一 定会被调用)。 创建一个 FTP 活动 为了对本章中目前为止我所描述的一些东西进行演示,我决定创建一个活动,我们当 中许多写行业处理软件的人都希望找到的一个有用的东西:FTP 活动。这个 FtpGetFileActivity 活动,使用.NET 中基于 Web 的 FTP 类来从远程 FTP 服务器中检索文件。 使用这些相同的类来把文件写到远程 FTP 资源中也是可行的,但我把这样的活动作为练习留 给你去创建。 备注:我将以你知道(并正确地配置过)FTP 站点的前提下开始我的工作。为了我们 此处的目的进行讨论,我将使用众所周知的 IP 地址 127.0.0.1 作为服务器的 IP 地址(当然, 这代表的是 localhost)。你也可自由地把这个 IP 地址替换为你喜欢的任何有效的服务器 IP 地址或者主机名。对于 FTP 安全的问题和服务器配置方面的内容超出了本章的范围,假 如你正使用的是 IIS 并需要了解关于 FTP 配置方面的更多信息的话,可看看 http://msdn.microsoft.com/en-us/library/6ws081sa.aspx。 为了宿主该 FTP 活动,我创建了一个名称为 FileGrabber 的示例应用程序(它的用户 界面如图 13-1 所示。)。有了它,你就能提供出一个 FTP 用户帐户和密码以及你想检索的 FTP 资源。我将下载的资源是一个 Saturn V 运载火箭移到发射位置的图像文件,我已经在 本书的 CD 中为你提供了该图片,你也可把它放到你的 FTP 服务器上。假设你的 FTP 服务器 在你的本机上,该图片的 URL 是 ftp://127.0.0.1/SaturnV.jpg。假如你不使用我的图片文 件,你就需要修改你的本地服务器上所能获取的某个文件的 URL 以和我所提供的地址匹配, 或者另外使用任何你能下载的文件的有效 URL。 图 13-1 FileGrabber 用户界面 和你可能已经知道的一样,不是所有的 FTP 站点都需要一个 FTP 用户账户和密码来进 行访问。有些允许匿名访问,它使用“anonymous”作为用户名,使用你的电子邮件地址作 为密码。该 FTP 活动也被这样配置,假如你不想提供它们,则用户名默认为 anonymous 而密 码默认为 someone@example.com。 因为本示例应用程序是一个 Windows Forms 应用程序,因此在工作流检索文件的时候 我们不想让应用程序看起来被锁定。毕竟工作流实例在不同的线程上执行,因此我们的用户 界面应能够继续响应。不过 ,我们将会禁用某些控制,同时允许其它的一些东西保持活跃状 态。一个状态控制将在文件传输正在发生的期间显示出来,一 旦 文件下载完成,该状态控制 将会被隐藏。假如用户在某个文件正在传输时试图退出该应用程序,我们将在取消该工作流 实例并退出应用程序之前对用户的决定进行确定。文件下载期间应用程序用户界面的情形如 图 13-2 所示。 图 13-2 FileGrabber 在下载某个文件时的用户界面 为了让你节约一些时间,该 FileGrabber 应用程序已经被写出了。唯一缺少的是一点 点配置工作流并让它启动的代码。但是,工作流将执行的这个 FTP 活动本身并不存在,我们 首先就来创建该 FTP 活动。随着本章的进展,我们将会( 逐 步)向该活动中添加更多的东西, 最后把它放到一个工作流中,FileGrabber 能执行该工作流去下载某个文件。 创建一个新的 FTP 工作流活动 1.该 FileGrabber 应用程序再次为你提供了两个版本:完整版本和非完整版本。你需 要下载本章源代码,打开 FileGrabber 文件夹中的解决方案。 2.FileGrabber 解决方案只包含有一个项目(它是一个 Windows Forms 应用程序)。 我们现在将添加第二个项目,我们将用它来创建我们的 FTP 活动。为此 ,向 我们的解决方案 中添加一个新项目,项目类型选择类库,项目名称为 FtpActivity,然后点击确定。 3.一旦该新的 FtpActivity 项目添加完成后,Visual Studio 会自动地打开它在本项 目中创建好的 Class1.cs 文件。首先做一些准备工作,把“Class1.cs”文件的名称重命名 为“FtpGetFileActivity.cs”,同时 Visual Studio 也会自动的把类的名称为你从 Class1 重命名为 FtpGetFileActivity。 4.确实,我们正创建的是一个 WF 活动,但是却没有添加相应的引用,我们不会离题太 远。当我们添加 WF 引用的时候,我们也将为我们本章将执行的任务添加其它的引用。因此 在解决方案资源管理器的 FtpActivity 项目上点击鼠标右键,然后选择添加引用。当打开“ 添 加 引 用”对话框后,从“.NET”选项卡列表中选中下面所有程序集,然后点击确定: a.System.Drawing b.System.Windows.Forms c.System.Workflow.Activities d.System.Workflow.ComponentModel e.System.Workflow.Runtime 5.现在我们就可以添加我们需要的名称空间了。添加下面的名称空间: using System.IO; using System.Net; using System.ComponentModel; using System.ComponentModel.Design; using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Compiler; using System.Workflow.ComponentModel.Design; using System.Workflow.Activities; using System.Drawing; 6.因为我们正创建的是一个活动,因此我们需要使 FtpGetFileActivity 派生自一个恰 当的基类。修改当前的类定义如下: public sealed class FtpGetFileActivity : System.Workflow.ComponentModel.Activity 备注:因为我们正创建的是一个基本活动,因此该 FTP 活动派生自 System.Workflow.ComponentModel.Activity。但是,假如你正创建的是一个组合活动的话, 它应当派生自 System.Workflow.ComponentModel.CompositeActivity。 7.对于本例子,FtpGetFileActivity 将暴露三个属性:FtpUrl、FtpUser 和 FtpPassword。活动的属性几乎总是依赖属性,因此我们将添加三个依赖属性,我们就从 FtpUrl 开始。在 FtpGetFileActivity 类的左大括号中输入下面的代码(此时该类没有包含 其它代码): public static DependencyProperty FtpUrlProperty = DependencyProperty.Register("FtpUrl", typeof(System.String), typeof(FtpGetFileActivity)); [Description ("Please provide the full URL for the file to download.")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] [ValidationOption(ValidationOption.Required)] [Browsable(true)] [Category("FTP Parameters")] public string FtpUrl { get { return ((string) (base.GetValue(FtpGetFileActivity.FtpUrlProperty))); } set { Uri tempUri = null; if (Uri.TryCreate(value, UriKind.Absolute, out tempUri)) { if (tempUri.Scheme == Uri.UriSchemeFtp) { base.SetValue(FtpGetFileActivity.FtpUrlProperty, tempUri.AbsoluteUri); } } else { // Not a valid FTP URI throw new ArgumentException("The value assigned to the" + " FtpUrl property is not a valid FTP URI."); }; } } 备注:完 整 地描述所有的设计器特性,并理解这些特性使 FtpGetFileActivity 在工作 流的视图设计器上怎样呈现出来方面的内容超出了本章的范围。不 过 ,话虽 如 此 ,我还是要 简要的描述一下。Description 特性提供了关于指定属性的相关说明,在该属性被选中的时 候将在 Visual Studio 的属性面板中显示出对应的这些相关说明。 DesignerSerializationVisibility 特性指定属性对设计时序列化程序所具有的可见性。 (在本例中,该属性将由代码生成器生成。)Browsable 特性告知 Visual Studio 把所修饰 的属性以编辑框的形式显示出来。Category 特性指明了所修饰的属性将呈现在哪种类别的 属性组中(本例中是自定义类别)。ValidationOption 特性是 WF 所特有的,它告知工作流 视图设计器它所修饰的属性的验证选项。(在本例中,FTP URL 是必须执行验证的。值必须 存在并将对其验证。)稍后当我们添加一个自定义活动验证器的时候我们将会需要这个特性。 http://msdn2.microsoft.com/en-us/library/a19191fh.aspx 为你提供了设计器特性和它 们的使用的一些概述信息以及相关更多信息的链接。 8.接下来为 FtpUser 属性添加代码。把下面的代码放到你前一步所插入的 FtpUrl 代码 的下面: public static DependencyProperty FtpUserProperty = DependencyProperty.Register("FtpUser", typeof(System.String), typeof(FtpGetFileActivity)); [Description("Please provide the FTP user account name.")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] [ValidationOption(ValidationOption.Optional)] [Browsable(true)] [Category("FTP Parameters")] public string FtpUser { get { return ((string)( base.GetValue(FtpGetFileActivity.FtpUserProperty))); } set { base.SetValue(FtpGetFileActivity.FtpUserProperty, value); } } 9.现在在你刚插入的 FtpUser 代码的下面放入最后的一个属性 FtpPassword: public static DependencyProperty FtpPasswordProperty = DependencyProperty.Register("FtpPassword", typeof(System.String), typeof(FtpGetFileActivity)); [Description("Please provide the FTP user account password.")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] [ValidationOption(ValidationOption.Optional)] [Browsable(true)] [Category("FTP Parameters")] public string FtpPassword { get { return ((string)( base.GetValue(FtpGetFileActivity.FtpPasswordProperty))); } set { base.SetValue(FtpGetFileActivity.FtpPasswordProperty, value); } } 10.正像你可能知道的,一 些 FTP 服务器允许匿名访问。虽然许多服务器都要求用户注 册,但也有其它的 FTP 站点被配置为公共的存取权限。在 公 共 存取权限的情况下,用 户 名通 常是 anonymous,并且用户的电子邮件地址被作为密码使用。我们将为 FtpGetFileActivity 指定一个 FTP URL 地址,但用户名和密码从应用程序的角度来看将是可选的。然而,从 FTP 的角度来看,我们必须提供一些东西。因此我们现在添加了一些常量字符串,以 便稍后我们 为 FTP 进行身份验证时添加代码的时候使用它。因此,在你刚刚添加的 FtpPassword 属性的 下面,添加下面这些常量字符串: private const string AnonymousUser = "anonymous"; private const string AnonymousPassword = "someone@example.com"; 11.根据你想让你的自定义活动去做的事情,你通常将重写基类 Activity 所暴露的一 个或多个虚拟方法。虽然严格意义上不是必须的,但你通常都可能想至少去对 Execute 进行 重写,因为在 Execute 中要完成的工作将得以实现。在你插入到 FtpGetFileActivity 源文 件的常量字符串的下面,添加这些重写 Execute 的代码: protected override ActivityExecutionStatus Execute( ActivityExecutionContext executionContext) { // Retrieve the file. GetFile(); // Work complete, so close. return ActivityExecutionStatus.Closed; } 12.Execute 调用了 GetFile 方法,因此在 Execute 的下面添加如下这些代码: private void GetFile() { // Create the Uri. We check the validity again // even though we checked it in the property // setter since binding may have taken place. // Binding shoots the new value directly to the // dependency property, skipping our local // getter/setter logic. Note that if the URL // is very malformed, the Uri constructor will // throw. Uri requestUri = new Uri(FtpUrl); if (requestUri.Scheme != Uri.UriSchemeFtp) { // Not a valid FTP URI throw new ArgumentException("The value assigned to the" + "FtpUrl property is not a valid FTP URI."); } // if string fileName = Path.GetFileName(requestUri.AbsolutePath); if (String.IsNullOrEmpty(fileName)) { // No file to retrieve. return; } // if Stream bitStream = null; FileStream fileStream = null; StreamReader reader = null; try { // Open the connection FtpWebRequest request = (FtpWebRequest)WebRequest.Create(requestUri); // Establish the authentication credentials if (!String.IsNullOrEmpty(FtpUser)) { request.Credentials = new NetworkCredential(FtpUser, FtpPassword); } // if else { request.Credentials = new NetworkCredential(AnonymousUser, !String.IsNullOrEmpty(FtpPassword) ? FtpPassword : AnonymousPassword); } // else // Make the request and retrieve response stream FtpWebResponse response = (FtpWebResponse)request.GetResponse(); bitStream = response.GetResponseStream(); // Create the local file fileStream = File.Create(fileName); // Read the stream, dumping bits into local file byte[] buffer = new byte[1024]; Int32 bytesRead = 0; while ((bytesRead = bitStream.Read(buffer, 0, buffer.Length)) > 0) { fileStream.Write(buffer, 0, bytesRead); } // while } // try finally { // Close the response stream if (reader != null) reader.Close(); else if (bitStream != null) bitStream.Close(); // Close the file if (fileStream != null) fileStream.Close(); } // finally } 备注:不可否认,假如我能找到能完成我所需要任务的现成代码而不是从零开始写的 话,我会每次都这样去做。(事实上,一位大学教授曾经告诉过我这是软件工程的一个重大 原则。)我重用的大部分代码都来自于 Microsoft 的示例。我提到这些是以防你想去创建这 个把文件发送到 FTP 服务器或者甚至可能去删除它们的活动。(对于这些操作的代码 Microsoft 的示例也已经提供了)你可以在 http://msdn.microsoft.com/en-us/library/system.net.ftpwebrequest.aspx 找到该例 子。 using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Net; using System.ComponentModel; using System.ComponentModel.Design; using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Compiler; using System.Workflow.ComponentModel.Design; using System.Workflow.Activities; using System.Drawing; namespace FtpActivity { [Designer(typeof(FtpGetFileActivityDesigner), typeof(IDesigner))] [ToolboxBitmap(typeof(FtpGetFileActivity), "FtpImage.bmp")] [ToolboxItem(typeof(FtpGetFileActivityToolboxItem))] [ActivityValidator(typeof(FtpGetFileActivityValidator))] public sealed class FtpGetFileActivity : System.Workflow.ComponentModel.A ctivity { public static DependencyProperty FtpUrlProperty = DependencyProperty. Register("FtpUrl", typeof(System.String), typeof(FtpGetFileActivity)); [Description("Please provide the full URL for the file to download.")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visi ble)] [ValidationOption(ValidationOption.Required)] [Browsable(true)] [Category("FTP Parameters")] public string FtpUrl { get { return ((string)(base.GetValue(FtpGetFileActivity.FtpUrlPrope rty))); } set { Uri tempUri = null; if (Uri.TryCreate(value, UriKind.Absolute, out tempUri)) { if (tempUri.Scheme == Uri.UriSchemeFtp) { base.SetValue(FtpGetFileActivity.FtpUrlProperty, temp Uri.AbsoluteUri); } } else { // Not a valid FTP URI throw new ArgumentException("The value assigned to the Ft pUrl property is not a valid FTP URI."); } } } public static DependencyProperty FtpUserProperty = DependencyProperty .Register("FtpUser", typeof(System.String), typeof(FtpGetFileActivity)); [Description("Please provide the FTP user account name.")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visi ble)] [ValidationOption(ValidationOption.Optional)] [Browsable(true)] [Category("FTP Parameters")] public string FtpUser { get { return ((string)(base.GetValue(FtpGetFileActivity.FtpUserProp erty))); } set { base.SetValue(FtpGetFileActivity.FtpUserProperty, value); } } public static DependencyProperty FtpPasswordProperty = DependencyProp erty.Register("FtpPassword", typeof(System.String), typeof(FtpGetFileActivity)); [Description("Please provide the FTP user account password.")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visi ble)] [ValidationOption(ValidationOption.Optional)] [Browsable(true)] [Category("FTP Parameters")] public string FtpPassword { get { return ((string)(base.GetValue(FtpGetFileActivity.FtpPassword Property))); } set { base.SetValue(FtpGetFileActivity.FtpPasswordProperty, value); } } private const string AnonymousUser = "anonymous"; private const string AnonymousPassword = "someone@example.com"; protected override ActivityExecutionStatus Execute( ActivityExecutionContext executionContext) { // Retrieve the file. GetFile(); // Work complete, so close. return ActivityExecutionStatus.Closed; } private void GetFile() { // Create the Uri. We check the validity again // even though we checked it in the property // setter since binding may have taken place. // Binding shoots the new value directly to the // dependency property, skipping our local // getter/setter logic. Note that if the URL // is very malformed, the Uri constructor will // throw. Uri requestUri = new Uri(FtpUrl); if (requestUri.Scheme != Uri.UriSchemeFtp) { // Not a valid FTP URI throw new ArgumentException("The value assigned to the FtpUrl property is not a valid FTP URI."); } // if string fileName = Path.GetFileName(requestUri.AbsolutePath); if (String.IsNullOrEmpty(fileName)) { // No file to retrieve. return; } // if Stream bitStream = null; FileStream fileStream = null; StreamReader reader = null; try { // Open the connection FtpWebRequest request = (FtpWebRequest)WebRequest.Create(requestUri); // Establish the authentication credentials if (!String.IsNullOrEmpty(FtpUser)) { request.Credentials = new NetworkCredential(FtpUser, FtpPassword); } // if else { request.Credentials = new NetworkCredential(AnonymousUser, !String.IsNullOrEmpty(FtpPassword) ? FtpPassword : AnonymousPassword); } // else // Make the request and retrieve response stream FtpWebResponse response = (FtpWebResponse)request.GetResponse(); bitStream = response.GetResponseStream(); // Create the local file fileStream = File.Create(fileName); // Read the stream, dumping bits into local file byte[] buffer = new byte[1024]; Int32 bytesRead = 0; while ((bytesRead = bitStream.Read(buffer, 0, buffer.Length)) > 0) { fileStream.Write(buffer, 0, bytesRead); } // while } // try finally { // Close the response stream if (reader != null) reader.Close(); else if (bitStream != null) bitStream.Close(); // Close the file if (fileStream != null) fileStream.Close(); } // finally } } } 其中接下来要做的一个更重要的事情是创建一个自定义验证器。尽管你可以使用该 FTP 活动了,因为它现在已经存在,但此时它是不完整的引入到工作流视图设计器中的。它 所缺少的是属性验证。我们就来看看怎样添加一个验证器。 创建一个自定义 ActivityValidator 现在,我确信你已经看到过小红色圆圈内包含一个感叹号的标记出现在那些在工作流 视图设计器中没有完成相应配置的活动中。 例如,在 Code 活动中假如没有为它设置 ExecuteCode 属性的话将显示这个指示标记。 原因是什么呢? 答案是活动验证器强迫这样做。验证器检查和它相关联的活动的属性并在需检查的任 何属性缺失和无效的时候就把错误添加进一个错误集合中。当设计器的状态发生改变(换句 话说,就是在添加了新活动或者属性发生改变的时候)以及工作流被编译的时候会要求验证 器重新对它适用的活动的属性进行判定。 验证器能选择是否对属性的配置不进行验证,它也能把它们标记为警告或者是不可接 受的错误。FTP 活动有三个属性,其中一个很关键(就是 URL)。其它两个可以不管,这将 产生默认(匿名)用户的身份验证。但我们实现我们的验证器时,我们将把缺少 URL 的情况 (或者在主工作流活动中缺少对 URL 属性的绑定)标记为一个错误。假如省略了用户名或密 码的话我们将产生警告信息来提示将使用匿名登录。 为 FtpGetFileActivity 工作流活动创建一个验证器 1.WF 中的活动验证器其实是一个类,因此我们要在 FtpActivity 项目中添加一个新类。 类的名称命名为“FtpGetFileActivityValidator.cs”。 2.在源文件中添加下面的名称空间: using System.Workflow.ComponentModel.Compiler; 3.当创建了 FtpGetFileActivityValidator 的新类创建后,它是一个 private 类型的 类。而且,WF 活动验证器必须使用 ActivityValidator 作为基类。因此在源文件中对该类 添加 public 关键字以及一个 ActivityValicator 基类来更改类的定义: public class FtpGetFileActivityValidator : ActivityValidator 4.为了实际去执行验证,你必须重写 Validate 方法。这 里 我们将对属性进行检查,假 如它们没有(设置)的话,你将把一个错误添加到设计器提供的错误集合中。下面是你需要 添加到 FtpGetFileActivityValidator 类中去的完整的 Validate 重写方法。 public override ValidationErrorCollection Validate(ValidationManager manager, object obj) { FtpGetFileActivity fget = obj as FtpGetFileActivity; if (null == fget) throw new InvalidOperationException(); ValidationErrorCollection errors = base.Validate(manager, obj); if (null != fget.Parent) { // Now actually validate the activity if (String.IsNullOrEmpty(fget.FtpUrl) && fget.GetBinding(FtpGetFileActivity.FtpUrlProperty) == null) { ValidationError err = new ValidationError("Note you must specify a URL " + "(including filename) for the FTP server.", 100, false); errors.Add(err); } // if Uri tempUri = null; if (Uri.TryCreate(fget.FtpUrl, UriKind.Absolute, out tempUri)) { if (tempUri.Scheme != Uri.UriSchemeFtp) { ValidationError err = new ValidationError("The FTP URL must be set to an" + " FTP endpoint.", 101, false); errors.Add(err); } // if } // if else if (!String.IsNullOrEmpty(fget.FtpUrl)) { ValidationError err = new ValidationError("The FTP URL must be a valid FTP URI.", 102, false); errors.Add(err); } // else if if (String.IsNullOrEmpty(fget.FtpUser) && fget.GetBinding(FtpGetFileActivity.FtpUserProperty) == null) { ValidationError err = new ValidationError("The 'anonymous' user account will " + "be used for logging into the FTP server.", 200, true); errors.Add(err); } // if if (String.IsNullOrEmpty(fget.FtpPassword) && fget.GetBinding(FtpGetFileActivity.FtpPasswordProperty) == null) { ValidationError err = new ValidationError("The default anonymous password " + "'someone@example.com' will be used for logging " + "into the FTP server.", 300, true); errors.Add(err); } // if } return errors; } 5.FtpGetFileActivityValidator 类现在就完成了,但我们实际上并没有通知 WF 去执 行验证。为此,回到 FtpGetFileActivity 类中,在该类定义的前面为该类添加下面的特性 标记: [ActivityValidator(typeof(FtpGetFileActivityValidator))] 6.生成 FtpActivity 项目,修正可能出现的任何错误。 完整的验证器代码如清单 13-2 所示。现在,当你拖拽该 FtpGetFileActivity 到你的 工作流中去的时候,假如你忘了指定该 URL 或者你没有为提供的 URL 创建绑定的话,工作流 不能编译。并且,假如你没有提供用户名或密码,或者你甚至没有在 Visual Studio 中使用 属性面板对它们进行绑定的话,你将收到警告信息。 清单 13-2 FtpGetFileActivityValidator.cs 的完整代码 using System; using System.Collections.Generic; using System.Text; using System.Workflow.ComponentModel.Compiler; namespace FtpActivity { public class FtpGetFileActivityValidator : ActivityValidator { public override ValidationErrorCollection Validate(ValidationManager manager, object obj) { FtpGetFileActivity fget = obj as FtpGetFileActivity; if (null == fget) throw new InvalidOperationException(); ValidationErrorCollection errors = base.Validate(manager, obj); if (null != fget.Parent) { // Now actually validate the activity if (String.IsNullOrEmpty(fget.FtpUrl) && fget.GetBinding(FtpGetFileActivity.FtpUrlProperty) == nul l) { ValidationError err = new ValidationError("Note you must specify a URL " + "(including filename) for the FTP server.", 100, false); errors.Add(err); } // if Uri tempUri = null; if (Uri.TryCreate(fget.FtpUrl, UriKind.Absolute, out tempUri)) { if (tempUri.Scheme != Uri.UriSchemeFtp) { ValidationError err = new ValidationError("The FTP URL must be set to a n FTP endpoint.", 101, false); errors.Add(err); } // if } // if else if (!String.IsNullOrEmpty(fget.FtpUrl)) { ValidationError err = new ValidationError("The FTP URL must be a valid FTP URI.", 102, false); errors.Add(err); } // else if if (String.IsNullOrEmpty(fget.FtpUser) && fget.GetBinding(FtpGetFileActivity.FtpUserProperty) == nu ll) { ValidationError err = new ValidationError("The 'anonymous' user account wil l " + "be used for logging into the FTP server.", 200, true); errors.Add(err); } // if if (String.IsNullOrEmpty(fget.FtpPassword) && fget.GetBinding(FtpGetFileActivity.FtpPasswordProperty) == null) { ValidationError err = new ValidationError("The default anonymous password " + "'someone@example.com' will be used for logging " + "into the FTP server.", 300, true); errors.Add(err); } // if } return errors; } } } 提供工具箱位图 我们下面将在我们的活动中做的事情是为它提供一个工具箱位图。这不是一个严格意 义上的 WF 任务。这种功能被集成到.NET 中,主要用于为 Visual Studio 设计器提供支持。 它也并不难做到。 为 FtpGetFileActivity 工作流活动指定一个工具箱位图 1.下载本章的示例代码,你将找到一个名称为 FtpImage 的位图文件。把 FtpImage 文 件从 Windows Explorer 窗口中拖拽到 FtpActivity 项目的树形控制节点下面,这会把该文 件复制并添加到你的项目目录中。 2.然后,你必须把该位图作为资源编译进你的程序集中。在解决方案资源管理器的 FtpActivity 项目中选中 FtpImage 文件以激活它的属性。更改“生成操作”属性,把它从 “编译”改为“嵌入的资源”。 3.和验证器一样,只是把一个位图编译进你活动的程序集中是不够的。你也必须通知 Visual Studio 该活动有一个相关的工具箱位图。和 先 前 一样,你使用一个特性来通知 Visual Studio 这件事。把下面的特性添加到 FtpGetFileActivity 类的定义中(就像你在前面一节 添加 ActivityValidator 一样): [ToolboxBitmap(typeof(FtpGetFileActivity), "FtpImage.bmp")] 备注:ToolboxBitmapAttribute 不是 WF 所特有的。它可以用到任何的控件。看看 http://msdn2.microsoft.com/en-us/library/4wk1wc0a(VS.80).aspx 可获得更多信息。 4.生成 FtpActivity 项目。不应出现任何错误,假如有的话,修正它们。 假如你此刻创建一个顺序工作流并把这个活动拖拽到该工作流中去的话,这个活动会 以相当普通的外观呈现出来。默认呈现出的外观是一个以黑色圆角作为边框并用白色填充的 矩形。想做得更好吗?看看下面怎么做。 修改活动在工作流视图设计器中的外观 工作流视图设计器其实基于通用的 Visual Studio 设计器。自.NET 1.0 以来,.NET Framework 中就有组件帮助你把你的自定义对象集成到通用功能的设计器中。这 些组件中的 一个就是 Designer 特性,它嵌入代码中被执行,视图设计器使用它去控制如对象的展示和 外观之类的事情。 WF 通过提供一种机制延伸了这一概念,它 通过一个 theme 来为可视化活动的展现提供 支持。主题(theme)实际上只不过是一个设计器类,它包含许多的属性,你能设置它们以 便去控制你的活动被怎样绘制。你能控制呈现的颜色、边框线的风格及颜色等等。 你也能控制它在视图设计器中的行为。例如,你能把一些东西添加到使用鼠标右键点 击活动时所弹出的快捷菜单中。主题和行为操作两者都要求你去写一个派生于 ActivityDesigner 或 CompositeActivityDesigner(针对组合活动)的类。对于我们的例子, 我们将创建一个专门的命名为 FtpGetFileActivityDesinger 的设计器类。 添加一个可视化设计器到 FtpGetFileActivity 工作流活动中 1.这里你将以和前一节同样的方式开始我们的工作:创建一个新类。为此,向 FtpActivity 项目中添加一个名称为 FtpGetFileActivityDesigner.cs 的类文件。 2.向该源文件中插入下面的名称空间,把它们放到已存在的名称空间语句的下面: using System.ComponentModel; using System.ComponentModel.Design; using System.Drawing; using System.Drawing.Drawing2D; using System.Workflow.Activities; using System.Workflow.ComponentModel.Design; 3.因为你正创建的设计器类派生自 ActivityDesigner,因此你需要修改源文件中的类 定义。使用下面的类定义来替换 Visual Studio 为你自动生成的类定义: public class FtpGetFileActivityDesigner : ActivityDesigner 备注:再重复一次,因为这是一个基本活动,因此你正创建的设计器类派生自 ActivityDesigner。但是,假如这个活动是一个组合活动的话,你应该使用 CompositeActivityDesigner 类来作为基类。 4.ActivityDesigner 提供了几个虚拟的属性和方法,你 能 重写它们以便把行为外观添 加到视图设计器中。例如 Verbs 属性可以让你添加上下文选择菜单。做这些相当地简单,从 行为外观的角度来看 FTP 活动不需要特别的支持,但它在调节视觉方面是很不错的。为做这 些,首先在 FtpGetFileActivityDesigner 类定义的前面添加下面的特性标记: [ActivityDesignerThemeAttribute(typeof(FtpGetFileActivityDesignerThem e))] 5.你刚刚添加的特性指定了一个包含绘制属性任务的设计器主题类,我们现在就来创 建这个类。寻找 FtpGetFileActivityDesigner 类的结束(右边)大括号,在该大括号的下 面添加如下这个内部(internal)类: internal sealed class FtpGetFileActivityDesignerTheme : ActivityDesignerTheme { public FtpGetFileActivityDesignerTheme(WorkflowTheme theme) : base(theme) { this.BorderColor = Color.Black; this.BorderStyle = DashStyle.Solid; this.BackColorStart = Color.Silver; this.BackColorEnd = Color.LightBlue; this.BackgroundStyle = LinearGradientMode.Horizontal; } } 备注:组合活动也有它们自己的设计器主题类:CompositeDesignerTheme。那是因为 组合活动需要去呈现子活动,并且你可能想在视觉外观上进行更严格的控制。 6.在有了验证器和工具箱位图后,你 需 要 为 FtpGetFileActivity 类添加一个特性来通 知工作流视图设计器你有为展示你的活动所需的基于 ActivityDesigner 的信息。 [Designer(typeof(FtpGetFileActivityDesigner), typeof(IDesigner))] 7.编译该项目,修正任何出现的编译错误。 FtpGetFileActivityDesigner 的完整文件如清单 13-3 所示。假如我们需要的话,我 们能在设计器类上做更多的工作,但在这个例子中,该设计器类的存在仅仅只是添加主题。 该活动将在工作流视图设计器中以银色到白蓝色的颜色水平渐变并以实心边框线的风格呈 现出来。 清单 13-3 FtpGetFileActivityDesigner.cs 的完整源代码 还剩下一个细节:当 它加载进工具箱后,FtpGetFileActivity 的名称和图标都将会展示出来。 using System; using System.Collections.Generic; using System.Text; using System.ComponentModel; using System.ComponentModel.Design; using System.Drawing; using System.Drawing.Drawing2D; using System.Workflow.Activities; using System.Workflow.ComponentModel.Design; namespace FtpActivity { [ActivityDesignerThemeAttribute(typeof(FtpGetFileActivityDesignerTheme))] public class FtpGetFileActivityDesigner : ActivityDesigner { } internal sealed class FtpGetFileActivityDesignerTheme : ActivityDesignerT heme { public FtpGetFileActivityDesignerTheme(WorkflowTheme theme) : base(theme) { this.BorderColor = Color.Black; this.BorderStyle = DashStyle.Solid; this.BackColorStart = Color.Silver; this.BackColorEnd = Color.LightBlue; this.BackgroundStyle = LinearGradientMode.Horizontal; } } } 把自定义活动集成到工具箱中 如你所知,当你的活动被装到 Visual Studio 工具箱中后,ToolboxBitmapAttribute 会显示一个和你的活动关联的图标。但碰巧的是,你能做比刚刚显示一个位图更多的事。 例如组合活动,通常要为其正常运行创建所必须的子活动。一个极好的例子是 IfElse 活动。当你拖拽一个 IfElse 活动到你的工作流中的时候,它 会 自动填充一个左右分支活动。 在这里我不会显示怎样做这些,因为我们正创建的是一个基本活动。但在这节的末尾我将提 供一个获取更多信息的链接以及创建组合活动并预先用子活动来填充它们的示例代码。 因此,如果我们不添加子活动的话,我们还需要完成些什么事才能把我们的活动集成 到工具箱中呢?对这件事来说,如没有其它的指示,Visual Studio 将把你的活动加载进工 具箱中并使用类的名字作为它显示的名字。因为有其它的 WF 活动没有使用它们的类名来作 为显示名,因此我们将对默认的行为进行重写并提供一个更像是真正的标准 WF 元素的显示 名(不使用类名来作为显示名)。尽管我们所有要调整的事情就是这些,但你也能修改像包 含一个描述信息、你的公司名称以及一个版本号在内的其它事情。 你也能提供过滤,以便让你的活动只在基于工作流中使用时呈现,但你很快将使用的 ActivityToolboxItem 基类为你提供了这种行为。 为 FtpGetFileActivity 工作流活动添加工具箱集成 1.和前面两节一样,你要创建一个新类,在 FtpActivity 项目中添加一个名称为 FtpGetFileActivityToolboxItem.cs 的类文件。 2.添加下面的名称空间,把它们放到现存的名称空间的下面: using System.Workflow.ComponentModel.Design; using System.Runtime.Serialization; 3.你正创建的类必须从 ActivityToolboxItem 派生。因此,你 需 要修改 Visual Studio 为你创建的默认的类定义。用下面的内容替换该类的类定义。 class FtpGetFileActivityToolboxItem : ActivityToolboxItem 4.FtpGetFileActivityToolboxItem 类必须被标记为可序列化的,因此在刚才类定义 的前面添加 Serializable 特性。 [Serializable] 5.现在添加该类的主要部分。你需要三个构造器:一个默认的构造器,一个带参数的 构造器和一个序列化构造器。每一个构造器都将调用 InitializeComponent 来指定它的显示 名称。 public FtpGetFileActivityToolboxItem() { // Initialize InitializeComponent(); } public FtpGetFileActivityToolboxItem(Type type) : base(type) { // Initialize InitializeComponent(); } private FtpGetFileActivityToolboxItem(SerializationInfo info, StreamingContext context) { // Call base method to deserialize. Deserialize(info, context); // Initialize InitializeComponent(); } protected void InitializeComponent() { // Assign the display name this.DisplayName = "FTP File Get"; } 备注:有一个虚拟方法 Initialize,你可以重写它去指定要显示的名称。但是这个方 法并不总会被调用。因此提供我们自己的 InitializeComponent 方法是确保指定的显示名称 在所有情况下都有效的最好方式。 6.为确保你刚刚创建的 ToolboxItem 被 FTP 活动使用,需要把下面的特性添加到你已 经为 FtpGetFileActivity 添加的一组特性的下面。 [ToolboxItem(typeof(FtpGetFileActivityToolboxItem))] 7.编译 FtpActivity 项目,修正任何可能出现的编译错误。 随着这最后的一个步骤,你的自定义活动就完成了。但是,FileGrabber 应用程序是 不完整的。你需要添加一个使用 FtpGetFileActivity 的工作流,并为 FileGrabber 应用程 序添加必须的代码以便调用该工作流。我们首先创建该工作流。 添加一个工作流并使用 FtpGetFileActivity 工作流活动 1.右键点击 FileGrabber 解决方案,然后选择“添加”。从 子 菜 单 中 选 中“ 新 建项目”。 2.新建的项目类型选择“顺序工作流库”,名称命名为“GrabberFlow”。 3.Visual Studio 添加了一个新的顺序工作流库后会打开工作流视图设计器,让你可 直接开始编辑你的工作流。打开 Visual Studio 工具箱后,你应该在那里能找到 FtpGetFileActivity。 备注:你或许会惊讶,漂亮的小 FTP 位图到哪里去了(取而代之的是一个蓝色齿轮图 标),以 及 你在 FtpGetFileActivityToolboxItem 类中添加的显示文本为什么没有在工具箱 中显示出来。这是因为 FtpGetFileActivity 由当前解决方案中的一个程序集支持。我将在 你完成了该工作流后来描述解决这些问题的办法。 4.拖拽一个 FtpGetFileActivity 到你的设计器界面上。 5.带感叹号标记的红点指明了当前存在一些验证错误。并且事实上,假如你把鼠标放 到向下的箭头上并单击的话,你将看到详细的验证失败信息。它看起来眼熟吗?哦...它是 你在前面章节创建活动验证类时所插入的相关验证错误信息。 6.FileGrabber 主应用程序要能够把用户名、密码和文件的 URL 传进你的工作流中。 因此你需要在你的工作流中为所有这些值都提供一个属性。这 里 有一个很棒的方式来完成这 些任务:让 Visual Studio 为你添加它们。假如你选中 FTP 活动然后在属性面板中看看它的 属性的话,你将看到你为活动添加好的三个属性:FtpUrl、FtpUser 和 FtpPassword。为了 让你首先清除错误的条件,你需要选中 FtpUrl 属性以激活浏览(...)按钮。点击该浏览按 钮。 7.这将激活“将‘FtpUrl’绑定到活动的属性”对话框。点击“绑定到新成员”选项 卡,在“新成员名称”中输入“FtpUrl”,确保选中的是“创建属性”。最后点击“确定”。 (注意现在带感叹号标记的红点消失了。) 8.按照相同的步骤(步骤 6 和步骤 7)添加一个新的 FtpUser 属性和一个新的 FtpPassword 属性。当你完成这些后,属性面板显示的为所有这三个属性的绑定效果如下图 所示: 9.编译该工作流项目,假如存在错误的话,请纠正这些错误。 我在第三步中提到过,我会描述怎样把 FtpGetFileActivity 加载进工具箱中去显示出 你在前面章节中添加的实际已存在的元数据。下面就是你要做的。 把 FtpGetFileActivity 工作流活动加载进工具箱中 1.对这一工作来说,你必须在工作流视图设计器中有一个工作流。(工具箱会过滤掉 不适合的组件。)GrabberFlow 工作流应该被加载进工作流视图设计器中了,如没有的话, 重新加载它并打开工具箱。在工具箱内容体(不是标题)上点击鼠标右键,这将弹出上下文 菜单,然后选择“选择项”。 2.这将打开“选择工具箱项”对话框。单击“活动”选项卡然后点击“浏览”按钮。 3.点击“浏览”将打开一个常见的文件对话框。使用导航工具,浏览并定位到你本地 文件系统中已编译的 FtpActivity 项目对应的目录,通常为 “\Chapter13\FileGrabber\FtpActivity\bin\Debug\”(或者为“Release\”,这取决于你 所选择的生成模式)。选中 FtpActivity.dll 文件,点击“打开”。然后点击“确定”关闭 “选择工具箱项”对话框。 4.这就把 FtpGetFileActivity 加载进工具箱中了,并且你应该会看到你在前面所添加 的自定义图标和显示文本。 我们最后的任务是去添加在主应用程序中开始该工作流所需的代码。 执行 FtpGetFileActivity 工作流 1.在代码视图模式下打开 Form1.cs 文件。 2.除了真正启动工作流实例所需的代码外,你 需 要 的 所 有代码我都已经为你添加好了。 该代码将放进 Get_Click 事件处理程序中。因此找到 Get_Click 事件处理程序,在 末 尾 添 加 如下代码: // Process the request, starting by creating the parameters Dictionary parms = new Dictionary(); parms.Add("FtpUrl", tbFtpUrl.Text); parms.Add("FtpUser", tbUsername.Text); parms.Add("FtpPassword", tbPassword.Text); // Create instance. _workflowInstance = _workflowRuntime.CreateWorkflow(typeof(GrabberFlow.Workflow 1), parms); // Start instance. _workflowInstance.Start(); 3.因为你正使用的工作流来自于 GrabberFlow 名称空间,因此你需要添加对该工作流 程序集的项目级引用。 4.按下 F5(或者 Ctrl+F5)执行 FileGrabber。假如你提供了一个有效的 FTP URL 文 件地址,该文件会被下载吗?(注意该文件将被下载并放到和你的应用程序的可执行文件相 同目录的位置中。) 在这一章,我们创建了一个基本活动。这些必需的步骤对于创建一个组合活动来说也 是相似的,只是稍微多了一些调用。(例如,你创建的 ToolBoxItem 就多些额外的代码去方 便对所容纳的活动进行添加。)假如你想阅读关于组合活动创建的更多资料的话,你 可以在 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnlong/html/pa rallelif.asp 中找到。 第十四章:基于状态的工作流 学习完本章,你将掌握: 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 步)并再次运行该应用程序,它应该能干净利落地运行。 第十五章:工作流和事务 学习完本章,你将掌握: 1.了解传统的事务模型以及这种模型在哪些地方适合去使用,哪些地方不适合使用 2.懂得在哪些地方不适合传统的事务以及什么时候是补偿事务的恰当时机 3.看看怎样回滚或补偿事务 4.看看怎样修改默认的补偿顺序 如果你是写软件的,你迟早需要去理解事务处理。事务处理(transactional processing)在这个意义上是指写那些把信息记录到一个持久化资源的软件,这 些 持 久化资 源如数据库、Microsoft 消息队列( 它在底层使用了一个数据库)、带 事 务 文件系统的 Windows Vista 以及注册表存取或者甚至是其它一些支持事务处理的软件系统。持 久化资源不管发生 什么事情,一旦数据被记录下来就会保留这些写入的信息。 事务对于任何业务流程都是关键的,因为通过事务,你能确保你的应用程序中数据的 一致性。假如业务流程维持一个错误但仍要持久化一些数据,那么这些错误的数据很有可能 会波及到整个系统,这就留给你一个问题:哪些数据是正确的,哪些数据是错误的。试想从 一个在线商家订购这本书,可是却发现商家和你的信用卡交易“出了点小意外”,收取了你 书的标价的 100 倍而不是他们的折扣价。当像这样的错误可能发生时,事 务处理就不再是一 个可笑的或者避而不谈的话题。 理解事务 事务处理,其核心就是管理你的应用程序状态。对于状态,我实际指的是应用程序的 所有数据的状况。当应用程序的所有数据是一致的,那么该应用程序就处于一个确定状态。 假如你把一条新的客户记录添加到你的数据库中,则 此 过 程 需 要两个插入(一是新增一条通 常的包含有把地址和你的客户联系在一起的信息的行记录,另一条是记录真实地址信息), 添加通常的行记录成功后,但添加它的地址时却失败了,这 就 使你的应用程序处于一个不确 定状态。当某人试图检索该地址将发生什么呢?系统会提示到地址应该在此,但真实的地址 记录已丢失。你的应用程序数据现在是不一致的。 为确保这两个操作成功,事务起到了作用。一个事务本身是一个单一的工作单元,它 要么全部成功要么全部失败。这 并 不是说你不能更新两个不同的数据库表。它 刚刚的意思是 两个表的更新被看作是一个单一的工作单元,两个都必须被更新否则都失败。假如其中一个 或者两个更新失败,理想情况下是你想让系统回到刚刚你试图更新这些表之前的状态。没有 迹象表明试图更新这些表的操作是非成功的话,你的应用程序就应该继续前进,但更重要的 是,你不想有来自更新不成功的一个表里有而另一个表里却没有的数据。 备注:有整卷书全部写和事务及事务处理相关知识的书籍。尽管我将描述的解释 Microsoft Windows Workflow Foundation(WF)是怎么支持事务的相关概念足够深,但我 不可能在本书中以相当深的深度覆盖事务处理的方方面面。假如你还没有重新看看.NET 2.0 中通常的事务支持的话,你应该这样去做。WF 的事务处理模式和.NET 2.0 的事务支持非常 紧密,你可以在下面的论文中找到有用的知识去理解 WF 的事务支持: msdn2.microsoft.com/en-us/library/ms973865.aspx。 传统上,事务来自于一个单一的模式:XA 或两阶段提交(two-phase commit)类型的 事务。但是,随着基于 Internet 通信的出现以及需要提交长时间运行的事务的要求,引进 了新一些的称作补偿式事务的事务类型。WF 支持这两种类型。我们将首先讨论传统的事务, 然后我们将提到使用这种类型的事务是一个低级的架构选择,再后我们将讨论补偿式事务。 传统(XA)事务 已知的第一个实现了事务处理的系统是一个航空公司的预订系统。对于需要预订多个 航班的请求,假如任何一个单独的航班不能预订,则该 预 订 就 不能 进行。系统设计师知道这 些并设计了一个我们今天称为 X/Open 分布式事务处理模型,简称 XA 的事务化方式来进行处 理。(看看 en.wikipedia.org/wiki/X/Open_XA。) XA 事务涉及到 XA 协议,即我先前提到的两阶段提交和三个实体:应用程序、资源和 事务管理器。应用程序也就是指你的应用程序。资源是一个设计的用来加入到 XA 类型的事 务中去的软件系统,也就是说它参与事务并懂得怎样参与两阶段提交数据以及提供持久性 (很快将进行讨论)。事务管理器监视整个事务处理流程。 因此什么是两阶段提交呢?最后,想像你的应用程序需要写入数据,也就是说一个数 据库。假如写入过程在事务下执行,数据库就保持这些要被写入的数据直到事务管理器发出 一条准备(prepare)指令为止。在那时,数据库以一个表决(vote)作为响应。假如该表 决是要前进并把数据提交(写入)到表中,那么事务管理器则进入并参与资源,假如有的话。 假如所有资源对提交数据都表决成功,则事务管理器发出一个提交(commit)指令然 后每一个资源就把数据写到它内部的数据存储中。只有到那时指定要写入你的表中的数据才 真正地插入到数据库中。 假如任何一个资源有问题并且没有对提交数据进行表决,则事务管理器发出一个回滚 (rollback)指令。所有参与进事务的资源就必须销毁和事务相关的信息,没有东西被永久 的记录保存下来。 一旦数据被提交,XA 协议会保证事务结果的持久性。数据是一致的,并且应用程序也 处在一个确定状态中。 ACID 属性 当我们谈到 XA 事务时是不可能不提到 ACID 的:原子性、一致性、隔离性和持久性 (en.wikipedia.org/wiki/ACID)。 对于原子性(atomic),我们指的是资源参与了两阶段提交协议的事务支持中。要被 处理的数据要么全部被处理(更新、删除或者其它任何操作)要么都不处理。假如事务失败, 资源就返回到刚刚要试图处理该数据之前的状态。 一致性(Consistency)指的是数据保持完整性。对数据库而言,典型的意思是指数据 不应违反任何一致性约束,但对于其它资源而言,保持完整性可能有所区别或者有额外的含 义。假如数据违反了任何规则或者一致性约束,它 最 后 的 结 果 会 是 应 用程序处于一个不确定 状态中,资源必须回滚事务以防止不一致的数据被持久记录进系统中。 隔离性( isolation)是指在事务进行中导致系统不能存取数据的一个事务属性。在 数 据 库 中,试 图 写 入一个之前被锁住的行或者读取一个未提交数据的行是不允许的。仅当数据 被提交后才能使用这些数据,或者是在你明确允许未提交读时进行读操作的情况( 通 常 称 作 “ 脏 数据”)。 持久性( durable)资源保证数据被提交后将总是能以非易失性的方式获取它。如果数 据库服务器的电源在数据被提交后的一毫秒被切断,在 数据库服务器重新上线后数据应还在 数据库中为你的应用程序的使用随时做好准备。在实际中做到这些比听起来还更加困难,一 个 主要的原因是架构师使用数据库来为关键的数据进行持久化的数据存储而不是像 XML 之 类的单一数据文件。(不可否认,Windows Vista 中的事务文件系统有了一些改观,但希望 你能领会我的观点。) 长时间运行的处理过程和应用程序状态 记住,XA 类型的事务的整个前提是假如事务回滚的话,你的应用程序将回到它的初始 状态。但是考虑一下这个情况:假如事务花费了过长的时间才提交的话你的应用程序会发生 什么呢? 在我回答之前,假想你的在线交易系统收到了一个客户的订单,但是信用卡验证的过 程中被中断了。无疑你的处理过程在一个事务中进行,因为假如某些地方失败的话你就不想 再去收取该客户的款项。但是与此同时,其它客户又正在发来订单,假如你运气不错的话, 这是大量的订单。假如第一个客户的交易失败后,订单在此期间发来将发生什么呢? 假如系统的目的不是为了隔离单独的一个订单处理失败,那么正确的做法是把系统完 全回滚到它的原始状态。但是考虑这种情况则意味着,我们不仅会丢失第一个客户的订单, 也会丢失在此期间其它每一个客户发来的订单。即使这可能只有两个订单,但也不是很好。 假如这是 10,000 份订单呢...损失的收入数额是不能容忍的。 当然,我们将保留这 10,000 份订单并把刚刚处理的第一份订单放到一个孤立的事件 中,但是在这种情况下我们同时也有可能会有意破坏事务属性中的某一个来保留这些订单收 入。这是一个风险,需要进行估量,但通常在现实世界情况下我们必须接受这一风险。 被破坏的属性其实是原子性,出于这个原因,写事务处理系统的人会努力在尽可能短 的时间内持有事务。你做的仅仅只能是你事务范围内所必须要做的事,你 要 尽 可 能 高 效地做 这些事以便事务快速地完成。 现在我们进入另外一种复杂情况:Internet。你的客户正在线发送订单,网络速度是 出了名的慢甚至中断。因此,在 Internet 之上的事务处理是有疑虑的。 补偿作为一个解决办法 这正是创建所需要的补偿事务的情况。假如我给你五个苹果,过程使用 XA-类型的事 务,事务失败时,时间会回滚到我开始给你苹果的那一刻。在某种意义上说,历史会被重写 以致这五个苹果没有被首先考虑到。但是假如我是在一个补偿式事务中给你的五个苹果的 话,事务失败进行补偿(以便我们维护一个确定的应用程序状态)时,你必须返回五个苹果 给我。看起来差别很小,但两种类型的事务之间存在明显的区别。 当写 XA 类型的事务时,负责回滚失败事务的职责落在资源上,例如你的数据库。相反, 当一个补偿式事务失败时,作为事务参与者,你有责任通过提供一个事务补偿功能来为你的 事务部分提供补偿。假如你扣除了某个在线客户信用卡中的金额并在后来被告知要补偿,你 会 立 即 向该客户账户存入你起初扣除的相同数目的金额。在一个 XA 类型的事务中,该账户 绝不会在一开始就被扣除。对于补偿式事务来说,你发起两个操作:一个是扣除账户,另一 个是后来存入它。 备注:毫无疑问,它将是一个极好的系统,它能在 Internet 上成功执行 XA 类型的事 务。但是要非常仔细地构思你的补偿功能,注意各个细节。如果你不这样做的话,你可能由 于一错再错而使情况更加糟糕。写出准确的补偿功能通常并不容易。 向你的工作流中引入事务 总的来说,在 WF 中引入事务和拖拽一个基于事务的活动到你的工作流中一样简单。但 是,如果你正使用事务活动话,你应该知道更多一些的东西。 工作流运行时和事务服务 当你在你的工作流中使用基于事务的活动时,需要两个可插拔的工作流服务。首先, 因为基于事务的 WF 活动需要标注 PersistOnClose 特性( 在 第 6 章“加载和卸载实例”中 提 过 ),因 此 你也 必须启动 SqlWorkflowPersistenceService。假如你没有的话,WF 不会崩溃, 但是你的事务也不会提交。 或许在本章更感兴趣的是 DefaultWorkflowTransactionService。 这个服务为你事务 操作的启动和提交负责。没有这样一个服务的话,工作流运行时内的事务是不可能实现的。 备注:你 可以 创建你自己的事务服务,尽管这超出了本章的范围。所有 WF 事务服务都 派生自 WorkflowTransactionService,因此创建你自己的服务基本上就是重写(override) 你想改变的基类功能。实际上,WF 用了一个自定义的事务服务: SharedConnectionWorkflowTransactionService 来承载共用的 Microsoft SQL Server 连接。 在 msdn2.microsoft.com/en-us/library/ms734716.aspx 可找到更详细的信息。 失败处理 对于事务失败,尽管在你的工作流中并不需要你去处理失败过程,但这是个好习惯。 我不想提它的原因是它被认为是一个最佳做法。我提到它的原因是你可能去实现你自己的, 在真正失败之前能自动检查异常并重新尝试事务处理的事务服务。尽管对这些要怎么做进行 演示超出了本章的范围,但你应该知道这是可以做到的。 环境事务(ambient transaction) 基于事务的活动都在被称为环境事务的东西下工作。当你的工作流进入一个事务范围 内的时候,工作流事务服务会自动为你创建一个事务。这不需要去尝试并建立你自己的事务。 所有嵌入到事务范围中的活动都属于这一个环境事务,假如事务成功它们就都提交,否则就 回滚(或者补偿)。 使用 TransactionScope 活动 在 WF 中 XA 类型的事务由 TransactionScope 活动实现。这个活动和.NET 的 System.Transactions 名称空间的联系密切,事实上当活动开始执行的时,它会创建一个 Transaction 来作为环境事务。该 TransactionScope 活动甚至与 System.Transactions 共 享数据结构(TransactionOptions)。 使用基于组合活动的 TransactionScope 确实和把它拖到你的工作流中一样容易。你 放 进 TransactionScope 活动中的任何活动都自动继承该环境事务并像通常的使用.NET 自己的 System.Transactions 事务时一样进行操作。 备注:你不能在其它事务活动中放入 TransactionScope 活动。事务嵌套是不允许的。 (这条规则也适用于 CompensatableTransactionScope。) 事务选项更精确地指定了环境事务将怎样进行操作。由 System.Transactions.TransactionOptions 结构支持的这些选项让你能去设置环境事务将 支持的隔离级别和延时时间。延时时间值可不需加以说明,但是隔离级别则不。 备注:超时值可以进行限制,它是可配置的。有一个机器范围(machine-wide)的设 置 System.Transactions.Configuration.MachineSettingsSection.MaxTimeout,和一个局 部范围的设置 System.Transactions.Confituration.DefaultSettingsSection.Timeout。 它们能为超时值设置一个最大上限值。这些值会覆盖你使用 TransactionOptions 所做的任 何设置。 事务隔离级别在很大程度上确定了事务能处理哪些要进行事务处理的数据。例如,你 或许想让你的事务能去读取未提交的数据(解除前面的事务数据库页面锁)。或者你正写入 的数据可能很关键,因此你要让事务只能读取已提交的数据,甚至,当你的事务正执行时, 你禁止让其它事务在该数据上工作。你可以选择使用的隔离级别如表 15-1 所示。使用 TransactionScope 活动的 TransactionOptions 属性,你可同时设置隔离级别和超时值。 表 15-1 事务的隔离级别 隔离级别 含义 Chaos 无法改写隔离级别更高的事务中的挂起的更改。 ReadCommitted 在正在读取数据时保持共享锁,以 避 免 脏 读 ,但 是在事务结束之前可以更 改数据,从而导致不可重复的读取或幻像数据。即在事务期间未提交的数 据不能读出,但能被修改。 ReadUncommitted 可以进行脏读,意思是说,不发布共享锁,也不接受独占锁。即在事务期 间未提交的数据既能读出也能修改。要记住数据可以修改:不 能 保 证 该 数 据 和 随 后 读 取 的 数据是相同的。 RepeatableRead 在查询中使用的所有数据上放置锁,以 防 止 其 他 用 户更新这些数据。防止 不可重复的读取,但是仍可以有幻像行。即在事务期间未提交的数据能读 出但不能修改。但是,能插入新数据。 Serializable 在 DataSet 上放置范围锁,以 防 止 在事务完成之前由其他用户更新行或向 数据集中插入行。未提交的数据能读出但不能修改,在事务期间不能插入 新数据。 Snapshot 通过在一个应用程序正在修改数据时存储另一个应用程序可以读取的相 同数据版本来减少阻止。表示您无法从一个事务中看到在其他事务中进行 的更改,即便重新查询也是如此。即未提交的数据能读出,但在事务真正 修改数据之前,该事务会验证在它最初读取数据后其它事务没有修改该数 据。假如数据被修改了,该事务就激发员工错误。这样做的目的是让事务 能读取先前提交的数据值。 Unspecified 正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。当使用 OdbcTransaction时,如果不设置IsolationLevel或者将IsolationLevel 设置为 Unspecied,事务将根据基础 ODBC 驱动程序的默认隔离级别来执 行。假如你试图设置事务隔离级别为该值,那么就会抛出一个异常。只有 事务系统能设置该值。 当你拖拽一个 TransactionScope 活动到你的工作流中后,隔离级别会自动设置为 Serializable。根据你的架构要求,你可不受限制地改变它。Serializable 是最严格的隔 离级别。但是它也在一定程度上限制了可扩展性。对要求吞吐量更大一些的系统而言,选择 ReadCommitted 来作为隔离级别并不是稀罕事,但是这要根据你的个人需求来做决定。 提交事务 假如你正使用的是 SQL Server 事务,或者可能是 COM+事务,你就知道一旦数据已被 插入、更新或删除,你就必须提交该事务。也就是说,你启动两阶段提交协议和数据库来永 久记录或移除这些数据。 但是,对于 TransactionScope 活动而言不是必要的。假如事务执行成功(当插入、更 新、删除数据时没有出错),当工作流执行过程离开该事务范围时,该事务会为你自动提交。 回滚事务 怎么样回滚失败的事务呢?哦,就像事务成功为你提交一样,假如事务失败数据也将 被回滚。有趣的是回滚是悄无声息的,至少就 WF 而言是很关注的。假如你需要检查事务是 成功还是失败,你 就 需 要 亲 自 加入逻辑代码来完成这件事。假如事务失败,TransactionScope 是不会自动抛出异常的。它仅仅回滚数据然后继续前进。 使用 CompensatableTransactionScope 活动 假如 XA 类型的事务不能胜任,你 可 以 拖 拽 CompensatableTransactionScope 活动到你 的工作流中来提供补偿式的事务处理过程。CompensatableTransactionScope 活动和 TransactionScope 一样是一个组合活动。但是,CompensatableTransactionScope 实现了 ICompensatableActivity 接口,通过实现 Compensate 方法赋予了它能对失败的事务进行补 偿的能力。 和 TransactionScope 相似,CompensatableTransactionScope 活动也创建一个环境事 务。包含进 CompensatableTransactionScope 的活动共享这个事务。假如它们操作都成功, 数据就被提交。但是,如果它们中任何一个操作失败,你 通 常 通过执行一个 Throw 活动去启 动补偿。 提示:补偿式事务能支持像数据库之类的传统资源,当事务提交时,数据就像以 XA 类型的事务一样被提交。但是,补偿式事务的优点是你不必选择一个 XA 类型的资源来存储 数据。对于不支持传统资源的一个典型例子是使用一个 Web 服务来把数据发送到一个远程站 点。假如你把数据发送到远程站点但是后来必须进行补偿的话,你 需 要 和 数据不再有效的远 程站点间进行某种通信。(你怎么实现这些有赖于各个远程站点。) Throw 导致死亡失败并为你的 CompensatableTransactionScope 活动去执行你的补偿 处理程序。你 可通过 CompensatableTransactionScope 活动的智能标签来访问该补偿处理程 序,在许多地方和你添加一个 FaultHandler 是一样的。 备注:尽管抛出一个异常启动事务补偿,但是 Throw 活动自身并不会进行处理。你也 能在你的工作流中放入一个 FaultHandler 活动来防止工作流提前终止。 使用 Compensate 活动 当你通过 CompensatableTransactionScope 对失败进行补偿时,会调用补偿处理程序。 假如你有多个补偿式事务时,事 务 将以默认的顺序进行补偿,从嵌套得最深的事务开始向外 工作。(在下一节中你将看到这些怎么实现。)当你的处理逻辑要求补偿的时候,你可以在 你的补偿处理程序中放入一个 Compensate 活动来开始为所有已经完成的、支持 ICompensatableActivity 的活动进行补偿。 实际上异常的情况总是会引起补偿,因此使用 Compensate 活动不是必须的。为什么还 要用它呢?因为你可能在一个 CompensatableSequence 活动中嵌入超过一个以上的补偿式 事务。假如一个事务失败并去补偿的话,你 就 能为其它事务开始补偿工作,即使该事务先前 已经成功完成。 备注:Compensate 活动只有在补偿处理程序(cmpensation handler)、取消处理程 序(cancellation handler)和失败处理程序(fault handler)中有效。 只有当你需要以某个其它的顺序而不是默认的补偿顺序进行补偿的时候,你才应该去 使用 Compensate 活动。默认补偿的顺序和所有嵌入的 ICompensatableActivity 活动的完成 顺序正好相反。假如这个顺序不适合你的工作流模型,或者假如你想要有选择性地为已完成 的可补偿子活动进行补偿的话,Compensate 活动是一个可选择的工具。 备注:Compensate 活动使用它的 TargetActivityName 属性来识别出哪个可补偿式活 动应该被补偿。假如超过一个以上的可补偿式活动要进行补偿工作的话,你 就 需 要 使用一个 以上的 Compensate 活动。假如你决定不去补偿一个特定的事务的话,对于该事务或者包含 其父活动中的补偿处理程序中可不做任何事情。 通过让你能决定你是否想立即对支持补偿的子活动进行补偿的这一手段,Compensate 活动为你提供了在补偿处理过程中进行控制的能力。这个能力能让你的工作流按照你业务流 程的需要在一个嵌套的补偿式活动中明确无误地执行补偿工作。通过在 Compensate 活动中 指定你想要去补偿的可补偿式活动,只要该可补偿式活动先前已成功的提交,那么该可补偿 式活动中的任何补偿代码都将被执行。 假如你想对超过一个以上的可补偿式活动进行补偿的话,你可在你的处理程序中为每 一个你想去补偿的可补偿式活动添加一个 Compensate 活动。假如 Compensate 活动用在了一 个可补偿式活动的处理程序中,而该可补偿式活动的又容纳有嵌入的可补偿式活动,并且假 如为该 Compensate 活动指定的 TargetActivityName 是其父活动的话,在 该 父 活动中所有已 经成功提交的子(可补偿式)活动的补偿工作也都会被调用。为了强调这些共说了三次。 使用 CompensatableSequence 活动 前一节可能留给你一个疑问:Compensate 活动以什么为载体。毕竟,你不能嵌入补偿 式事务,你不能嵌入基于 WF 的任何一种类型的事务。 但是让我们从不同的角度来看待它。你会怎样把两个可补偿式事务捆在一起以便其中 一个失败时触发另一个去补偿,而且假如另一个已经成功完成了呢?答案是你成对地把补偿 式事务放进一个单一的 CompensatableSequence 活动中。然后,假如两个子事务活动中的其 中一个失败的话,在 CompensatableSequence 活动的补偿代码或者失败处理程序中,你 就 触 发 这 两 个 子 事 务 活动去进行补偿。甚至更有趣的是,在你把三个补偿式事务一起捆进一个单 一的 CompensatableSequence 活动中,并且允许即使其它两个事务失败并被补偿的情况下, 该事务也能成功。Compensate 活动为你提供了这些控制能力。 这些突出了 CompensatableSequence 活动的作用。CompensatableSequence 活动从本 质上来说是一个 Sequence 活动,你使用 CompensatableSequence 活动的方式和任何顺序活 动一样。主要的区别是你能在一个单一的 CompensatableSequence 活动中嵌入多个可补偿式 活动,实际上是把相关的事务捆在一起。让 CompensatableSequence 活动结合 CompensatableTransactionScope 和 Compensate 活动能为你在你的工作流中提供强大的事 务控制能力。 备注:CompensatableSequence 活动能被嵌进其它的 CompensatableSequence 活动中, 但是它们不能作为 CompensatableTransactionScope 活动的子活动。 提示:当在单一的一个可补偿式序列中包含多个补偿式事务的时候,你不需要为个别 事务活动指定补偿功能。如果需要的话补偿会流向父活动,因此假如你需要的话,你 能 把你 的补偿活动都聚集到一个封闭的可补偿式顺序活动中。 创建一个事务型的工作流 我已创建了一个模拟自动柜员机(ATM)的应用程序,你提供你的个人识别码(也称作 PIN)后,就可从你的银行账户中存款和取款。存款操作将被嵌进一个 XA 类型的事务中,而 取款过程如果操作失败的话将进行补偿。为了对应用程序的事务性质进行练习,我在该应用 程序中放入了一个“Force Transactional error”复选框(check box)。只要简单地选中 该复选框,接下来相关的数据库操作将失败。对于该应用程序的工作流来说是基于状态的, 它比你在前一章(第 14 章,“基于状态的工作流”)中看到过的应用程序还要复杂。我在 图 15-1 中展示了该状态机工作流。应用程序的大部分代码我已经为你写出了。接下来你将 在练习中添加事务组件。 图 15-1 WorkflowATM 的状态图 该应用程序的用户界面如图 15-2 所示。这是应用程序的初始状态,插入银行卡之前 ATM 的状态都和这近似。当然,该示例不能处理真实的银行卡,因此点击 B 键将把用户界面 (和应用程序状态)切换到 PIN 验证状态(如图 15-3 所示)。 图 15-2 WorkflowATM 的初始用户接口 图 15-3 WorkflowATM 的 PIN 验证用户界面 你使用按键输入正确的 PIN。一旦输入了四个数字代码,你可点击 C 键来开始对数据 库进行查询以验证该 PIN。假如 PIN 经过验证(注意帐号在左下角,该 PIN 码必须是该帐号 的),用户界面便切换到如图 15-4 所示的操作选择状态。这里你可选择从你的账户中存款 或取款。 图 15-4 WorkflowATM 的操作选择用户界面 对于存款和取款的应用程序用户界面是相似的,因此我只展示了存款的用户界面,如 图 15-5 所示。你可再次使用按键输入金额然后点击一个命令键:D 键来进行存款或取款, 或者 E 键来取消该交易。 图 15-5 WorkflowATM 的存款交易用户界面 假如交易成功,你会看到如图 15-6 所示的界面。假如失败,你将看到如图 15-7 所示 的错误屏幕。这都没关系,点击 C 键重新开始该工作流。 图 15-6 WorkflowATM 交易成功的用户界面 图 15-7 WorkflowATM 交易失败的用户界面 该应用程序需要一个数据库来完整地对 WF 的事务能力进行测试。因此,我创建了一个 简单的数据库来保存包含有 PIN 和账户余额的用户帐户信息。几个存储过程也被用来和数据 库进行配合。所有涉及数据库更新的存储过程都必须在一个事务中执行:我要对@@trancount 检查,假如它为 0,我就从该存储过程中返回一个错误。假如我错误地使用一些 ADO.NET 代 码来初始化我自己的 SQL Server 事务的话,这些就能证实环境事务正被使用。这些也意味 着你需要创建一个数据库实例,但是这很容易实现,因为你在前面的章节中已经学过了怎样 在 SQL Server Management Studio Express 中执行查询语句。实际上,我们将从这个任务 起步因为我们将很快需要数据库来对应用程序进行开发和测试。 备注:之前我忘了提到,该数据库的创建脚本只创建了一个账户:11223344,PIN 为 1234。该应用程序允许你去改变账户以及你想使用的任何 PIN 值,但是你要么使用该账户 (11223344)以及它的 PIN(1234),要么创建你自己的账户记录,否则将不允许你去进行 存取款。 创建 Woodgrove ATM 数据库 1.在本章源代码中你会找到“Create Woodgrove Database.sql”数据库生成脚本。找 到它然后启动 SQL Server Management Studio Express。 备注:当然 SQL Server 完全版也可以。 2.当 SQL Server Management Studio Express 打开后,把“Create Woodgrove Database.sql”文件拖拽到 SQL Server Management Studio Express 中。再打开该脚本文 件后执行它。 3.该脚本会创建 Woodgrove 数据库和全部数据。该脚本的第五和第七行指明了数据库 的目录和文件名。假如你不能在该默认目录(C:\Program Files\Microsoft SQL Server) 下加载 SQL Server,你可能需要修改将被生成的数据库的默认目录。你可以根据需要随意修 改。在大多数情况下,你不需要作出修改。点击“执行”按钮运行该脚本并生成数据库。 4.当你在使用 SQL Server Management Studio Express 中,假如你没有进行第 6 章“加 载和卸载实例”中的“为持久化创建 SQL Server”这一节的话,需要去安装工作流持久化 数据库,现在就这样做。 在完成了这四个步骤的话,你将有两个数据库:Woodgrove 数据库用来保存银行业务信息, WorkflowStore 数据库用来进行工作流的持久化:现在我们就来写一些工作流事务代码。 添加 XA 类型事务到工作流中 1.下载本章源代码,在 WorkflowATM 目录中你会找到 WorkflowATM 应用程序 (WorkflowATM Completed 目录中是本解决方案的最终完成版),打开 WorkflowATM 目录中 的解决方案。你可能需要在 App.Config 文件中修改 SQL Server 的连接字符串。 2.为了让自定义活动在 Visual Studio 工具箱中显示出来,需要编译整个解决方案。 3.尽管 WorkflowATM 应用程序比较复杂,但它遵循的模式我们贯穿本书都在使用。该 Windows Forms 应用程序自身和工作流的通信通过一个本地通信服务实现,它使用了我用 wca.exe 创建的一些自定义活动。该服务在 BankingServer 项目中,但是该工作流却在 BankingFlow 项目中。我们只关注工作流自身的代码。在 BankingFlow 项目中找到 Workflow1.cs 文件,然后在工作流视图设计器中打开它准备进行编辑。该工作流会显示出 你在这里看到的界面。它看起来和图 15-1 有些相像吗? 4.为插入 XA 类型的事务,首先双击 DepositState 活动中的 CmdPressed4 EventDriven 活动。 5.仔细看看左边,你会看到名称为 makeDeposit1 的 Code 活动。从工具箱中拖拽一个 TransactionScope 活动到 makeDeposit1 活动和该 Code 活动上面的 ifElseBranchActivity11 标题之间。 6.拖拽你刚才插入的该 transaction scope 活动下面的 makeDeposit1 活动,把它放到 该 transaction scope 活动中以便让 makeDeposit1 Code 活动在事务中执行。 备注:随时检查 MakeDeposit 方法中包含的代码,它 被 绑 定到 makeDeposit1 活动。你 会发现这些代码有通常的 ADO.NET 数据库访问代码。一个有趣的事情是你可看到在该代码中 没有发起 SQL Server 事务。相反,当该代码被执行时将使用环境事务。 7.编译整个解决方案。 8.按下 F5 或者从 Visual Studio 的“调试”菜单中选择“启动调试”去测试该应用程 序。该账户应该已经设置好了。点击 B 键进入密码验证界面,然后键入 1234(PIN 码)。点 击 C 键验证该 PIN 码并进入业务选择界面。 备注:假如该应用程序验证 PIN 失败,并且你键入的是正确的 PIN 码,则可能是因为 Woodgrove 数据库的连接字符串不正确。(我进行了错误处理是为了让应用程序不会崩溃。) 验证连接字符串是正确的后再一次运行该应用程序。第 5 章有一些针对创建连接字符串的建 议。 9.因为你为存款逻辑添加了事务,因此点击 C 键进行一次存款。 10.输入 10 去存入$100($10.00 的 10 倍),然后点击 D 键去启动该业务。这个业务 应该成功并且界面现在会指出该业务已成功完成。因为 Woodgrove 数据库创建脚本加载了一 个有$1234.56 的虚拟银行账户,因此现在显示的余额为$1334.56。注意你能从应用程序的 左下角看到该余额。点击 C 键回到初始界面。 11.现在我们来强制让该业务执行失败。Deposit 存储过程带有一个会引起该存储过程 返回一个错误的参数值。选择“Fore Transactional error”多选框会为 Deposit(存储过 程)指定一个产生错误的值。因此点击 B 键再一次进入 PIN 验证界面,然后输入 1234,点 击 C 键进入银行业务选择界面。 12.再一次点击 C 键进行存款,然后输入 10 再去存入$100,但是这次在点击 D 键之前 选中“Fore Transactional error”多选框。 13.点击 D 键后,应用程序会显示业务执行失败,但要注意余额。它显 示 当 前余额仍然 是$1334.56,这是该事务执行前的余额。成功的事务(步骤 10)和失败的事务(步骤 12) 两者都由 TransactionScope 活动处理,它是在你第 5 步放进工作流中的。 这非常强大!通过包括一个单一的 WF 活动,我们获得了在数据库更新之上的自动的事 务(XA-style)控制能力。执行一个补偿事务也能一样容易吗?碰巧,它需要更多的工作, 但是把一个补偿事务添加到你的工作流中仍然不难。 向你的工作流中添加补偿事务 1.打开 WorkflowATM 解决方案,在工作流视图设计器中再次打开 Workflow1.cs 文件。 找到 WithdrawState 活动,然后双击 CmdPress5 活动。这 会 打 开 CmdPressed5 活动进行编辑, 一旦它被打开后,你会在工作流的左边看到 makeWithdrawal1 Code 活动。 2.和你之前处理事务的工作类似,从 Visual Studio 的工具箱中拖拽一个 CompensatableTransactionScope 活动,把它放到 makeWithdrawal1 活动和它上面的 ifElseBranchActivity13 标题的中间。 3.从 compensatableTransactionScope1 活动的下面拖拽 makeWithdrawal1 Code 活动, 把它放进事务的范围(transaction scope)之内。MakeWithdrawal 方法被绑定到 makeWithdrawal1 活动,现在它将在一个环境事务中执行它的 ADO.NET 代码,就像存款 (deposit)活动做的一样。 4.但是,和存款功能不同,你必须提供补偿逻辑。传统意义上业务不能回退。相反, 你需要访问 compensatable TransactionScope1 补偿处理程序并添加你自己的补偿功能。为 此 ,把鼠标移到 compensatable TransactonScope1 的标题下面的智能标签上,一旦点击它 则在它下面将弹出和该活动相关的视图菜单。 5.点击最下面的“查看补偿处理程序”菜单,激活补偿处理程序视图。 6.从 Visual Studio 拖拽一个 Code 活动并把它放到该补偿处理活动中。 7.在该 Code 活动的 ExecuteCode 属性中输入 CompensateWithdrawal。Visual Studio 会在你的源代码中自动插入该方法并为你切换到代码编辑器界面下。 8.在为你刚刚插入的 CompensateWithdrawal 方法中添加下面的代码: // Here, you "undo" whatever was done that did succeed. The // code that withdrew the money from the account was actually // successful (there is no catch block), so this compensation // is forced. Therefore, we're safe in depositing the amount // that was withdrawn. Note we can't use MakeDeposit since // we require a SQL Server transaction and this method is // called within the compensation handler (i.e., We can't drop // a TransactionScope activity into the compensation to kick // off the SQL Server transaction). We'll create the transaction // ourselves here. // // Craft your compensation handlers carefully. Be sure you know // what was successfully accomplished so that you can undo it // correctly. string connString = ConfigurationManager.ConnectionStrings["BankingDatabase"]. ConnectionString; if (!String.IsNullOrEmpty(connString)) { SqlConnection conn = null; SqlTransaction trans = null; try { // Create the connection conn = new SqlConnection(connString); // Create the command object SqlCommand cmd = new SqlCommand("dbo.Deposit", conn); cmd.CommandType = CommandType.StoredProcedure; // Create and add parameters SqlParameter parm = new SqlParameter("@AccountNo", SqlDbType.Int); parm.Direction = ParameterDirection.Input; parm.Value = _account; cmd.Parameters.Add(parm); parm = new SqlParameter("@ThrowError", SqlDbType.SmallInt); parm.Direction = ParameterDirection.Input; parm.Value = 0; cmd.Parameters.Add(parm); parm = new SqlParameter("@Amount", SqlDbType.Money); parm.Direction = ParameterDirection.Input; parm.Value = CurrentMoneyValue; cmd.Parameters.Add(parm); SqlParameter outParm = new SqlParameter("@Balance", SqlDbType.Money); outParm.Direction = ParameterDirection.Output; outParm.Value = 0; // initialize to invalid cmd.Parameters.Add(outParm); // Open the connection conn.Open(); // Initiate the SQL transaction trans = conn.BeginTransaction(); cmd.Transaction = trans; // Execute the command cmd.ExecuteNonQuery(); // Commit the SQL transaction trans.Commit(); // Pull the output parameter and examine CurrentBalance = (decimal)outParm.Value; } catch { // Rollback Note we could issue a workflow exception here // or continue trying to compensate (by writing a transactional // service). It would be wise to notify someone if (trans != null) trans.Rollback(); } finally { // Close the connection if (conn != null) conn.Close(); } } 9.把补偿代码添加到你的工作流中后,回到工作流视图设计器上来。遵循你刚刚插入 Code 活动的步骤,拖拽一个自定义的 Failed 活动到补偿处理程序中。注意当你回到工作流 视图设计器上的时候,Visual Studio 可能会重新进行调整并把你带回到顶级状态活动布局 界面上来。假如这样的话,可简单地在 WithdrawState 中再一次双击 CmdPressed5 活动来访 问 compensatableTransactionScope1 活动,并再一次从它的智能标签中选择补偿处理程序 视图。 10.在 Failed 活动的 error 属性中输入“Unable to withdraw funds”。 11.在刚才你插入进你工作流的 Failed 活动的下面,拖入一个 SetState 活动。在它的 TargetStateName 属性中选择 CompletedState。 12.按下 F5 或者选择“调试”菜单中的“启动调试”来再次测试该应用程序。在该应 用程序开始执行后,点击 B 键进入 PIN 验证界面,然后输入 1234(PIN 码)。点击 C 键对 PIN 进行验证并进入业务选择界面。 13.点击 D 键进行取款。 14.输入 10 取$100($10.00 的十倍),然后点击 D 键开始交易。假如没有你的干预的 话,该业务应该会成功完成,并且屏幕现在会告诉你业务完成了,账户余额是$1234.56。 15.现在我们再次让该业务执行失败。点击 C 键重新启动 ATM,然后点击 B 键再次进入 PIN 验证界面。输入 1234,然后点击 C 键进入银行业务选择界面。 16.输入 10 再次取出$100,并且选中“Fore Transactional error”复选框。然后点 击 D 键启动该业务。 17.在你点击 D 键后,应用程序会指出业务执行失败并显示当前的账户余额 ($1234.56)。由于在 MakeWithdrawal 方法中没有 catch 语句,因此我们知道进行了取款。 (假如不是如此的话,应用程序会由于一个重大的错误而被终止。)这意味着该账户实际上 是取出了$100,并且补偿功能也执行了,它为该账户又添加回$100。 注意:也有其它办法看到账户的取款和存款。你 可 以在补偿功能模块中设置一个断点, 或者假如你熟悉 SQL Server Profiler 并且你使用的是完整零售版本的 SQL Server 话,你 甚至可以执行 SQL Server Profiler 进行查看。 第十六章:声明式工作流 学习完本章,你将掌握: 1.理解过程式(imperative)工作流模型和声明式(declarative)工作流模型之间的 主要区别 2.创建声明式工作流 3.使用 XAML XML 词汇来创建工作流 4.调入基于 XAML 的工作流并执行 许多开发者或许并不知道 WF 既能用基于过程化的定义来执行工作流(使用工作流视图 设计器)也能用基于声明式的定义来执行工作流(工作流使用 XML 来进行定义)。 每一种风格都有优点。当你使用我们贯穿本书已使用过的技术来创建你的工作流应用 程序的时候,工作流模型实际上是被编译进了一个可执行的程序集中。其 优 点是加载、执 行 工作流的速度快。 但这种风格也缺乏灵活性。尽管有为 WF 加入动态能力的办法(这不包括在本书中), 但通常你的工作流仍然要由你去编译它们。假如你的业务处理逻辑变化了的话,除非你为工 作流中的判定使用了声明性规则(在第 12 章“策略和规则”中讨论过),否则的话你将不 得不修改你的工作流定义,重新进行编译和部署,此外还伴随着去执行所有相关的测试和验 证工作。 但是,工作流运行时有能力接受几乎任何形式的工作流定义。你以前还不得不去写一 些代码来把你提供的工作流定义转化成工作流运行时能够执行的模型。事实上,这些正是 WF 处理基于 XML 的工作流定义所要做的事。 正如你可能期望的,把你的工作流记录进一个 XML 格式的文件中,这能让你很容易地 修改和重新部署。这就不用在 Microsoft Visual Studio 中重新编译你的工作流,你可简单 地使用任何 XML 编辑器(甚至是 Windows 中的“Notepad”记事本程序)来修改基于 XML 的 工作流定义并把它创建的工作流模型提供给工作流运行时。你 甚至能有两全其美的办法:通 过使用 WF 的工作流编译器来编译你的 XML 工作流定义。我们将在本章探讨这些内容。 声明式工作流——XML 标记 首先,.NET 3.0 中的声明式应用程序(它包括 WPF)的定义有着悠久的历史。WPF 开 始提供声明式编程的能力,它 既 可 完 全地进行声明化也可进行部分声明。你 可 完 全地使用像 XML 应用程序标记语言或者 XAML(读作“ zammel”)中的 XML 标记词汇来封装你的应用程序。 或者,通过使用特殊的基于 XAML 的结构,你能把你应用程序的某部分编译进程序集中并通 过 XAML 来把它调入执行。你甚至能写下 C#代码并把它嵌入到你的 XAML 定义中,或者把你 的 C#代码放进代码后置文件中,以便稍后执行它。 备注:你不能找到比 Charles Petzold 的最新著作:“应用程序 = 代码 + 标记” (“Application = Code + Markup”,2006 年微软印刷)更好的在 XAML 和 WPF 方面的专 著了。假如你对 XAML 论题的详细细节感兴趣的话,强烈建议你重新看看本书的第 19 章。 做下面的这个实验其实只是为了好玩。在你的系统中创建一个新的文本文件,把它命 名为 Button.xaml。把列表 16-1 中的代码输入到该文件中并进行保存,然后双击该文件。 因为你必须安装.NET 3.0 组件才能创建工作流应用程序,因此其实你也已经完成了对.xaml 类型文件的注册工作。Windows 知道把 XAML 文件加载到你的 Web 浏览器并显示它。尽管它 仅仅只有一个按钮,但这是一个完整的 WPF 应用程序,虽然它很简单。图 16-1 显示了使用 Window XP 中的 IE 7.0 来展示该按钮的输出效果。 列表 16-1 展示一个按钮的基于 XAML 的应用程序示例 图 16-1 正在运行的基于 XAML 的按钮 WF 团队也把这些理念一起融入到了 WF 中。虽然 WF 的 XML 遵循 XAML 命名空间的约定, 但包含和 WF 相关的 XML 的文件名通常都使用.xoml 文件扩展名来进行命名。这种做法能让 自动化工具把该文件解释为一个工作流文件而不是一个外观(presentation)文件。事实上, 我们将在本章中使用的一个工具,它叫工作流编译器(wfc.exe),在创建基于 XAML 的工作 流时它要求使用.xoml 类型的文件。 尽管列表 16-1 不是对工作流的声明,但要仔细看看你看到的 XML。注意该 XML 元素的 名称和.NET WPF 类中支持的名称是相同的,在本例子中它是 Button。还值得一提的是该按 钮的属性将由 XAML 文件进行解释,它们是:FontSize,Margin 和 Foreground。通过改变这 些属性或者添加其它属性,我们能非常容易地修改该按钮的特性。 基于工作流的 XAML 文件也有同样的特点。XML 元素的名称代表了像 CodeActivity 或 者 IfElseActivity 之类的活动类型。和你可能期望的一样,每一个元素能够包含它们的属 性以及它们的值。至于工作流的结构,组合活动将会有子 XML 元素,而基本活动则没有。 声明命名空间与命名空间组织 XML 通常对命名空间非常敏感,XAML 也不例外。有几个命名空间是关键的,这些包括 和工作流相关的命名空间和与.NET 自身相联系的命名空间。 你的 XAML 文件必须包括的主要的命名空间是 http://schemas.microsoft.com/winfx/2006/xaml,它 通 常 使用 x 前缀。在 XML 文件中,命 名空间的声明看起来和下面这些相似: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 基于工作流的 XAML 文件也必须包括工作流的命名空间: http://schemas.microsoft.com/winfx/2006/xaml/workflow。如果有命名空间的前缀的话, 约定它为 wf,但在基于工作流的 XAML 文件中你通常为工作流的命名空间指定默认的命名空 间(也就是省略它的前缀): xmlns=http://schemas.microsoft.com/winfx/2006/xaml/workflow 它是一个罕见的工作流,因为它不需要访问.NET 运行时,但这些是怎样在 XML 文件中 做到的呢?哦,其实该文件并不调用任何和.NET 运行时相关的东西——工作流运行时会为 你做这些。该 XML 文件仅仅需要识别出它需要的.NET 公共语言运行时(CLR)的组件,这些 东西要么由.NET 本身来提供,要么是你用基于 XML 的工作流所包含的自定义对象。为了识 别出这些行为,可使用一个特定的句法规则来创建一个 XML 命名空间。 XAML 将使用的 CLR 命名空间可通过使用两个关键字来创建:clr-namespace 和 assembly=。例如,如果你想在你的基于 XAML 的工作流中使用 Console.WriteLine,你就需 要为.NET 的命名空间 System 创建如下的一个命名空间: xmlns:sys="clr-namespace:System;assembly=System" 但是当你在基于 XAML 的工作流中创建你想使用的程序集时,仅仅创建程序集,为它指 定命名空间,然后在工作流中包含它们是不够的。工作流运行时将加载你指定的程序集,但 是它需要使用一个附加的特性:XmlnsDefinition 特性,它自动加载对象并执行。 例如,考虑你想在你的基于 XAML 的工作流中使用下面这个活动: public class MyActivity : Activity { } 假设该类在 MyAssembly 程序集的 MyNamespace 命名空间中,则把它引进基于 XAML 的 工作流中去的代码就应该和列表 16-2 中展示的一样。 列表 16-2 使用 XmlnsDefinition 特性的例子 using System; using System.Collections.Generic; using System.Text; using System.Workflow.ComponentModel; using System.Workflow.Activities; [assembly:XmlnsDefinition("urn:MyXmlNamespace","MyNamespace")] namespace MyNamespace { public class MyActivity : Activity { // Not a very active activity ! } } 引用这个类的 XML 如下: xmlns:ns0="urn:MyXmlNamespace" 命名空间使用的字符串并不重要,这 也 许 令 人 惊 奇 。重 要 的是在声明命名空间的时候, 在 XmlnsDefinition 中声明的命名空间字符串要和在 XML 文件中使用的命名空间字符串相 同。在 XML 中使用的命名空间必须是唯一的。也就是说,它 们 必须和 XML 文件中已经使用的 现有的命名空间有所不同。我们将在接下来的一节对这进行更多的体会。 备注:假如你对 XML 命名空间比较生疏,则下面的网址或许对你有所帮助: http://msdn.microsoft.com/XML/Understanding/Fundamentals/default.aspx?pull=/lib rary/en-us/dnxml/html/xml_namespaces.asp。 创建并执行基于 XAML 的工作流 在 XML 文件中定义的工作流能以两种方式执行:通过工作流运行时直接执行或者编译 为程序集。对于直接执行 XML 文件中包含的工作流来说,首先把 XML 加载进 XmlTextReader 后,只需简单地调用使用 XmlTextReader 作为参数的工作流运行时的 CreateWorkflow 方法 即可去执行该工作流。编 译 XML 文件涉及到工作流编译器 wfc.exe 的使用。我们通过使用你 在图 16-2 中看到的这个简单的工作流来看看这两种情形。 图 16-2 基于 XAML 的工作流定义实验所用的简单工作流 创建一个直接执行 XML 的新工作流应用程序 1.下载本章源代码,打开 DirectXml-Workflow 目录中的解决方案。 2.在我们对 Program.cs 文件进行修改以便执行我们的工作流前,我们先创建该工作流 本身。在 Visual Studio 的解决方案资源管理器中右键单击 DirectXmlWorkflow 项目,依次 选择“添加”、“新建项”。当“新建项”对话框出现后,从列表中选择“XML 文件”并把 它命名为“Workflow1.xml”。最后点击“添加”。 备注:Visual Studio 将创建一个扩展名为.xml 的文件。这时还不能把它改为.xoml, 其原因是让该 XML 文件能在工作流视图设计器中进行编辑。.xoml 文件扩展名是一个约定, 对于 WF 工作流运行时来说这不是必须的,因此我们实际上只是把 Visual Studio 作为一个 简单的 XML 编辑器来使用(其实这里可使用任何 XML 编辑器)。当应用程序执行时要确保该 XML 文件已被复制到该应用程序的执行路径下以便代码能找到该文件。 3.在解决方案资源管理器中选中该 Workflow1.xml 文件以便在属性面板中激活它的文 件属性。 4.修改 Workflow1.xml 文件的“复制到输出目录”属性选项,把它从“不复制”修改 为“如果较新则复制”。 5.添加下面的 XML 内容到该文件中,然后进行保存。 6.打开 Program.cs 文件进行编辑。在 已 存 在的 using 语句末尾添加下面的 using 语句。 using System.Xml; 7.查看代码,找到下面这行代码: Console.WriteLine("Waiting for workflow completion."); 8.在你上一步骤找到的代码下面添加下面的代码,以便能去调用你刚才创建的基于 XAML 的工作流。 // Load the XAML-based workflow XmlTextReader rdr = new XmlTextReader("Workflow1.xml"); 9.在你刚添加的代码下添加如下这些创建工作流实例的代码: // Create the workflow instance. WorkflowInstance instance = workflowRuntime.CreateWorkflow(rdr); // Start the workflow instance. instance.Start(); 10.编译该解决方案,纠正任何编译错误。 11.按下 Ctrl+F5 或者 F5 键执行该应用程序,你将看到下面的执行结果。 你能以这种方式创建一些相当复杂的工作流。但是,它 只 局 限 于以下几个方面。首先, 尽管有在被调用的工作流内部执行 C#代码的机制,但如果没特别要求的话,最好还是创建 自定义的程序集来容纳你想执行的代码。其 次 ,如果没有首先创建一个自定义的根活动(root activity)来接受你输入的参数的话,你就不能把参数传到该工作流中。对于这些限制,或 许将来的某些时候会有所改变,但对于现在来说就只能这样。在本章的晚些时候我们将创建 一个自定义的程序集来进行演示。 但是,有一个中间的步骤是把你的基于 XAML 的工作流编译进一个程序集中,以 便 你 能 进行引用并执行。为了编译该基于 XAML 的 XML 文件,我们将使用工作流的编译器:wfc.exe。 备注:想了解 wfc.exe 的更多知识的话,可看 看:msdn2.microsoft.com/en-us/library/ms734733.aspx。 创建执行一个对 XML 进行了编译的新工作流应用程序 1.下载本章源代码,打开 CompiledXmlWorkflow 目录中的解决方案。 2.和前一个示例中的第 2 个步骤一样,添加一个新的 Workflow1.xml 文件。因为你将 使用工作流编译器来编译该工作流,因此你不需要改变该文件的编译器设置(也就是说不需 要前一节的步骤 3 和步骤 4)。 3.把下面的 XML 添加到 Workflow1.xml 文件中并进行保存: 尽管这些可能看起来和你在前一个示例中添加的 XML 相似,但有一个细微的差别: x:Class 属性。工作流编译器需要它,因为当编译工作流时编译器将需要这个新类的名称。 4.在解决方案资源管理器中把该文件的扩展名从.xml 修改为.xoml。假如 Visual Studio 发出一个关于修改文件类型的警告提示的话,忽略它并尽情点击“是”。wfc.exe 工具不接受以.xml 作为扩展名的 XML 文件,它 们 必须使用.xoml 作为文件扩展名。从“文件” 菜单中选择“保存 Workflow1.xoml”或者按下 Ctrl+S 来保存该文件。 5.在 Windows 中打开命令提示符 cmd 窗口。 6.在命令提示符中输入:cd \Workflow\Chapter16\CompiledXmlWorkflow\CompiledXmlWorkflow,然后按下回车键改变 相对目录。在该目录中将能直接访问到该 Workflow1.xoml 文件。 7.在命令提示符中输入"C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\Wfc.exe" workflow1.xoml,然后按下回车键。当然,这是假想 Windows SDK 是安装到你 C 盘的 Program Files 目录中的。假如你把 Windows SDK 安装到了 其它地方,则需要修改上面的目录。 8.工作流编译器应该执行并无错误发生。当它的编译过程完成后,它会生成一个动态 链接库 workflow1.dll,它的目录位置和 Workflow1.xoml 所在的目录位置是一样的。你现 在需要引用这个动态链接库,因为它包含了你在第 3 步创建的 XML 文件中定义的工作流。为 此 ,在解决方案资源管理器中的 CompiledXmlWorkflow 项目上点击右键,然后选择“添加引 用”。当“ 添 加 引 用”对话框弹出后,选择“浏览”选项卡,然后从列表中选中 workflow1.dll, 最后点击“确定”。 9.该工作流现在就完成了,我们现在需要完成我们的主应用程序。打开 Program.cs 文件进行编辑,查看该文件的源代码,找到下面这行代码: Console.WriteLine("Waiting for workflow completion."); 10.在上面这行代码下添加如下这些代码: // Create the workflow instance. WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(Workflow1)); // Start the workflow instance. instance.Start(); 11.编译本解决方案,纠正任何出现的编译错误。 12.按下 Ctrl+F5 或者 F5 执行该应用程序,你将看到如下的执行结果。 你也可以创建一个代码后置文件并把包含的代码编译进你的工作流程序集中,尽管本 示例我们没有使用它。按照约定,假如该 XML 文件使用.xoml 作为文件扩展名,该代码后置 文件就使用.xoml.cs。wfc.exe 工具能接受一系列的.xoml 文件以及与它们相关联 的.xoml.cs 文件,它将把所有这些文件都编译进一个单独的工作流程序集中,前提是编译 过程中没有错误。 虽然从表面上看这是一个小细节,但是这个细节将妨碍你进行工作流的编译。第一个 示例中的工作流和第二个示例中的工作流之间的唯一差异是要向标记中附加 x:Class 属性。 缺少 x:Class 属性的基于 XAML 的工作流对于直接执行来说只能作为候选。仅仅是添加 x:Class 属性就意味着你必须使用 wfc.exe 来对基于 XAML 的工作流进行编译。假如你试图 直接执行你的基于 XML 的工作流的话,其结果是将产生一个 WorkflowValidationFailedException 异常,这是最可能出现的问题。 编译你的基于 XAML 的工作流的实例中最有意义的一件事发生在你需要把初始化的参 数传入到你的工作流的时候。直接执行基于 XAML 的工作流不能够接受初始化的参数。为了 演示这些,我们完全回到第一章“WF 简介”中,然后重新创建一个邮政编码验证的示例应 用程序。但是,我们将在 XML 中宿主该工作流而不是直接用 C#代码的方式。我们将使用一 个代码后置文件,因为我们还有条件要去进行判定,还有代码要去执行。 备注:你 能 直 接 使用 x:Code 元素来把代码直接放进 XML 的标记中。假如你对这些感兴 趣的话,可看看 msdn2.microsoft.com/en-gb/library/ms750494.aspx。 创建一个编译了 XML 的接受初始化参数的新工作流应用程序 1.在已经下载的本章源代码中打开 PCodeXaml 目录中的解决方案。 2.和前面的步骤一样,添加一个新的 Workflow1.xml 文件。你不需要改变该文件的编 译器设置。事实上,一旦基于 XAML 的工作流完成并编译后,我们将从项目中移除这个文件 (也包括它的代码后置文件),以便防止疏忽而导致编译器产生警告信息。 3.向 Workflow1.xml 文件中添加下面的 XML 内容并保存它: 4.在解决方案资源管理器中把 Workflow1.xml 的文件扩展名从.xml 修改为.xoml 并保 存该文件。 5.接下来添加代码后置文件。在解决方案资源管理器中的 PCodeXaml 项目的名称上单 击鼠标右键,选择“添加”,然后选择“类”。在“名称”中输入 Workflow1.xoml.cs,最 后点击“添加”。 6.向文件中添加下面的代码,完全替换 Visual Studio 为你添加的内容。然后保存这 个你刚刚创建的文件。 using System; using System.Workflow.Activities; using System.Text.RegularExpressions; public partial class Workflow1 : SequentialWorkflowActivity { private string _code = ""; public string PostalCode { get { return _code; } set { _code = value; } } private void EvaluatePostalCode(object sender, ConditionalEventArgs e) { string USCode = @"^(\d{5}$)|(\d{5}$\-\d{4}$)"; string CanadianCode = @"[ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d"; e.Result = (Regex.IsMatch(_code, USCode) || Regex.IsMatch(_code, CanadianCode)); } private void PostalCodeValid(object sender, EventArgs e) { Console.WriteLine("The postal code {0} is valid.", _code); } private void PostalCodeInvalid(object sender, EventArgs e) { Console.WriteLine("The postal code {0} is *invalid*.", _code); } } 备注:这些代码应该看起来比较熟悉。我是完全从第 1 章中的 PCodeFlow 应用程序和 工作流中的原始代码中复制粘贴过来的。 7.选择“文件”菜单中的“全部保存”,或者按下 Ctrl+Shift+S。 8.打开一个命令提示符窗口,在命令提示符中输入:cd \Workflow\Chapter16\PCodeXaml\PCodeXaml,然后按下 Enter 改变当前的工作目录。 9.在命令提示符中输入:"C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\Wfc.exe" workflow1.xoml workflow1.xoml.cs,然后按下回车键。 10.工作流编译器会把 XML 和 C#文件都汇集到一起。当它完成编译后,它会再次生成 一个名称为 workflow1.dll 的动态链接库,你 现 在 应对它进行引用。在解决方案资源管理器 中的 PCodeXaml 项目的名称上单击右键,然后选择“添加引用”。在“添加引用”对话框弹 出后,选择“浏览”选项卡,从列表中选中 workflow1.dll,最后点击“确定”。 11.回到主应用程序,选中 Program.cs 文件进行修改。在 该 文件中找到下面这行代码: Console.WriteLine("Waiting for workflow completion."); 12.在刚刚找到的代码下添加如下这些代码: // Create the execution parameters Dictionary parms = new Dictionary(); parms.Add("PostalCode", args.Length > 0 ? args[0] : ""); // Create the workflow instance. WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(Workflow1), parms); // Start the workflow instance. instance.Start(); 13.这个项目有个小问题。你 引 用了 workflow1.dll 程序集,但是在代码后置文件中也 存在有创建该 Workflow1 类的代码。这 就 造 成 了 存 在 两 个 名称都为 Workflow1 的类实例的冲 突。因为你想在已编译的程序集中进行类的定义,因此需要在解决方案资源管理器中右键点 击 Workflow1.xoml 和 Workflow.xoml.cs 文件,然后选择“从项目中排除”来移除它们。 14.编译该解决方案,纠正任何可能出现的错误。 15.为了测试该应用程序,我们将使用命令提示符窗口。把 当 前目录更改到 Debug 目录 (假如你编译应用程序的模式使用的是 Release 模式的话,则要更改到 Release)。为此, 在命令提示符中输入:cd bin\Debug 然后按下 Enter 回车键(假如你以 Release 模式编译 的话需要把 Debug 改为 Release)。 16.在命令提示符中,输入 PCodeXaml 12345 并按下回车键。你会看到下面这些应用程 序的输出结果: 17。为了试试让程序输出否定的结果,在命令提示符中输入 PCodeXaml 1234x 并按下 Enter 回车键。你会得到下面的输出结果: 在本章我们将创建最后一个示例,它涉及到一个让我们能对 XmlnsDefinition 进行演 练的自定义的类 创建一个新的带有自定义活动的基于 XAML 的工作流应用程序 1.在已经下载的本章源代码中打开 XmlnsDefFlow 目录中的解决方案。 2.添加一个新的名称为 Workflow1.xml 的文件。但是你不需要更改该文件的编译器设 置或者对它进行重新命名。 3.在 Workflow1.xml 文件中添加下面的 XML 内容,然后进行保存: 4.在解决方案资源管理器中把该文件的扩展名从.xml 改为.xoml,然后忽略弹出的警 告信息,最后保存该文件。工作流现在就完成了,但注意要在 XML 标记中引用 PrintMessageActivity,它是你需要去创建的一个新的自定义活动。为 此 ,在解决方案资源 管理器中添加一个新项目,从 Workflow 项目类型中选择“工作流程活动库”,在项目的名 称中输入 XmlnsDefLib。 5.Visual Studio 会创建一个名称为 Activity1 的工作流活动。在解决方案资源管理 器中把 Activity1.cs 文件的名称重命名为 PrintMessageActivity.cs。 6.在代码编辑视图中打开该活动的源文件准备进行编辑。 7.该活动当前派生于 SequenceActivity。现需要修改该活动派生的基类为 Activity。 该类定义应如下所示: public partial class PrintMessageActivity: Activity 8.在构造器的下面添加如下这些代码: protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { // Print message to screen Console.WriteLine(this.Message); return ActivityExecutionStatus.Closed; } public static DependencyProperty MessageProperty = DependencyProperty.Register("Message", typeof(System.String), typeof(XmlnsDefLib.PrintMessageActivity)); public string Message { get { return ((System.String)(base.GetValue( XmlnsDefLib.PrintMessageActivity.MessageProperty))); } set { base.SetValue( XmlnsDefLib.PrintMessageActivity.MessageProperty, value); } } 9.现在添加 XmlnsDefinition 特性。就在命名空间声明的前面,插入这些代码: [assembly: XmlnsDefinition("urn:PrintMessage", "XmlnsDefLib")] 备注:假如你正在一个大应用程序中使用该活动,或者要分发给客户或者其它外部用 户使用,可以使用包含你公司名称的命名空间 URI、产品组、项目或者其它典型的唯一的值 来避免命名空间的歧义。当使用 XML 工作时,这通常被认为是最佳实践。 10.编译 XmlnsDefLib 项目以便生成一个能被工作流编译器引用的 DLL。 11.尽管我们将执行的是一个基于 XAML 的工作流,但工作流运行时仍然需要去访问你 刚刚创建的 PrintMessage 活动。因此,需要为 XmlnsDefFlow 项目添加对 XmlnsDefLib 的项 目级引用。 12.和前面的示例应用程序一样,打开一个命令提示符窗口。 13.在命令提示符中输入 cd \Workflow\Chapter16\XmlnsDefFlow\XmlnsDefFlow,然 后按下 Enter 回车键来改变当前的工作目录。Workflow1.xoml 文件现在就可在该目录下直 接进行访问。 14.在目录提示符中输入"C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\Wfc.exe" workflow1.xoml /r:..\XmlnsDefLib\bin\Debug\XmlnsDefLib.dll,然后按下 Enter 回车键。和前面一样, 如果你把 Windows SDK 安装到了其它地方,你需要使用你所安装的目录去执行 wfc.exe,并 且,假如你以 Release 的方式来编译 XmlnsDefLib 项目的话,你 也需要使用 Release 来替换 Debug。 15.工作流编译器执行时应该不会出现错误。它会在和 Workflow1.xoml 的相同目录下 生成一个动态链接库 workflow1.dll。你现在需要在你的主应用程序中引用该库,因为它包 含了你在第三步创建的 XML 文件中所定义的工作流。 16.随着工作流的完成,我们要回到主应用程序中。在解决方案资源管理器中选中 Program.cx 文件,假如它没有打开的话需要打开它。查看该文件,找到下面这行代码: Console.WriteLine("Waiting for workflow completion."); 17.在你刚刚找到的代码下添加如下这些代码: // Create the workflow instance. WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(Workflow1)); // Start the workflow instance. instance.Start(); 18.编译本解决方案,纠正任何出现的编译错误。 19.按下 Ctrl+F5 或者 F5 执行该应用程序。你将看到如下的这些输出结果。 备注:即使我们这里创建了四个应用程序,我也没有完全清楚地说明声明式工作流定 义方面的所有相关内容。假如你对这些感兴趣的话,你可在 MSDN 上找到大量的论文: msdn.microsoft.com/msdnmag/issues/06/01/windowsworkflowfoundation/。

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档

相关文档