学习完本章,你将掌握:
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中添加的以下代码:
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的开始标记和结束标记间插入下面的内容:
< add name ="TrackingDatabase" connectionString ="Data Source=(local)\SQLEXPRESS;Initial Catalog=WorkflowTracking;Integrated Security=True;" />
</ connectionStrings >
备注:上面的连接字符串可能和你实际应用中有所不同,你需要灵活进行配置。
4.点击WorkflowTracker项目中的WorkflowFactory.cs文件,查看其代码。
5.在该文件中声明以下名称空间(需添加 System.Configuration 引用):
using System.Workflow.Runtime.Tracking;
using System.Configuration;
6.在WorkflowFactory.cs文件中,找到我们创建WorkflowRuntime实例的地方,在这里我们需要为WorkflowRuntime引入SqlTrackingService。在GetWorkflowRuntime方法中添加下面的代码:
_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(ConfigurationManager.ConnectionStrings["TrackingDatabase"].ConnectionString);
SqlTrackingWorkflowInstance sqlTrackingWorkflowInstance = null;
sqlTrackingQuery.TryGetWorkflow(instanceId, out sqlTrackingWorkflowInstance);
if (sqlTrackingWorkflowInstance != null)
{
Console.WriteLine("\nActivity Tracking Events:\n");
Console.WriteLine(" Status :: Date/Time :: Qualified ID");
foreach (ActivityTrackingRecord atr in sqlTrackingWorkflowInstance.ActivityEvents)
{
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(ConfigurationManager.ConnectionStrings["TrackingDatabase"].ConnectionString);
SqlTrackingWorkflowInstance sqlTrackingWorkflowInstance = null;
sqlTrackingQuery.TryGetWorkflow(instanceId, out sqlTrackingWorkflowInstance);
if (sqlTrackingWorkflowInstance != null)
{
Console.WriteLine("\nWorkflow Instance Events:\n");
Console.WriteLine(" Description :: Date/Time");
foreach (WorkflowTrackingRecord workflowTrackingRecord in sqlTrackingWorkflowInstance.WorkflowEvents)
{
Console.WriteLine(" {0} :: {1}", workflowTrackingRecord.TrackingWorkflowEvent, 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.Data;
using System.Data.SqlClient;
using System.Globalization;
using System.IO;
3.上述步骤后,我们去找到Main方法。在该方法中创建工作流的代码前添加以下代码:
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(Activity));
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 profilethis 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.InvariantCulture);
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.NVarChar, 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 already exists in the database.");
} // if
else
{
// Write error message
Console.WriteLine("Error writing profile to database: {0}", ex.ToString());
} // else
} // if
else
{
// Write error message
Console.WriteLine("Error writing profile to database: {0}", ex.ToString());
} // 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程序的用户界面如下图所示:
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(StopWorkflowRuntime);
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)
{
}
}
}
}
}
}
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Linq;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using System.Windows.Forms;
namespace TrackedWorkflow
{
public sealed partial class Workflow1: SequentialWorkflowActivity
{
public Workflow1()
{
InitializeComponent();
}
private void QueryDelay(object sender, ConditionalEventArgs e)
{
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
}
private void PreDelayMessage(object sender, EventArgs e)
{
MessageBox.Show("Pre-delay code is being executed.");
this.TrackData("Delay commencing");
}
private void PostDelayMessage(object sender, EventArgs e)
{
MessageBox.Show("Post-delay code is being executed.");
this.TrackData("Delay completed");
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.Runtime;
using System.Threading;
using System.Workflow.Runtime.Tracking;
using System.Workflow.ComponentModel;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Globalization;
using System.IO;
namespace WorkflowTracker
{
class Program
{
// The auto-reset event that will cause the main thread to wait
// until the workflow completes or is terminated.
private static AutoResetEvent waitHandle = new AutoResetEvent(false);
// The main application method.
static void Main(string[] args)
{
// Obtain a workflow runtime object.
WorkflowRuntime workflowRuntime = WorkflowFactory.GetWorkflowRuntime();
// Add the event handlers.
workflowRuntime.WorkflowIdled += new EventHandler<WorkflowEventArgs>(workflowIdled);
workflowRuntime.WorkflowCompleted += new EventHandler<WorkflowCompletedEventArgs>(workflowCompleted);
workflowRuntime.WorkflowTerminated += new EventHandler<WorkflowTerminatedEventArgs>(workflowTerminated);
// Print banner.
Console.WriteLine("Waiting for workflow completion.");
// Create the workflow instance.
TrackingProfile profile = CreateProfile();
StoreProfile(profile, ConfigurationManager.ConnectionStrings["TrackingDatabase"].ConnectionString);
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof(TrackedWorkflow.Workflow1));
// Start the workflow instance.
instance.Start();
// Wait for the workflow to complete.
waitHandle.WaitOne();
ShowWorkflowTrackingEvents(instance.InstanceId);
ShowActivityTrackingEvents(instance.InstanceId);
// Print banner.
Console.WriteLine("Done.");
Console.ReadLine();
}
// The "terminated" event handler (we killed it or it had an exception).
static void workflowTerminated(object sender, WorkflowTerminatedEventArgs e)
{
Console.WriteLine("Workflow instance terminated.");
waitHandle.Set();
}
// The "completed" event handler (ran to completion, no errors).
static void workflowCompleted(object sender, WorkflowCompletedEventArgs e)
{
Console.WriteLine("Workflow instance completed.");
waitHandle.Set();
}
// The "idled" event handler.
static void workflowIdled(object sender, WorkflowEventArgs e)
{
Console.WriteLine("Workflow instance idled.");
}
// The activity tracking record display method
static void ShowActivityTrackingEvents(Guid instanceId)
{
SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery(ConfigurationManager.ConnectionStrings["TrackingDatabase"].ConnectionString);
SqlTrackingWorkflowInstance sqlTrackingWorkflowInstance = null;
sqlTrackingQuery.TryGetWorkflow(instanceId, out sqlTrackingWorkflowInstance);
if (sqlTrackingWorkflowInstance != null)
{
Console.WriteLine("\nActivity Tracking Events:\n");
Console.WriteLine(" Status :: Date/Time :: Qualified ID");
foreach (ActivityTrackingRecord atr in sqlTrackingWorkflowInstance.ActivityEvents)
{
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(ConfigurationManager.ConnectionStrings["TrackingDatabase"].ConnectionString);
SqlTrackingWorkflowInstance sqlTrackingWorkflowInstance = null;
sqlTrackingQuery.TryGetWorkflow(instanceId, out sqlTrackingWorkflowInstance);
if (sqlTrackingWorkflowInstance != null)
{
Console.WriteLine("\nWorkflow Instance Events:\n");
Console.WriteLine(" Description :: Date/Time");
foreach (WorkflowTrackingRecord workflowTrackingRecord in sqlTrackingWorkflowInstance.WorkflowEvents)
{
Console.WriteLine(" {0} :: {1}", workflowTrackingRecord.TrackingWorkflowEvent, workflowTrackingRecord.EventDateTime);
} // foreach
}
}
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(Activity));
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;
}
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.InvariantCulture);
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.NVarChar, 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.Contains("已经存在大于或等于新版本的版本"))
{
// Version already exists...
Console.WriteLine("NOTE: a profile with the same version already exists in the database.");
} // if
else
{
// Write error message
Console.WriteLine("Error writing profile to database: {0}", ex.ToString());
} // else
} // if
else
{
// Write error message
Console.WriteLine("Error writing profile to database: {0}", ex.ToString());
} // else
} // catch
finally
{
// Close the connection
if (conn != null)
{
conn.Close();
} // if
} // finally
}
}
}