您的位置:首页 > 其它

Windows Workflow开发演练

2007-05-16 10:13 197 查看
[align=center]Windows Workflow开发演练[/align]

1 入手... 2
1.1 工作流项目模板... 2
1.2 最基本的顺序模型... 2
1.3 通过参数传递数据... 4
1.4 实现自定义活动... 8
2 提高... 12
2.1 通过工作流实现远程对象的业务... 12
2.1.1 实现基本的远程对象... 12
2.1.2 对远程对象改进... 16
2.1.3 通过工作流实现远程对象的业务逻辑... 20
2.2 调用外部方法... 28
2.2.1 简单的无参无返回值调用... 29
2.2.2 传递参数... 32
2.2.3 传递返回值... 35
2.3 处理外部事件... 37
2.3.1 搭建解决方案,进行第一次尝试... 38
2.3.2 触发事件... 44
2.3.3 还没明白... 47
2.3.4 传递参数... 47
2.4 建立状态机工作流... 51
3 一个综合演练... 51
3.1 规划并建立解决方案、项目... 51
3.2 实现报告提交部分... 55
3.3 优化程序,使Report状态改变后自动更新客户端... 60
3.4 完成自动审批业务... 60
3.5 完成管理者审批业务... 63
3.6 总结... 63
4 Windows Workflow深入知识... 63
4.1 Workflow的复用... 63
4.2 Windows Workflow与数据库... 63
4.3 Windows Workflow与Web Service. 63
4.4 Windows Workflow的可再发行设计工具... 63
4.5 Windows Workflow宿主程序... 63
4.6 管理与监控... 63
5 一个CMMI流程演练... 63
5.1 需求... 63
5.2 设计... 63
5.3 实现... 63
6 结束语... 63
7 参考... 64

1 入手
1.1 工作流项目模板
Visual Studio 2005 中的工作流项目类型
l 顺序工作流控制台应用程序 (Sequential Workflow Console Application):创建用于生成工作流的项目,该工作流包含一个默认的顺序工作流和一个控制台测试宿主应用程序。
l 顺序工作流库 (Sequential Workflow Library):创建用于以库的形式生成顺序工作流的项目。
l 工作流活动库 (Workflow Activity Library):创建一个用来创建活动的库的项目,以后可以将其作为工作流应用程序中的构造块重用。
l 状态机控制台应用程序 (State Machine Console Application):创建用于生成状态机工作流和控制台宿主应用程序的项目。
l 状态机工作流库 (State Machine Workflow Library):创建用于以库的形式生成状态机工作流的项目。
1.2 最基本的顺序模型
这个例子我们完成一个最基础的工作流,在控制台上显示一些此工作流自身的信息。
这个练习我们学习工作流相关的基本操作。

新建项目,选择“顺序工作流控制台应用程序”:



从工具箱中拖一个

到设计器:



填写代码:
选择codeActivity1,点属性页,ExecuteCode属性输入方法名称showInfo,双击方法名,Visual studio自动生成方法代码框架,



填写代码:
[align=left] public sealed partial class Workflow1: SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left]……[/align]
[align=left] private void showInfo(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] CodeActivity c = (CodeActivity)sender;[/align]
[align=left] Console.WriteLine("Hello, from '{0}'./nI'm an instance of the {1} class.",[/align]
[align=left] c.Name, c.ToString());[/align]
[align=left] Console.ReadLine();[/align]
[align=left] }[/align]
}
运行可以看到:



1.3 通过参数传递数据
这个例子我们实现一个工作流,弹出消息框,显示从主程序接收到的数据。
这个练习我们学习如何通过参数传递数据到工作流。

让我们继续分析并修改该工作流,以使其在实例化以后接收和使用数据。有两种在实例化工作流以后使其接收数据的常规方法:参数和事件。如果选择使用参数,则需要在可视化设计器中手动定义参数名称和类型的列表。如果选择使用事件,则需要创建并添加一个自定义活动(该活动充当在工作流模型中的某个位置介入的外部源),并且传入一些数据。首先我们学习参数的使用,后面我们将说明基于事件的方法。
首先为Workflow1添加属性FirstNameLastName
[align=left] public sealed partial class Workflow1: SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left] ……[/align]
[align=left] private string _FirstName;[/align]
[align=left] public string FirstName[/align]
[align=left] {[/align]
[align=left] get { return _FirstName; }[/align]
[align=left] set { _FirstName = value; }[/align]
[align=left] }[/align]
[align=left] //{[/align]
[align=left] // 数据可以保存到UserData中,如下:[/align]
[align=left] // get { return (string)UserData["FirstName"];}[/align]
[align=left] // set { UserData["FirstName"] = value; }[/align]
[align=left] //}[/align]
[align=left] [/align]
[align=left] private string _LastName;[/align]
[align=left] public string LastName[/align]
[align=left] {[/align]
[align=left] get { return _LastName; }[/align]
[align=left] set { _LastName = value; }[/align]
[align=left] }[/align]
……
}

在解决方案中新建一个windows 应用程序WinFormHost,设计主窗体如下:



添加引用:



生成tbStartWorkflow_Click事件并填写代码(代码可以从WorkflowConsoleApplicationprogram.cs中复制得到):
[align=left]using System.Windows.Forms;[/align]
[align=left]using System.Threading;[/align]
[align=left]using System.Workflow.Runtime;[/align]
[align=left]using System.Workflow.Runtime.Hosting;[/align]
[align=left] [/align]
[align=left]namespace WinFormHost[/align]
[align=left]{[/align]
[align=left] public partial class Form1 : Form[/align]
[align=left] {[/align]
[align=left]……[/align]
[align=left] private void tbStartWorkflow_Click(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())[/align]
[align=left] {[/align]
[align=left] AutoResetEvent waitHandle = new AutoResetEvent(false);[/align]
[align=left] workflowRuntime.WorkflowCompleted += delegate(object sender1, WorkflowCompletedEventArgs e1) { waitHandle.Set(); };[/align]
[align=left] workflowRuntime.WorkflowTerminated += delegate(object sender1, WorkflowTerminatedEventArgs e1)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(e1.Exception.Message);[/align]
[align=left] waitHandle.Set();[/align]
[align=left] };[/align]
[align=left] [/align]
[align=left] Dictionary<string, object> parameters = new Dictionary<string, object>();[/align]
[align=left] parameters.Add("FirstName", tbFirstName.Text);[/align]
[align=left] parameters.Add("LastName", tbLastName.Text);[/align]
[align=left] WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(WorkflowConsoleApplication6.Workflow1), parameters);[/align]
[align=left] instance.Start();[/align]
[align=left] [/align]
[align=left] waitHandle.WaitOne();[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] }[/align]
}
修改Workflow1的代码如下:
[align=left] public sealed partial class Workflow1: SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left] ……[/align]
[align=left] private void showInfo(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] System.Windows.Forms.MessageBox.Show("Welcome, " + FirstName + " " + LastName);[/align]
[align=left] }[/align]
[align=left] }[/align]
运行WinFormHost:



1.4 实现自定义活动
这个例子中我们写一个自动发送Mail的工作流,其中发送Mail由我们自定义的一个Activity实现。
这个练习我们学习如何实现自定义活动。

首先新建一个工作流Activity库:SendMail



改名:



定义属性:ToFromSubjectBodyHost,改写方法Execute:
[align=left] public partial class SendMailActivity: SequenceActivity[/align]
[align=left] {[/align]
[align=left] public String To[/align]
[align=left] {[/align]
[align=left] get { return _To; } set { _To = value; }[/align]
[align=left] }[/align]
[align=left] public String From[/align]
[align=left] {……}[/align]
[align=left] public String Subject[/align]
[align=left] {……}[/align]
[align=left] public String Body[/align]
[align=left] {……}[/align]
[align=left] public String Host[/align]
[align=left] {……}[/align]
[align=left] [/align]
[align=left] protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)[/align]
[align=left] {[/align]
[align=left] MailAddress toAddress = new MailAddress(To);[/align]
[align=left] MailAddress fromAddress = new MailAddress(From);[/align]
[align=left] [/align]
[align=left] MailAddressCollection addresses = new MailAddressCollection();[/align]
[align=left] addresses.Add(toAddress);[/align]
[align=left] [/align]
[align=left] MailMessage msg = new MailMessage(fromAddress, toAddress);[/align]
[align=left] msg.Subject = Subject;[/align]
[align=left] msg.Body = Body;[/align]
[align=left] [/align]
[align=left] SmtpClient mail = new SmtpClient(Host);[/align]
[align=left] mail.Credentials = new NetworkCredential("hyhzj@public.hy.js.cn", "保密,呵呵");[/align]
[align=left]//SMTP服务器需要身份验证[/align]
[align=left] mail.Send(msg);[/align]
[align=left] return ActivityExecutionStatus.Closed;[/align]
[align=left] }[/align]
[align=left]}[/align]
[align=left]建立工作流,调用自定义Activity[/align]
[align=left]解决方案中添加一个顺序工作流控制台项目:WorkflowConsoleApplicationSendMail,工具箱中我们能看到SendMailActivity,拖到工作流设计对应区域,注意SendMail与WorkflowConsoleApplicationSendMail在同一解决项目中,否则请做:工具箱,右键-〉添加选项卡-〉取名,右键选项-〉选择SendMail程序集-〉确定:[/align]



[align=left]设置SendMailActivity属性:[/align]



[align=left]执行工作流,查看收件箱:[/align]



[align=left]可以重写ValidateProperties方法来完成我们的验证。活动的ValidateProperties 方法是在进行编译时执行的验证方法。验证我们输入属性格式是否是正确。[/align]

2 提高
你必须Remoting技术,不熟悉参看文档:Remoting技术概述.Doc
2.1 通过工作流实现远程对象的业务
这个练习我们实现一个对用户传递来的数据进行判断的业务,用户提交一个费用报告,程序根据费用数值的大小来决定Approve还是Reject。
这个练习中我们将学习用工作流实现业务、将业务发布为远程对象。

前面我们的练习,总是有类似如下的代码:
[align=left]WorkflowRuntime workflowRuntime = new WorkflowRuntime();[/align]
[align=left]WorkflowInstance instance = workflowRuntime.CreateWorkflow(……);[/align]
[align=left]instance.Start();[/align]
[align=left]工作流运行时引擎、工作流实例总是在本地建立,这个练习中我们将实现在远程机器上建立工作流运行时引擎、工作流实例,然后发布为一个远程对象,通过远程对象的包装对用户屏蔽业务的实现。[/align]
2.1.1 实现基本的远程对象
[align=left]第一步:建立空解决方案:RemotingWorkflow1[/align]
[align=left]第二步:添加一个类库项目:ExpenseReportProj[/align]
[align=left]添加类ExpenseReport用来在Client与Service端交换数据,注意必须标注Serializable属性,因为Remoting要求Client与Service端交换的引用类型数据必须能够序列化。[/align]
[align=left]ExpenseReport.cs[/align]
[align=left]namespace ExpenseReportProj[/align]
[align=left]{[/align]
[align=left] [Serializable][/align]
[align=left] public class ExpenseReport[/align]
[align=left] {[/align]
[align=left] public string EmployeeId = "";[/align]
[align=left] public int Amount = 0;[/align]
[align=left] public DateTime SubmittedTime;[/align]
[align=left] }[/align]
[align=left]}[/align]
[align=left]第三步:添加控制台应用程序RemotingServiceProj,并设置为启动项目[/align]
[align=left]Program.cs[/align]
[align=left]using System.Runtime.Remoting;[/align]
[align=left] [/align]
[align=left]namespace RemotingServiceProj[/align]
[align=left]{[/align]
[align=left] class Program[/align]
[align=left] {[/align]
[align=left] static void Main(string[] args)[/align]
[align=left] {[/align]
[align=left] RemotingConfiguration.Configure("serverCfg.xml", false);[/align]
[align=left] [/align]
[align=left] Console.WriteLine("Press Enter to terminate...");[/align]
[align=left] Console.ReadLine();[/align]
[align=left] }[/align]
[align=left]}[/align]
[align=left]}[/align]
[align=left] [/align]
[align=left]RemotingService.cs[/align]
[align=left]using ExpenseReportProj;[/align]
[align=left] [/align]
[align=left]namespace RemotingServiceProj[/align]
[align=left]{[/align]
[align=left] public class RemotingService : MarshalByRefObject [/align]
[align=left] {[/align]
[align=left] public void SubmitExpenseReport(ExpenseReport report)[/align]
[align=left] {[/align]
[align=left] String dispInfo = report.EmployeeId +" " + report.SubmittedTime [/align]
[align=left] + " Submit: " + report.Amount;[/align]
[align=left] if (report.Amount > 1000)[/align]
[align=left] Console.WriteLine(dispInfo + " Rejected");[/align]
[align=left] else[/align]
[align=left] Console.WriteLine(dispInfo + " Approved");[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left]}[/align]
[align=left] [/align]
[align=left]serverCfg.xml:注意要复制到输出目录[/align]
[align=left]<?xmlversion="1.0"encoding="utf-8" ?>[/align]
[align=left]<configuration>[/align]
[align=left] <system.runtime.remoting>[/align]
[align=left] <application>[/align]
[align=left] <service>[/align]
[align=left] <wellknownmode="SingleCall"type="RemotingServiceProj.RemotingService, [/align]
[align=left]RemotingServiceProj" //type: 类全名(含全部名字空间),程序集名[/align]
[align=left] objectUri="RemotingWorkflow1" /> [/align]
[align=left] </service>[/align]
[align=left] <channels>[/align]
[align=left] <channelref="tcp server"port="1234" />[/align]
[align=left] </channels>[/align]
[align=left] </application>[/align]
[align=left] </system.runtime.remoting>[/align]
[align=left]</configuration>[/align]
[align=left][/align]
[align=left]第四步:添加一个Windows 应用程序:TestApplication[/align]
[align=left]文件:Form1.cs[/align]
[align=left]设计界面:[/align]



[align=left]代码:[/align]
[align=left]using System.Runtime.Remoting;[/align]
[align=left]using ExpenseReportProj;[/align]
[align=left]using RemotingServiceProj;[/align]
[align=left] [/align]
[align=left]namespace TestApplication[/align]
[align=left]{[/align]
[align=left] public partial class Form1 : Form[/align]
[align=left] {[/align]
[align=left] protected RemotingService Service;[/align]
[align=left] public Form1()[/align]
[align=left] {[/align]
[align=left] InitializeComponent();[/align]
[align=left] [/align]
[align=left] RemotingConfiguration.Configure("ClientCfg.xml", false); [/align]
[align=left] Service = (RemotingService)Activator.GetObject(typeof(RemotingService), "tcp://localhost:1234/RemotingWorkflow1");[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] private void button1_Click(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] ExpenseReport Report = new ExpenseReport();[/align]
[align=left] Report.EmployeeId = EmployeeId.Text;[/align]
[align=left] Report.Amount = Convert.ToInt32(Amount.Text);[/align]
[align=left] Report.SubmittedTime = DateTime.Now;[/align]
[align=left] Service.SubmitExpenseReport(Report);[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left]}[/align]
[align=left]ClientCfg.xml:注意要复制到输出目录[/align]
[align=left]<?xmlversion="1.0"encoding="utf-8" ?>[/align]
[align=left]<configuration>[/align]
[align=left] <system.runtime.remoting>[/align]
[align=left] <application>[/align]
[align=left] <client>[/align]
[align=left] <wellknowntype="RemotingServiceProj.RemotingService, RemotingService"[/align]
[align=left]url="RemotingWorkflow1" /> //type: 类全名(含全部名字空间),程序集名[/align]
[align=left] </client>[/align]
[align=left] <channels>[/align]
[align=left] <channelref="tcp client"port="1234" />[/align]
[align=left] </channels>[/align]
[align=left] </application>[/align]
[align=left] </system.runtime.remoting>[/align]
[align=left]</configuration>[/align]
[align=left]解决方案资源:[/align]



[align=left]项目引用关系:[/align]

[align=center][b]TestApplication[/align]

[align=center]RemotingServiceProj[/align]

[align=center]ExpenseReportProj[/align]

[/b]
[align=left]类图:[/align]



[align=left]运行效果:[/align]
[align=left]我们修改Form1.cs代码[/align]
[align=left]Service = (RemotingService)Activator.GetObject(typeof(RemotingService), "tcp://localhost:1234/RemotingWorkflow1");[/align]
[align=left]中的URL到指定的机器,就可以让服务程序与客户程序在不同的机器运行。[/align]



[align=left]小提示:[/align]
[align=left] 调试时Visual studio可以启动多个项目的:运行,启动了“启动项目”,然后在某个你希望启动的项目上右击鼠标,选择“调试”,点“启动新实例”即可。[/align]
2.1.2 对远程对象改进
首先从前面的设计我们可以发现程序的客户端必须引用实现服务的程序集RemotingServiceProj并使用实现服务的类RemotingService,服务的任何改动都需要重新编译客户端,实际上客户端仅需要知道一个描述服务的接口。所以我们进行改进:
客户服务器交换数据定义项目ExpenseReportProj定义服务接口:
文件:IremotingService.cs
[align=left]namespace ExpenseReportProj[/align]
[align=left]{[/align]
[align=left] public interface IRemotingService[/align]
[align=left] {[/align]
[align=left] void SubmitExpenseReport(ExpenseReport report);[/align]
[align=left] }[/align]
}

服务器端RemotingServiceProj的RemotingService实现IremotingService接口:
文件:RemotingService.cs
[align=left]namespace RemotingServiceProj[/align]
[align=left]{[/align]
[align=left]public class RemotingService : MarshalByRefObject, IRemotingService [/align]
[align=left]{[/align]
[align=left] ……[/align]
[align=left]}[/align]
}

客户端TestApplication获得服务接口IRemotingService
文件:Form1.cs
[align=left]namespace TestApplication[/align]
[align=left]{[/align]
[align=left] public partial class Form1 : Form[/align]
[align=left] {[/align]
[align=left] protected IRemotingService Service;[/align]
[align=left] public Form1()[/align]
[align=left] {[/align]
[align=left] InitializeComponent();[/align]
[align=left] [/align]
[align=left] RemotingConfiguration.Configure("ClientCfg.xml", false);[/align]
[align=left] Service = (IRemotingService)Activator.GetObject(typeof(IRemotingService), "tcp://localhost:1234/RemotingWorkflow1");[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left]……[/align]
[align=left]}[/align]
}

更改客户端配置:
文件:ClientCfg.xml
[align=left]……[/align]
[align=left] <client>[/align]
[align=left] <wellknowntype="ExpenseReportProj.IRemotingService, ExpenseReportProj"url="RemotingWorkflow1" />[/align]
[align=left]</client>[/align]
……

移除TestApplication对RemotingServiceProj的引用,现在我们可以自由改变服务接口的实现而不需要改客户端程序。类图如下:



文件Form1.cs中我们对访问服务器的URL写死在代码中了,我们希望通过配置文件来指定服务器,所以做如下改进:
[align=left]namespace TestApplication[/align]
[align=left]{[/align]
[align=left] public partial class Form1 : Form[/align]
[align=left] {[/align]
[align=left] public Form1()[/align]
[align=left] {[/align]
[align=left] Service = (IRemotingService)Activator.GetObject(typeof(IRemotingService), "tcp://localhost:1234/RemotingWorkflow1");[/align]
[align=left] }[/align]
[align=left]……[/align]
[align=left]}[/align]
}

Form1.cs做如下改进,注意添加了方法GetURLFromRemotingClientConfig
[align=left]using ExpenseReportProj;[/align]
[align=left]using System.Xml;[/align]
[align=left] [/align]
[align=left]namespace TestApplication[/align]
[align=left]{[/align]
[align=left] public partial class Form1 : Form[/align]
[align=left] {[/align]
[align=left] protected IRemotingService Service;[/align]
[align=left] public Form1()[/align]
[align=left] {[/align]
[align=left] InitializeComponent();[/align]
[align=left] [/align]
[align=left] String ClientCfgFileName = "ClientCfg.xml";[/align]
[align=left] RemotingConfiguration.Configure(ClientCfgFileName, false);[/align]
[align=left] Service = (IRemotingService)Activator.GetObject(typeof(IRemotingService), [/align]
[align=left] GetURLFromRemotingClientConfig(ClientCfgFileName));[/align]
[align=left] }[/align]
[align=left] …….[/align]
[align=left] private String GetURLFromRemotingClientConfig (String ClientCfgFileName)[/align]
[align=left] {[/align]
[align=left] string serviceValue = "", refValue = "", portValue = "", urlValue = "";[/align]
[align=left] [/align]
[align=left] using (XmlReader clientXml = XmlReader.Create(ClientCfgFileName))[/align]
[align=left] while (clientXml.Read())[/align]
[align=left] for (int i = 0; i < clientXml.AttributeCount; i++)[/align]
[align=left] {[/align]
[align=left] clientXml.MoveToAttribute(i);[/align]
[align=left] switch (clientXml.Name.ToLower())[/align]
[align=left] {[/align]
[align=left] case "service":[/align]
[align=left] serviceValue = clientXml.Value;[/align]
[align=left] break;[/align]
[align=left] case "ref":[/align]
[align=left] refValue = clientXml.Value;[/align]
[align=left] break;[/align]
[align=left] case "port":[/align]
[align=left] portValue = clientXml.Value;[/align]
[align=left] break;[/align]
[align=left] case "url":[/align]
[align=left] urlValue = clientXml.Value;[/align]
[align=left] break;[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] if (refValue.ToUpper().IndexOf("TCP") == 0)[/align]
[align=left] return "tcp://" + serviceValue + ":" + portValue + "/" + urlValue;[/align]
[align=left] else[/align]
[align=left] return "";[/align]
[align=left] }[/align]
[align=left] }[/align]
}

更改客户端配置:
文件:ClientCfg.xml
[align=left]<?xmlversion="1.0"encoding="utf-8" ?>[/align]
[align=left]<configuration>[/align]
[align=left] <system.runtime.remoting>[/align]
[align=left] <application>[/align]
[align=left] <client>[/align]
[align=left] <wellknowntype="ExpenseReportProj.IRemotingService, ExpenseReportProj"url="RemotingWorkflow1" />[/align]
[align=left] </client>[/align]
[align=left] <channels>[/align]
[align=left] <channelref="tcp client"service="localhost" port="1234" />[/align]
[align=left] </channels>[/align]
[align=left] </application>[/align]
[align=left] </system.runtime.remoting>[/align]
</configuration>

通过前面两个练习,我们熟悉了Remoting技术,建立了一个结构良好的Remoting应用,后面我们将在此基础上用工作流技术实现这一应用的服务。
2.1.3 通过工作流实现远程对象的业务逻辑
在解决方案中添加一个顺序工作流控制台应用程序:WorkflowExpenseReportServiceProj
[align=left]小提示:[/align]
我们需要的是一个类库,实际上控制台应用程序、windows应用程序都可以直接当作类库使用的,我们这儿建立控制台应用程序为了在开发时更方便地进行测试。
添加对ExpenseReportProj的引用
删除program.cs
添加类WorkflowExpenseReportService
WorkflowExpenseReportService.cs
[align=left]using ExpenseReportProj;[/align]
[align=left] [/align]
[align=left]namespace WorkflowExpenseReportServiceProj[/align]
[align=left]{[/align]
[align=left] public class Test[/align]
[align=left] {[/align]
[align=left] static void Main(string[] args)[/align]
[align=left] {[/align]
[align=left] WorkflowExpenseReportService Service = new WorkflowExpenseReportService();[/align]
[align=left] ExpenseReport report = new ExpenseReport();[/align]
[align=left] report.EmployeeId = "当作控制台程序测试下";[/align]
[align=left] report.SubmittedTime = DateTime.Now;[/align]
[align=left] report.Amount = 100;[/align]
[align=left] [/align]
[align=left] Service.SubmitExpenseReport(report);[/align]
[align=left] Console.ReadLine(); [/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public class WorkflowExpenseReportService : MarshalByRefObject, IRemotingService[/align]
[align=left] {[/align]
[align=left] public void SubmitExpenseReport(ExpenseReport report)[/align]
[align=left] {[/align]
[align=left] using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())[/align]
[align=left] {[/align]
[align=left] AutoResetEvent waitHandle = new AutoResetEvent(false);[/align]
[align=left] workflowRuntime.WorkflowCompleted += delegate(object sender, [/align]
[align=left] WorkflowCompletedEventArgs e) { waitHandle.Set(); };[/align]
[align=left] workflowRuntime.WorkflowTerminated += delegate(object sender, [/align]
[align=left] WorkflowTerminatedEventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(e.Exception.Message);[/align]
[align=left] waitHandle.Set();[/align]
[align=left] };[/align]
[align=left] [/align]
[align=left] Dictionary<string, object> parameters = new Dictionary<string, object>();[/align]
[align=left] parameters.Add("Report", report);[/align]
[align=left] WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(Workflow1), parameters);[/align]
[align=left] instance.Start();[/align]
[align=left] [/align]
[align=left] waitHandle.WaitOne();[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left]}[/align]
[align=left]说明:[/align]
[align=left]WorkflowExpenseReportService类做为远程服务,必须派生自MarshalByRefObject,且实现我们本解决方案需要的服务接口IremotingService,在SubmitExpenseReport方法中传递参数report到工作流,如何传递参数前面练习做过。[/align]
Main方法中我们使工作流能够直接测试。
设计工作流:
双击Workflow1.cs,打开设计器,从工具箱拖一个code activity到设计器:



选中设计器中的codeActivity1,点属性,为ExecuteCode属性输入此Activity执行的方法名ShowReportInfo,并双击,Visual studio会自动生成对应方法的代码框架:



编辑Workflow1对应的代码,添加属性public ExpenseReport Report,在ShowReportInfo方法中显示属性Report的信息:
文件Workflow1.cs
[align=left]using ExpenseReportProj;[/align]
[align=left] [/align]
[align=left]namespace WorkflowExpenseReportServiceProj[/align]
[align=left]{[/align]
[align=left] public sealed partial class Workflow1: SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left] public Workflow1()[/align]
[align=left] {[/align]
[align=left] InitializeComponent();[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] ExpenseReport _Report;[/align]
[align=left] public ExpenseReport Report[/align]
[align=left] {[/align]
[align=left] get { return _Report; }[/align]
[align=left] set { _Report = value; }[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] private void ShowReportInfo(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(Report.EmployeeId);[/align]
[align=left] }[/align]
[align=left] }[/align]
}
运行此工作流控制台程序WorkflowExpenseReportServiceProj,应该看到:



第一步我们完成了建立一个实现IRemotingService 接口的可以作为Remoting服务的类WorkflowExpenseReportService, 并对它独立地完成了测试。
现在我们把WorkflowExpenseReportService当作远程服务进行测试:
首先修改RemotingServiceProj的配置文件ServerCfg.xml:
[align=left]<?xmlversion="1.0"encoding="utf-8" ?>[/align]
[align=left]<configuration>[/align]
[align=left] <system.runtime.remoting>[/align]
[align=left] <application>[/align]
[align=left] <service>[/align]
[align=left] <!--<wellknown mode="SingleCall" type="RemotingServiceProj.RemotingService, RemotingServiceProj" [/align]
[align=left] objectUri="RemotingWorkflow1" />-->[/align]
[align=left] <wellknownmode="SingleCall"type="WorkflowExpenseReportServiceProj.WorkflowExpenseReportService, WorkflowExpenseReportServiceProj"[/align]
[align=left] objectUri="RemotingWorkflow1" />[/align]
[align=left] </service>[/align]
[align=left] <channels>[/align]
[align=left] <channelref="tcp server"port="1234" />[/align]
[align=left] </channels>[/align]
[align=left] </application>[/align]
[align=left] </system.runtime.remoting>[/align]
[align=left]</configuration>[/align]
我们更改了type属性,定位到我们用工作流实现的服务业务逻辑WorkflowExpenseReportService 类。
复制程序集WorkflowExpenseReportServiceProj到RemotingServiceProj的bin/Debug目录下,运行RemotingServiceProj,运行TestApplication,测试,我们应该看到:



前面我们已经用工作流完成了远程业务对象,并进行了测试,当然业务逻辑还没有实现,业务逻辑我们可以在可视化设计器中方便地设计了,使用什么类实现远程服务只需要改个配置文件就可以了。
[align=left]小提示:[/align]
调试时我们让RemotingServiceProj引用WorkflowExpenseReportServiceProj项目,就可以不用手工复制文件了,免得调试时WorkflowExpenseReportServiceProj改了但忘记复制到RemotingServiceProj的bin/debug下。
设计业务逻辑:
修改Workflow1. ShowReportInfo方法:
[align=left] private void ShowReportInfo(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] String dispInfo = Report.EmployeeId + " " + Report.SubmittedTime[/align]
[align=left] + " Submit: " + Report.Amount;[/align]
[align=left] if (Report.Amount > 1000)[/align]
[align=left] Console.WriteLine(dispInfo + " Rejected");[/align]
[align=left] else[/align]
[align=left] Console.WriteLine(dispInfo + " Approved");[/align]
}
运行RemotingServiceProj,运行TestApplication,测试,我们应该看到:



业务逻辑已经实现。我们换种方法,通过工作流设计器设计业务逻辑:
删除codeActivity1,从工具箱拖一个IfElseActivity到设计器, 选择IfElseActivity,点红色感叹号,选择下拉菜单“Property Condition is not set”:



IfElseActivity实现工作流的条件判断,运行引擎将从左向右依次处理各分支的条件,如果某分支处理结果是true则顺着此分支执行下去,最后一个分支固定是else,不可设置条件;我们可以拖动分支改变左右顺序,用右键菜单增加分支或按delete键删除分支;分支的条件可以是一个返回boolean的方法或一个boolean表达式,通过condition属性设置。
将跳转到属性窗体并定位于condition属性,我们选择Declarative Rule Condition,定义一个规则表达式:




点condition前的+(上图红色圈处),展开condition,点ConditionName右侧‘...’(下图箭头所指):



弹出Select Condition窗体,点‘New’(红色箭头所指),弹出Rule Condition Editor窗体,输入条件表达式:this.Report.Amount > 1000:




点‘OK’,回到设计器,我们已经完成了条件判断,下面我们要完成各条件下执行的操作,我们从工具箱拖两个CodeActivity分别放到两个条件分支下:



将两个IfElse分支分别改名为RejectBranch、ElseBranch,对应的两个codeActivity分别改名为RejectActivity、ApproveActivity:



设置RejectActivity、ApproveActivity的ExecuteCode分别为RejectReport、ApproveReport,双击生成代码,编辑并删除过时的方法ShowReportInfo
Workflow1.cs
[align=left]using ExpenseReportProj;[/align]
[align=left] [/align]
[align=left]namespace WorkflowExpenseReportServiceProj[/align]
[align=left]{[/align]
[align=left] public sealed partial class Workflow1 : SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left] public Workflow1()[/align]
[align=left] {[/align]
[align=left] InitializeComponent();[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] ExpenseReport _Report;[/align]
[align=left] [/align]
[align=left] public ExpenseReport Report[/align]
[align=left] {[/align]
[align=left] get { return _Report; }[/align]
[align=left] set { _Report = value; }[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] private void RejectReport(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(Report.EmployeeId + " " + Report.SubmittedTime[/align]
[align=left] + " Submit: " + Report.Amount + " Rejected");[/align]
[align=left] }[/align]
[align=left][/align]
[align=left] private void ApproveReport(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(Report.EmployeeId + " " + Report.SubmittedTime[/align]
[align=left] + " Submit: " + Report.Amount + " Approved");[/align]
[align=left] }[/align]
[align=left] }[/align]
}
保存、编译、运行,我们将看到系统正确地实现了我们的业务逻辑。
2.2 调用外部方法
我们知道Workflows需要寄宿在一个进程中,任何的应用或者服务都可以是Workflow的宿主,WF处理空间和Host空间之间的数据交换通过ExternalDataExchangeService完成。
WF和Host之间的数据交换通过以ExternalDataExchangeAttribute修饰的接口定义,这些接口我们称为本地服务。我们在ExternalDataExchangeService中注册实现这些本地服务的对象。
CallExternalMethodActivityHandleExternalEventActivity共同实现与本地服务的双向交流。
2.2.1 简单的无参无返回值调用
一:创建顺序工作流控制台程序CallExternalMethod以及对应的解决方案。
二:在解决方案中添加windows类库ExternalDataInterface
三:让CallExternalMethod引用ExternalDataInterface
四:定义本地服务以及实现这些本地服务的类,并编译ExternalDataInterface
[align=left]using System.Workflow.Activities;[/align]
[align=left]using System.Windows.Forms;[/align]
[align=left] [/align]
[align=left]namespace ExternalDataInterface[/align]
[align=left]{[/align]
[align=left] [ExternalDataExchange][/align]
[align=left] public interface IExternal[/align]
[align=left] {[/align]
[align=left] void ExternalMethod();[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public class External: IExternal[/align]
[align=left] {[/align]
[align=left] public void ExternalMethod()[/align]
[align=left] {[/align]
[align=left] System.Windows.Forms.MessageBox.Show("External.ExternalMethod Executive");[/align]
[align=left] }[/align]
[align=left] }[/align]
}
五:注册本地服务,见下列代码中粗体部分:
[align=left]using System.Workflow.Runtime;[/align]
[align=left]using System.Workflow.Runtime.Hosting;[/align]
[align=left]using System.Workflow.Activities;[/align]
[align=left]using ExternalDataInterface;[/align]
[align=left] [/align]
[align=left]namespace CallExternalMethod[/align]
[align=left]{[/align]
[align=left] class Program[/align]
[align=left] {[/align]
[align=left] static void Main(string[] args)[/align]
[align=left] {[/align]
[align=left] using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())[/align]
[align=left] {[/align]
[align=left] AutoResetEvent waitHandle = new AutoResetEvent(false);[/align]
[align=left] [/align]
[align=left] ExternalDataExchangeService dataService = new ExternalDataExchangeService();[/align]
[align=left] workflowRuntime.AddService(dataService);[/align]
[align=left][/align]
[align=left] External external = new External();[/align]
[align=left] dataService.AddService(external);[/align]
[align=left] [/align]
[align=left] workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e) {waitHandle.Set();};[/align]
[align=left] workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(e.Exception.Message);[/align]
[align=left] waitHandle.Set();[/align]
[align=left] };[/align]
[align=left] [/align]
[align=left] workflowRuntime.CreateWorkflow(typeof(CallExternalMethod.Workflow1)).Start();[/align]
[align=left] waitHandle.WaitOne();[/align]
[align=left] Console.WriteLine("Press any key to exit...");[/align]
[align=left] Console.Read();[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left]}[/align]
六:调用本地服务中的方法:
打开CallExternalMethod的工作流Workflow1,从工具箱拖一个CallExternalMethod到设计器:



设置CallExternalMethodActivity的属性,点InterfaceType右侧省略号,见下图:



会弹出Browse and Selet a .NET Type窗体,选择我们定义的本地服务类型,如果没有列出,检查服务是否用ExternalDataExchangeAttribute修饰、CallExternalMethod是否引用ExternalDataInterface并编译ExternalDataInterface。



设置要调用的方法名:



运行,我们可以看到External.ExternalMethod方法被调用:



注意:
本地服务以ExternalDataExchangeAttribute修饰的接口定义,其中仅直接说明的方法和事件有效,如果从其它接口派生,即使父接口也是本地服务,父接口中的方法也不能被WF处理空间调用。
每一个本地服务在ExternalDataExchangeService中仅可以注册一个实现这个本地服务的对象,否则出错。
2.2.2 传递参数
一:修改本地服务的方法ExternalMethod,添加参数:
[align=left]using System.Workflow.Activities;[/align]
[align=left]using System.Windows.Forms;[/align]
[align=left] [/align]
[align=left]namespace ExternalDataInterface[/align]
[align=left]{[/align]
[align=left] [ExternalDataExchange][/align]
[align=left] public interface IExternal[/align]
[align=left] {[/align]
[align=left] void ExternalMethod(String Info);[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public class External: IExternal[/align]
[align=left] {[/align]
[align=left] public void ExternalMethod(String Info)[/align]
[align=left] {[/align]
[align=left] System.Windows.Forms.MessageBox.Show("External.ExternalMethod Executive, parameter: " + Info);[/align]
[align=left] }[/align]
[align=left] }[/align]
}
二:编译ExternalDataInterface
三:设置CallExternalMethodActivity的属性,我们看到多了属性Info



点Info右侧省略号,弹出Bind ‘Info’ to an activity’s property窗体,在此我们设置调用External.ExternalMethod方法时把Workflow1的哪一个成员(属性或字段)做为参数传递给ExternalMethod方法的参数Info。现有成员中没有合适的,我们点Bind a new member页,为Workflow1添加一个新的属性WorkflowInfo,并传递给ExternalMethod方法:



此处我们可以创建Field或Property,选择Property将使我们程序更灵活点。确定,查看Workflow1.cs的代码,在构造函数中对WorkflowInfo赋值
[align=left]using System.Workflow.Activities;[/align]
[align=left]using System.Workflow.Activities.Rules;[/align]
[align=left] [/align]
[align=left]namespace CallExternalMethod[/align]
[align=left]{[/align]
[align=left] public sealed partial class Workflow1: SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left] public Workflow1()[/align]
[align=left] {[/align]
[align=left] InitializeComponent();[/align]
[align=left] WorkflowInfo = Name + " Initialize: " + DateTime.Now.ToString();[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public static DependencyProperty WorkflowInfoProperty = DependencyProperty.Register("WorkflowInfo", typeof(System.String), typeof(CallExternalMethod.Workflow1));[/align]
[align=left][/align]
[align=left] [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)][/align]
[align=left] [BrowsableAttribute(true)][/align]
[align=left] [CategoryAttribute("Parameters")][/align]
[align=left] public String WorkflowInfo[/align]
[align=left] {[/align]
[align=left] get[/align]
[align=left] {[/align]
[align=left] return ((string)(base.GetValue(CallExternalMethod.Workflow1.WorkflowInfoProperty)));[/align]
[align=left] }[/align]
[align=left] set[/align]
[align=left] {[/align]
[align=left] base.SetValue(CallExternalMethod.Workflow1.WorkflowInfoProperty, value);[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] }[/align]
}
运行,我们可以看到External.ExternalMethod方法被调用且工作流的参数被正确传递出:



2.2.3 传递返回值
传递返回值与传递参数非常类似,首先我们修改本地服务的方法ExternalMethod,添加返回值,编译
[align=left]using System.Windows.Forms;[/align]
[align=left] [/align]
[align=left]namespace ExternalDataInterface[/align]
[align=left]{[/align]
[align=left] [ExternalDataExchange][/align]
[align=left] public interface IExternal[/align]
[align=left] {[/align]
[align=left] String ExternalMethod(String Info);[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public class External: IExternal[/align]
[align=left] {[/align]
[align=left] public String ExternalMethod(String Info)[/align]
[align=left] {[/align]
[align=left] System.Windows.Forms.MessageBox.Show("External.ExternalMethod Executive, parameter: " + Info);[/align]
[align=left] return "ExternalMethod Result";[/align]
[align=left] }[/align]
[align=left] }[/align]
}
设计工作流,我们看到CallExternalMethodActivity属性多了ReturnValue,把它绑定到Workflow1的新属性ExternalMethodResult(如何做见传递参数部分).



添加CodeActivity用来显示ExternalMethodResult:



设置codeActivity1的ExecuteCode为ShowExternalMethodResult,双击,编写代码:



[align=left]using System.Workflow.Activities.Rules;[/align]
[align=left] [/align]
[align=left]namespace CallExternalMethod[/align]
[align=left]{[/align]
[align=left] public sealed partial class Workflow1: SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left]……[/align]
[align=left] private void ShowExternalMethodResult(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(ExternalMethodResult);[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left]}[/align]
运行,我们看到返回值已经正确接收到:



2.3 处理外部事件
外部事件是工作流的驱动力,任何实际的工作流都是靠外部事件驱动它一步步完成业务的。Windows workflow的外部事件是我们学习的关键,同时也是比较复杂的,为更牢固地掌握外部事件,这一小节我们用不断尝试错误的方法进行学习,也许过于繁琐,因为我实在想不出合适的方法和例子来讲解,哭。
感觉这个练习设计的有很大的问题。
2.3.1 搭建解决方案,进行第一次尝试
一:建立顺序工作流控制台项目以及对应解决方案ListenExternalEvent,添加windows 雷库项目 LocalServicesProj,让ListenExternalEvent引用LocalServicesProj。
二:LocalServicesProj添加引用:



三:在LocalServicesProj项目中定义本地服务接口IlocalServices以及实现此接口的对象localServices:
IlocalServices.cs:
[align=left]using System;[/align]
[align=left]using System.Collections.Generic;[/align]
[align=left]using System.Text;[/align]
[align=left]using System.Workflow.Activities;[/align]
[align=left] [/align]
[align=left]namespace LocalServicesProj[/align]
[align=left]{[/align]
[align=left] public delegate void dlgExternalEvent();[/align]
[align=left] [/align]
[align=left] [ExternalDataExchange][/align]
[align=left] public interface ILocalServices[/align]
[align=left] {[/align]
[align=left] event dlgExternalEvent ExternalEvent;[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public class LocalServices : ILocalServices[/align]
[align=left] {[/align]
[align=left] public event dlgExternalEvent ExternalEvent;[/align]
[align=left] [/align]
[align=left] public void TriggerExternalEvent()[/align]
[align=left] {[/align]
[align=left] ExternalEvent();[/align]
[align=left] }[/align]
[align=left] }[/align]
}
四:Workflow1添加一个处理外部事件的HandleExternalEventActivity:



五:设置HandleExternalEventActivity1的属性,首先需要设置InterfaceType,点省略号,选择,见下图:






如果没列出LocalServicesProj,检查ListenExternalEvent是否引用了LocalServicesProj并编译LocalServicesProj。
如果LocalServicesProj中没有ILocalServices,检查IlocalServices是否用ExternalDataExchangeAttribute修饰
六:设置HandleExternalEventActivity1EventName:



七:在program.cs注册本地服务:
[align=left]using System.Threading;[/align]
[align=left]using System.Workflow.Runtime;[/align]
[align=left]using System.Workflow.Runtime.Hosting;[/align]
[align=left]using System.Workflow.Activities;[/align]
[align=left]using LocalServicesProj;[/align]
[align=left] [/align]
[align=left]namespace ListenExternalEvent[/align]
[align=left]{[/align]
[align=left] class Program[/align]
[align=left] {[/align]
[align=left] static void Main(string[] args)[/align]
[align=left] {[/align]
[align=left] using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())[/align]
[align=left] {[/align]
[align=left] AutoResetEvent waitHandle = new AutoResetEvent(false);[/align]
[align=left] [/align]
[align=left] ExternalDataExchangeService dataService = new ExternalDataExchangeService();[/align]
[align=left] workflowRuntime.AddService(dataService);[/align]
[align=left][/align]
[align=left] LocalServices localServices = new LocalServices();[/align]
[align=left] dataService.AddService(localServices);[/align]
[align=left] [/align]
[align=left] [/align]
[align=left] workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e) { waitHandle.Set(); };[/align]
[align=left] workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(e.Exception.Message);[/align]
[align=left] waitHandle.Set();[/align]
[align=left] };[/align]
[align=left] [/align]
[align=left] WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(ListenExternalEvent.Workflow1));[/align]
[align=left] instance.Start();[/align]
[align=left] [/align]
[align=left] waitHandle.WaitOne();[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] }[/align]
}
八:编译解决方案,得到错误:



原来工作流中HandleExternalEventActivity要求的事件必须是一个用ExternalDataEventArgs实例化的EventHandler范型方法,我们根据提示修改IlocalServices.cs中的IlocalServices以及LocalServices:
[align=left]namespace LocalServicesProj[/align]
[align=left]{[/align]
[align=left] //public delegate void dlgExternalEvent();[/align]
[align=left] [/align]
[align=left] [ExternalDataExchange][/align]
[align=left] public interface ILocalServices[/align]
[align=left] {[/align]
[align=left] event EventHandler<ExternalDataEventArgs> ExternalEvent;[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public class LocalServices : ILocalServices[/align]
[align=left] {[/align]
[align=left] public event EventHandler<ExternalDataEventArgs> ExternalEvent;[/align]
[align=left] [/align]
[align=left] public void TriggerExternalEvent()[/align]
[align=left] {[/align]
[align=left] ExternalEvent(null, null);[/align]
[align=left] }[/align]
[align=left] }[/align]
}
九:可以通过编译,运行,程序什么也没做,暂停,我们看到程序停在waitHandle.WaitOne()上:



十:添加两个CodeActivity,分别取名WorkflowStart、WorkflowEnd,设置它们的ExecuteCode属性,分别执行方法WriteStartInfo、WriteEndInfo,Program.cs适当修改,这样我们就可以从控制台上看到工作流的开始、结束了:



[align=left]Workflow1.cs:[/align]
[align=left]namespace ListenExternalEvent[/align]
[align=left]{[/align]
[align=left] public sealed partial class Workflow1: SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left] public Workflow1()[/align]
[align=left] {[/align]
[align=left] InitializeComponent();[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] private void WriteStartInfo(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(this.WorkflowInstanceId.ToString() + " Start");[/align]
[align=left] }[/align]
[align=left][/align]
[align=left] private void WriteEndInfo(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(this.WorkflowInstanceId.ToString() + " End");[/align]
[align=left] }[/align]
[align=left] }[/align]
}

Program.cs:
[align=left]namespace ListenExternalEvent[/align]
[align=left]{[/align]
[align=left] class Program[/align]
[align=left] {[/align]
[align=left] static void Main(string[] args)[/align]
[align=left] {[/align]
[align=left] using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())[/align]
[align=left] {[/align]
[align=left]……[/align]
[align=left] waitHandle.WaitOne();[/align]
[align=left] [/align]
[align=left] System.Console.WriteLine("Hit <enter> to exit...");[/align]
[align=left] System.Console.ReadLine();[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] }[/align]
}
十一:运行,我们看到程序停在waitHandle.WaitOne()上



2.3.2 触发事件
前面我们建立了一个顺序工作流,这一部分我们试图使其响应事件。
一:在program.cs添加如下行触发事件:
[align=left]Program.cs:[/align]
[align=left]……[/align]
[align=left] WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(ListenExternalEvent.Workflow1));[/align]
[align=left] instance.Start();[/align]
[align=left] [/align]
[align=left] localServices.TriggerExternalEvent();[/align]
[align=left] [/align]
[align=left] waitHandle.WaitOne();[/align]
[align=left] [/align]
[align=left] System.Console.WriteLine("Hit <enter> to exit...");[/align]
[align=left] System.Console.ReadLine();[/align]
[align=left] }[/align]
[align=left] }[/align]
[align=left] }[/align]
}
二:运行,异常,查看:



三:报的错误是ArgumentNullException,查帮助:
ArgumentNullException的引发条件为:调用某种方法时所传递的参数中,至少有一个在任何情况下都不应为空引用(在 Visual Basic 中为 Nothing)的参数为空引用(在 Visual Basic 中为 Nothing)。
我们中断程序,调试,发现ExternalEvent事件居然有值了,我们从没为它添加事件响应方法!异常是因为在调用这一事件响应函数时参数为null值而抛出的。我们对LocalServices做小改动,验证下:



可以发现,ExternalEvent2为null, 而ExternalEvent是有值的,它们间的差别就在Workflow1试图响应ExternalEvent事件,ExternalEvent在什么地方被添加了事件响应函数呢?显然与program.cs的下列代码有关,ExternalDataExchangeService为ExternalEvent事件赋值了:
[align=left] ExternalDataExchangeService dataService = new ExternalDataExchangeService();[/align]
[align=left] workflowRuntime.AddService(dataService);[/align]
[align=left][/align]
[align=left] LocalServices localServices = new LocalServices();[/align]
[align=left] dataService.AddService(localServices);[/align]
前面抛出的异常显然是因为我们触发事件的代码提供了两个null值:ExternalEvent(null, null);我们修改代码,提供参数:
[align=left] public class LocalServices : ILocalServices[/align]
[align=left] {[/align]
[align=left] public event EventHandler<ExternalDataEventArgs> ExternalEvent;[/align]
[align=left] [/align]
[align=left] public void TriggerExternalEvent()[/align]
[align=left] {[/align]
[align=left] ExternalEvent(null, new ExternalDataEventArgs());[/align]
[align=left] }[/align]
[align=left] }[/align]
ExternalDataEventArgs有三个构造函数,但它们都需要一个参数Guid instanceId ,我们可用想到:我们在ExternalDataExchangeService中注册本地服务时,没有指定Workflow类型与实例,由ExternalDataExchangeService根据接口自动处理,当我们在本地服务触发一个事件时由那个工作流实例响应呢?显然参数Guid instanceId具有决定性作用, ExternalDataExchangeService提供了单一的事件响应函数,然后根据instanceId决定将事件传递给哪一个工作流实例。这也解释了为什么本地服务的事件必须是一个用ExternalDataEventArgs实例化的EventHandler范型方法。
四:我们修改事件触发代码,从外部接收一个instanceId参数,并且让用户决定什么时刻实际触发事件:
[align=left] public class LocalServices : ILocalServices[/align]
[align=left] {[/align]
[align=left] public event EventHandler<ExternalDataEventArgs> ExternalEvent;[/align]
[align=left] [/align]
[align=left] public void TriggerExternalEvent(Guid InstanceID)[/align]
[align=left] {[/align]
[align=left] MessageBox.Show("点确定将触发ExternalEvent事件");[/align]
[align=left] ExternalEvent(null, new ExternalDataEventArgs(InstanceID));[/align]
[align=left] }[/align]
}
五:修改program.csmain方法,传递参数:
[align=left] class Program[/align]
[align=left] {[/align]
[align=left] static void Main(string[] args)[/align]
[align=left] {[/align]
[align=left] using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())[/align]
[align=left] {[/align]
[align=left]……[/align]
[align=left] WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(ListenExternalEvent.Workflow1));[/align]
[align=left] instance.Start();[/align]
[align=left] [/align]
[align=left] localServices.TriggerExternalEvent(instance.InstanceId);[/align]
[align=left] waitHandle.WaitOne();[/align]
[align=left] [/align]
[align=left] System.Console.WriteLine("Hit <enter> to exit...");[/align]
[align=left] System.Console.ReadLine();[/align]
[align=left] }[/align]
}
六:运行:



我们点“确定”:



2.3.3 还没明白



触发事件时ExternalEvent的Sender参数为什么必须是null?其它值会出现异常:



2.3.4 传递参数
前面的练习我们看到外部事件是一个标准EventHandler,显然参数和返回值通过TEventArgs e 传递,我们看下ExternalDataEventArgs,不适合传递其它参数,所以我们需要创建派生于ExternalDataEventArgs的类型用来传递参数,事件需要ExternalDataEventArgs,触发事件传递ExternalDataEventArgs的派生类的实例是可以的。
一:我们修改LocalServicesProj.cs的代码:
[align=left]namespace LocalServicesProj[/align]
[align=left]{[/align]
[align=left] public class exerciseExternalDataEventArgs: ExternalDataEventArgs[/align]
[align=left] {[/align]
[align=left] public exerciseExternalDataEventArgs(Guid instanceId)[/align]
[align=left] : base(instanceId)[/align]
[align=left] { [/align]
[align=left] }[/align]
[align=left] public DateTime triggerTime, respondTime; [/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] [ExternalDataExchange][/align]
[align=left] public interface ILocalServices[/align]
[align=left] {[/align]
[align=left] event EventHandler<ExternalDataEventArgs> ExternalEvent;[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public class LocalServices : ILocalServices[/align]
[align=left] {[/align]
[align=left] public event EventHandler<ExternalDataEventArgs> ExternalEvent;[/align]
[align=left] [/align]
[align=left] public void TriggerExternalEvent(Guid InstanceID)[/align]
[align=left] {[/align]
[align=left] exerciseExternalDataEventArgs eventArgs = new exerciseExternalDataEventArgs(InstanceID);[/align]
[align=left] MessageBox.Show("点确定将触发ExternalEvent事件");[/align]
[align=left] eventArgs.triggerTime = DateTime.Now;[/align]
[align=left] ExternalEvent(null, eventArgs);[/align]
[align=left] Console.WriteLine("LocalServices WriteLine: respondTime: " + eventArgs.respondTime);[/align]
[align=left] }[/align]
[align=left] }[/align]
}

我们用exerciseExternalDataEventArgs的属性triggerTime和respondTime传入传出参数。
二:编译运行,出错!



三:这个错误是因为触发事件传递的参数e必须能够被序列化,而我们定义的exerciseExternalDataEventArgs没有用Serializable修饰,为什么必须这么做,原因我还没搞清楚。修改exerciseExternalDataEventArgs
[align=left] [Serializable][/align]
[align=left] public class exerciseExternalDataEventArgs : ExternalDataEventArgs[/align]
[align=left] {[/align]
[align=left] public exerciseExternalDataEventArgs(Guid instanceId)[/align]
[align=left] : base(instanceId)[/align]
[align=left] { [/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public DateTime triggerTime, respondTime; [/align]
[align=left] }[/align]
四:运行,可以了。现在我们在工作流中接收数据并返回值:



我们看到有参数sendere,我们可以把它们绑定到工作流的属性或字段,操作见前面的调用外部方法。
我们还看到有Invoked属性,也就是说HandleExternalEventActivity在事件发后可以直接调用一个方法,这是非常方便的,不必要将参数绑定到字段,再加个CodeActivity处理了,我们设置Invoked属性handleEvent,双击编写代码:



[align=left] public sealed partial class Workflow1: SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left]……[/align]
[align=left] private void handleEvent(object sender, ExternalDataEventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine("Workflow1 WriteLine: triggerTime" + (e as exerciseExternalDataEventArgs).triggerTime);[/align]
[align=left] Thread.Sleep(2000);[/align]
[align=left] (e as exerciseExternalDataEventArgs).respondTime = DateTime.Now;[/align]
[align=left] }[/align]
}
五:运行:



明显值传递进了工作流,但我们的host触发事件后自己在继续跑,见”LocalServices WriteLine””Workflow1 WriteLine”前面,这是一个非常重要的概念,事件是异步的-工作流实例和host程序是异步的。所以我们想得到返回值还得用其它方法,如HandleExternalEventActivity后面加个CallExternalMethod等。
2.4 建立状态机工作流
[align=left] <ProjectTypeGuids>{14822709-B5A1-4724-98CA-57A101D1B079};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>[/align]

3 一个综合演练
在这一练习中我们完成一个费用审批业务,这是一个比较全面的练习,对基础的技术与操作不再详细描述,你必须完成前面的练习。
这一练习模仿Hands-On Lab 1-3/ Hands-On Lab 1-4。
这一练习我们还是采用逐步完善的策略,迭代完成。
业务描述:用户提交费用报告,如果费用少于某值,此报告自动通过,否则由管理者决定是否通过此费用报告。
3.1 规划并建立解决方案、项目
已完成代码见:MyExpenseWorkflows_1目录
建立项目与解决方案:
l 解决方案:MyExpenseWorkflows
l Windows窗体应用程序:ExpenseClientApplication是客户端程序,与用户交互,处理费用报告的提交、查看
l 控制台应用程序:ExpenseHost远程服务的Host程序
l 类库项目:ExpenseServices远程服务的服务实现以及与实现业务的工作流交换数据的本地服务实现
l 类库项目:ExpenseRemoteInterface定义客户端与服务端的服务接口
l 类库项目:ExpenseReportType 费用报告数据对象定义
l 类库项目:ExpenseLocalServiceInterface定义远程服务与工作流的数据交换的本地服务接口
l 工作流库项目:MyExpenseWorkflows实现业务逻辑的工作流
项目间关系:





建立所有接口、类、窗体以及配置文件,删除无意义文件,解决方案结构如上图。
注意配置文件的复制到输出目录。
在解决方案目录下建立子目录:Bin,设置所有项目的输出目录为../BIN
提示:
visual studio项目如果输出目录是项目主目录的子,则项目重新生成时,会删除输出目录下所有内容。这回导致项目A输出到项目B的输出目录,且项目A先于项目B编译,项目B编译时会删除项目A的输出,这算是一个功能BUG。所以我们通常在解决方案下建立公共的输出目录。
做基本测试:
定义远程服务接口IExpenseRemotingService.cs
[align=left]namespace ExpenseRemoteInterface[/align]
[align=left]{[/align]
[align=left] public interface IExpenseRemotingService[/align]
[align=left] {[/align]
[align=left] void Test(String S);[/align]
[align=left] }[/align]
}
实现远程服务接口ExpenseRemoteService.cs:
[align=left]namespace ExpenseServices[/align]
[align=left]{[/align]
[align=left] public class ExpenseRemoteService : MarshalByRefObject, IExpenseRemotingService[/align]
[align=left] {[/align]
[align=left] public void Test(String S)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine("ExpenseRemoteService.Test: " + S);[/align]
[align=left] }[/align]
[align=left] }[/align]
}
调用远程服务Form1.cs:
[align=left]namespace ExpenseClientApplication[/align]
[align=left]{[/align]
[align=left] public partial class Form1 : Form[/align]
[align=left] {[/align]
[align=left] protected IExpenseRemotingService Service;[/align]
[align=left] public Form1()[/align]
[align=left] {[/align]
[align=left] InitializeComponent();[/align]
[align=left] [/align]
[align=left] String ClientConfigFileName = "ExpenseClient.Cfg.xml";[/align]
[align=left] String Url = RemotingClientConfig.GetURLFromRemotingClientConfig(ClientConfigFileName);[/align]
[align=left] [/align]
[align=left] RemotingConfiguration.Configure(ClientConfigFileName, false);[/align]
[align=left] Service = (IExpenseRemotingService)Activator.GetObject(typeof(IExpenseRemotingService), Url);[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] private void button1_Click(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] Service.Test("dddddddddddd");[/align]
[align=left] }[/align]
[align=left] }[/align]
}
以下文件参看已完成的代码,文档中不列出:
l RemotingClientConfig.cs:通过客户端配置获得服务器URL。
[align=left]l ExpenseClient.Cfg.xml:客户端远程服务访问参数配置,注意客户端<client><wellknown>小节配置完整的URL后,<channels>小节可以省略。[/align]
[align=left]l ExpenseService.Cfg.xml:服务端服务发布参数配置。[/align]
[align=left]设置ExpenseServices的启动操作:[/align]



[align=left]配置解决方案启动项目如下(点解决方案,右键,属性):[/align]



[align=left]执行,你应该看到远程方法被正确调用:[/align]



3.2 实现报告提交部分
已完成代码见:MyExpenseWorkflows_2目录
一:定义IExpenseRemotingService
[align=left] public interface IExpenseRemotingService[/align]
[align=left] {[/align]
[align=left] void SubmitExpenseReport(ExpenseReport expenseReport);[/align]
[align=left] [/align]
[align=left] List<ExpenseReport> GetExpenseReportsList();[/align]
}
二:设计客户端
操作不再详述,参看已经完成的文档
三:设计工作流ExpenseWorkflow,准备接收参数Report并在控制台上输出信息表达工作流开始
[align=left] public partial class ExpenseWorkflow : SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left] private void ShowStartInfo(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine("ExpenseWorkflow(" + WorkflowInstanceId + ") Start");[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] private ExpenseReport report;[/align]
[align=left] [/align]
[align=left] public ExpenseReport Report[/align]
[align=left] {[/align]
[align=left] get { return report; }[/align]
[align=left] set { report = value; }[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] }[/align]



四:实现远程服务
[align=left] public class ExpenseRemoteService : MarshalByRefObject, IExpenseRemotingService[/align]
[align=left] {[/align]
[align=left] public ExpenseRemoteService()[/align]
[align=left] {[/align]
[align=left] //代码段1[/align]
[align=left] //Console.WriteLine("new ExpenseRemoteService!");[/align]
[align=left] [/align]
[align=left] StartWorkflowHost();[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] WorkflowRuntime workflowRuntime;[/align]
[align=left] ExpenseLocalService LocalService;[/align]
[align=left] List<ExpenseReport> ReportsList = new List<ExpenseReport>();[/align]
[align=left] Dictionary<Guid, Guid> InstanceList = new Dictionary<Guid, Guid>();[/align]
[align=left] [/align]
[align=left] private void StartWorkflowHost()[/align]
[align=left] {[/align]
[align=left] workflowRuntime = new WorkflowRuntime();[/align]
[align=left] workflowRuntime.StartRuntime();[/align]
[align=left] [/align]
[align=left] AutoResetEvent waitHandle = new AutoResetEvent(false);[/align]
[align=left] workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e)[/align]
[align=left]   { waitHandle.Set(); };[/align]
[align=left] workflowRuntime.WorkflowTerminated += delegate(object sender,WorkflowTerminatedEventArgs e)[/align]
[align=left] {[/align]
[align=left] Console.WriteLine(e.Exception.Message);[/align]
[align=left] waitHandle.Set();[/align]
[align=left] };[/align]
[align=left] [/align]
[align=left] ExternalDataExchangeService exSvc = new ExternalDataExchangeService();[/align]
[align=left] workflowRuntime.AddService(exSvc);[/align]
[align=left] LocalService = new ExpenseLocalService();[/align]
[align=left] exSvc.AddService(LocalService);[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public void SubmitExpenseReport(ExpenseReport expenseReport)[/align]
[align=left] {[/align]
[align=left] //代码段2[/align]
[align=left] //Assembly assm = Assembly.Load("MyExpenseWorkflows");[/align]
[align=left] //Type typeExpenseWorkflow = assm.GetType("MyExpenseWorkflows.ExpenseWorkflow");[/align]
[align=left] [/align]
[align=left] Dictionary<string, Object> parameters = new Dictionary<string, Object>();[/align]
[align=left] parameters.Add("Report", expenseReport);[/align]
[align=left] [/align]
[align=left] //代码段3[/align]
[align=left] //WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeExpenseWorkflow, parameters);[/align]
[align=left] WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(ExpenseWorkflow), parameters);[/align]
[align=left] ReportsList.Add(expenseReport);[/align]
[align=left] InstanceList.Add(expenseReport.ExpenseReportId, instance.InstanceId);[/align]
[align=left] [/align]
[align=left] instance.Start();[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] public List<ExpenseReportType.ExpenseReport> GetExpenseReportsList()[/align]
[align=left] {[/align]
[align=left] return ReportsList;[/align]
[align=left] }[/align]
}
五:运行程序



我们发现问题:工作流的确启动了,但点“Refresh Reports”,表格没有数据!
在ExpenseRemoteService的如下位置设置断点调试:



我们发现在方法SubmitExpenseReport中确实将expenseReport加入了ReportList,但在方法GetExpenseReportsList中ReportList总是空,而我们任何位置都没有删除过ReportList中的对象,我们有足够的理由猜到GetExpenseReportsList中ReportList已经不是SubmitExpenseReport中的ReportList了,在StartWorkflowHost方法添加代码“//代码段1”,调试:



我们发现每次调用远程对象的方法都回新建一个ExpenseRemoteService,根据我们的Remoting知识,我们需要改ExpenseService.Cfg.xml文件,使ExpenseRemoteService由服务器端激活:
对象生存期和租用期
[align=left] 一个single-call服务器端激活对象只在方法调用期间生存。之后,被垃圾回收器标记为删除。Singleton 服务器激活对象和客户端激活对象不一样,他们的生存期被租用控制。租用是一个对象,它实现了定义在System.Runtime.Remoting.Lifetime名称空间的Ilease接口。[/align]
[align=left] Singleton 服务器端激活对象和客户端激活对象缺省的租用对象有一个5分钟的InitialLeaseTime,2分钟的RenewOnCallTime,5分钟的CurrentLeaseTime。如果对象没有方法被调用,当CurrentLeaseTime为0时它被清除,也就是5分钟后被清除。[/align]
[align=left]<?xmlversion="1.0"encoding="utf-8" ?>[/align]
[align=left]<configuration>[/align]
[align=left] <system.runtime.remoting>[/align]
[align=left] <application>[/align]
[align=left] <service>[/align]
[align=left] <wellknownmode="Singleton"[/align]
[align=left] type="ExpenseServices.ExpenseRemoteService, ExpenseServices"[/align]
[align=left] objectUri="ExpenseRemoteService" />[/align]
[align=left] </service>[/align]
[align=left] <channels>[/align]
[align=left] <channelref="tcp server"port="8342" />[/align]
[align=left] </channels>[/align]
[align=left] </application>[/align]
[align=left] </system.runtime.remoting>[/align]
</configuration>
再次运行程序,可以看到ExpenseRemoteService只激活一次,GetExpenseReportsList也能正确返回结果了。
这是一个练习,我们没有持久化数据,只是在ExpenseRemoteService对象中保存我们的数据。实际系统中当然不能这样处理。
六:对程序做小的优化:
我们加入代码段2,3,注释掉代码段3下的行:
[align=left]WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(ExpenseWorkflow), parameters);[/align]
[align=left]移除项目ExpenseServices对MyExpenseWorkflows的引用,我们在动态地装载实现业务的工作流,如果装载来源我们从配置文件得到,我们就能实现ExpenseRemoteService通过配置文件决定用哪一个具体工作流实现它所需要的业务了,ExpenseRemoteService会提供一个IexpenseLocalService以及参数Report。[/align]
3.3 优化程序,使Report状态改变后自动更新客户端
我们的程序每次提交费用报告后必须手工刷新才能看到服务器端数据的变化,我们可以通过Remoting远程事件使服务器端Report改变后自动触发客户端事件更新客户端界面。
这个过程比较复杂,且Remoting远程事件不是本文档的主要内容,因此我们完成了此功能,放在MyExpenseWorkflows_3下,在此不再讲述相关技术以及操作过程了。你可以参照完成的例子以及文档“Remoting技术概述.Doc”自行学习。
为避免繁杂的内容干扰我们的学习,后面的例子我们仍然从MyExpenseWorkflows_2改进和发展。
3.4 完成自动审批业务
已完成代码见:MyExpenseWorkflows_4目录
删除CodeActivity1,添加一个IfElseActivity,两个CodeActivity,并取合适名称,设计工作流如下:



设置AutoApproveBranch条件如下:



设置两个CodeActivity的ExecuteCode
[align=left] public partial class ExpenseWorkflow : SequentialWorkflowActivity[/align]
[align=left] {[/align]
[align=left]……[/align]
[align=left] private void Approve(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] report.Status = StatusEnum.Approved;[/align]
[align=left] Console.WriteLine("Report " + report.ExpenseReportId [/align]
[align=left] + " Amount: " + report.Amount + " Approved");[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] private void Reject(object sender, EventArgs e)[/align]
[align=left] {[/align]
[align=left] report.Status = StatusEnum.Rejected;[/align]
[align=left] Console.WriteLine("Report " + report.ExpenseReportId[/align]
[align=left] + " Amount: " + report.Amount + " Rejected");[/align]
[align=left] }[/align]
[align=left] [/align]
[align=left] }[/align]
删除不再使用的方法,运行:



我们看到自动审批业务已经完成
3.5 完成管理者审批业务
3.6 总结
4 Windows Workflow深入知识
4.1 Workflow的复用
4.2 Windows Workflow与数据库
4.3 Windows Workflow与Web Service
4.4 Windows Workflow的可再发行设计工具
4.5 Windows Workflow宿主程序
4.6 管理与监控
5 一个CMMI流程演练
5.1 需求
5.2 设计
5.3 实现
6 结束语
Windows Workflow是比较新的技术,我也是边学习边写,有太多不妥之处,希望能得到你的帮助。
代码练习用,质量比较差,别笑话哈。
微软的workflow Hands on Labs有十组练习,建议你将其全部做一遍,微软的练习只讲怎么做,不说为什么这么做,且技术跳跃比较大,所以你先学习本文档后再做微软的那组练习会有更大收获。
所以需要下载的virsul studio插件、.Net 3.0、SDK等都在:
//10.10.40.16/公司内部培训资料/workflow
如果还有问题联系我:衡正军 jsnjjnhzj@hotmail.com
7 参考
基于新闻组和论坛的技术支持社区:
http://wf.netfx3.com

http://hi.baidu.com/biointeractive/blog/category/Windows%20Workflow%20Foundation

http://www.microsoft.com/china/MSDN/library/Windev/WindowsVista/WWFGetStart.mspx?mfr=true

http://blog.csdn.net/artlife/archive/2006/12/19/1448877.aspx
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: