学习完本章,你将掌握:
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)。
<configuration>
<connectionStrings>
<add name="StorageDatabase" connectionString="Data Source=(local)\SQLEXPRESS;Initial Catalog=WorkflowStore;Integrated Security=True;"/>
</connectionStrings>
</configuration>
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.WorkflowCompleted +=
new EventHandler<WorkflowCompletedEventArgs>(Runtime_WorkflowCompleted);
_runtime.WorkflowTerminated +=
new EventHandler<WorkflowTerminatedEventArgs>(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中添加下面的代码:
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”按钮是不可使用的。这样做的意义是:假如卸载请求失败,也就不用再加载。
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”事件处理方法(该方法包含代码我们已在前面的步骤中添加)的下面添加下面这些方法:
{
WorkflowCompleted();
}
void Runtime_WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e)
{
WorkflowCompleted();
}
15.当然,我们现在还要创建“WorkflowCompleted”方法。假如你熟悉Windows编程,你可能知道Windows一直以来存在着的限制问题。这个限制简单的说就是你不能在创建窗口控件的线程外的任何线程中改变该窗口控件的状态。因此假如你想改变某个控件的text,你必须在创建该控件的同一线程上指定该控件的text,使用任何其它线程最有可能导致的结果是造成你的应用程序崩溃。因此我们即将加入的代码对你来说可能很搞笑,但所有要做的这些的真正目的是确信我们是在原来的创建按钮的线程中来使按钮能用和禁用。(事件处理几乎总是在不同的线程上进行调用。)假如我们只是在按钮自身的事件处理中使其可用,应用程序可能还是能工作,但它更大的可能是崩溃和挂起。简单地把下面的代码复制并放到源文件Form1类的尾部,应用程序才会正确的工作。
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。最后为运行时添加持久化服务,方法是在创建工作流运行时对象后添加下面的代码:
_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方法添加下面的代码:
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方法中添加下面必要的代码:
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”事件处理中添加下面的代码:
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步一样,在运行时对象被创建后添加持久化服务:
parms.Add("UnloadOnIdle", "true");
parms.Add("ConnectionString", ConfigurationManager.ConnectionStrings["StorageDatabase"].ConnectionString);
_workflowRuntime.AddService(new SqlWorkflowPersistenceService(parms));
4.和前面的例子一样添加一个应用程序配置文件(连接字符串仍然相同)。具体过程可参考第五章中的“为你的工作流添加跟踪服务”,那里添加了该app.config文件。
5.创建一个单独的顺序工作流库项目来承载我们的新工作流,工作流库的名称为IdledWorkflow。
6.重复前一个名称为“创建一个新的可卸载的工作流”例子中的步骤2到步骤4。这些步骤放置了二个Code活动到你的工作流中。
7.在源代码文件中添加下面的代码到“PreUnload”方法中(先前一步你已添加了“PostUnload”方法的代码)。
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.当然,因为没有工作流被启动,该应用程序也就不会等待工作流完成。因此,创建一个工作流实例,在你找到的那行代码下添加下面的代码:
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof(IdledWorkflow.Workflow1));
// Start the workflow instance.
instance.Start();
13.按F6键编译该解决方案,纠正任何弹出的编译错误。现在,当你执行该WorkflowIdler应用程序时,Delay活动将强制工作流实例持久化到存储数据库中。然而,你会等上超过30秒(多达2分钟)的时间,实例才被重新加载。那是因为工作流运行时在空闲状态下由于延时,要周期性地对持久化工作流进行检查,但它不能保证那些工作流将仅仅等上所期望的延时时间。WF会周期性地轮询数据库,寻找那些正等待计时器事件的空闲并已被持久化的工作流(Delay活动使用了一个计时器)。默认的轮询时间是2分钟。
备注:可以更改默认的数据库轮询时间,方法是使用SqlWorkflowPersistenceService服务时为其提供一个TimeSpan,可使用四个参数的构造函数(分别是连接字符串、工作流处于空闲状态时是否卸载的标志、工作流保持锁定的时间长度以及持久化服务轮询数据库以查找计时器已过期的工作流的频率)。
本章源代码:源码下载(里面包含本章练习项目、完整代码以及相关数据库)
using System.Collections.Generic;
using System.Text;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;
using System.Configuration;
using System.Collections.Specialized;
namespace WorkflowIdler
{
public static class WorkflowFactory
{
// Singleton instance of the workflow runtime.
private static WorkflowRuntime _workflowRuntime = null;
// Lock (sync) object
private static object _syncRoot = new object();
// Factory method.
public static WorkflowRuntime GetWorkflowRuntime()
{
// Lock execution thread in case of multi-threaded
// (concurrent) access.
lock (_syncRoot)
{
// Check for startup condition.
if (null == _workflowRuntime)
{
// Provide for shutdown
AppDomain.CurrentDomain.ProcessExit += new
EventHandler(StopWorkflowRuntime);
AppDomain.CurrentDomain.DomainUnload += new
EventHandler(StopWorkflowRuntime);
// Not started, so create instance.
_workflowRuntime = new WorkflowRuntime();
NameValueCollection parms = new NameValueCollection();
parms.Add("UnloadOnIdle", "true");
parms.Add("ConnectionString", ConfigurationManager.ConnectionStrings["StorageDatabase"].ConnectionString);
_workflowRuntime.AddService(new SqlWorkflowPersistenceService(parms));
// Start the runtime.
_workflowRuntime.StartRuntime();
} // if
// Return singleton instance.
return _workflowRuntime;
} // lock
}
// Shutdown method
static void StopWorkflowRuntime(object sender, EventArgs e)
{
if (_workflowRuntime != null)
{
if (_workflowRuntime.IsStarted)
{
try
{
// Stop the runtime
_workflowRuntime.StopRuntime();
}
catch (ObjectDisposedException)
{
// Already disposed of, so ignore...
} // catch
} // if
} // if
}
}
}
using System.Collections.Generic;
using System.Text;
using System.Workflow.Runtime;
using System.Threading;
namespace WorkflowIdler
{
class Program
{
private static AutoResetEvent _waitHandle = new AutoResetEvent(false);
static void Main(string[] args)
{
WorkflowRuntime workflowRuntime = WorkflowFactory.GetWorkflowRuntime();
workflowRuntime.WorkflowIdled += new EventHandler<WorkflowEventArgs>(workflowIdled);
workflowRuntime.WorkflowCompleted += new EventHandler<WorkflowCompletedEventArgs>(workflowCompleted);
workflowRuntime.WorkflowTerminated += new EventHandler<WorkflowTerminatedEventArgs>(workflowTerminated);
Console.WriteLine("Waiting for workflow completion.");
// Create the workflow instance.
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof(IdledWorkflow.Workflow1));
// Start the workflow instance.
instance.Start();
_waitHandle.WaitOne();
Console.WriteLine("Done.");
Console.ReadLine();
}
static void workflowTerminated(object sender, WorkflowTerminatedEventArgs e)
{
Console.WriteLine("Workflow instance terminated, " +
"reason: '{0}'.", e.Exception.Message);
_waitHandle.Set();
}
static void workflowCompleted(object sender, WorkflowCompletedEventArgs e)
{
Console.WriteLine("Workflow instance completed.");
_waitHandle.Set();
}
static void workflowIdled(object sender, WorkflowEventArgs e)
{
Console.WriteLine("Workflow instance idled.");
}
}
}
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
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;
namespace IdledWorkflow
{
public sealed partial class Workflow1: SequentialWorkflowActivity
{
public Workflow1()
{
InitializeComponent();
}
private DateTime _started = DateTime.MinValue;
private void PreUnload(object sender, EventArgs e)
{
_started = DateTime.Now;
System.Diagnostics.Trace.WriteLine(
String.Format("*** Workflow {0} started: {1}",
WorkflowInstanceId.ToString(),
_started.ToString("MM/dd/yyyy hh:mm:ss.fff")));
}
private void PostUnload(object sender, EventArgs e)
{
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()));
}
}
}
¥¥¥¥¥¥¥¥¥¥¥¥¥
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Workflow.Runtime;
namespace WorkflowPersister
{
public partial class Form1 : Form
{
protected WorkflowRuntime _runtime = null;
protected WorkflowInstance _instance = null;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
_runtime = WorkflowFactory.GetWorkflowRuntime();
_runtime.WorkflowCompleted +=
new EventHandler<WorkflowCompletedEventArgs>(Runtime_WorkflowCompleted);
_runtime.WorkflowTerminated +=
new EventHandler<WorkflowTerminatedEventArgs>(Runtime_WorkflowTerminated);
}
private void button1_Click(object sender, EventArgs e)
{
button2.Enabled = true;
button1.Enabled = false;
_instance = _runtime.CreateWorkflow(typeof(PersistedWorkflow.Workflow1));
_instance.Start();
}
private void button2_Click(object sender, EventArgs e)
{
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
}
void Runtime_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e)
{
WorkflowCompleted();
}
void Runtime_WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e)
{
WorkflowCompleted();
}
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
}
private void button3_Click(object sender, EventArgs e)
{
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;
}
}
}
using System.Collections.Generic;
using System.Text;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;
using System.Configuration;
namespace WorkflowPersister
{
public static class WorkflowFactory
{
// Singleton instance of the workflow runtime
private static WorkflowRuntime _workflowRuntime = null;
// Lock (sync) object
private static object _syncRoot = new object();
// Factory method.
public static WorkflowRuntime GetWorkflowRuntime()
{
// Lock execution thread in case of multi-threaded
// (concurrent) access.
lock (_syncRoot)
{
// Check for startup condition.
if (null == _workflowRuntime)
{
// Provide for shutdown
AppDomain.CurrentDomain.ProcessExit +=
new EventHandler(StopWorkflowRuntime);
AppDomain.CurrentDomain.DomainUnload +=
new EventHandler(StopWorkflowRuntime);
// Provide for shutdown
AppDomain.CurrentDomain.ProcessExit += new
EventHandler(StopWorkflowRuntime);
AppDomain.CurrentDomain.DomainUnload += new
EventHandler(StopWorkflowRuntime);
// Not started, so create instance.
_workflowRuntime = new WorkflowRuntime();
string conn = ConfigurationManager.ConnectionStrings["StorageDatabase"].ConnectionString;
_workflowRuntime.AddService(new SqlWorkflowPersistenceService(conn));
// Start the runtime
_workflowRuntime.StartRuntime();
} // if
// Return singleton instance.
return _workflowRuntime;
} // lock
}
// Shutdown method
static void StopWorkflowRuntime(object sender, EventArgs e)
{
if (_workflowRuntime != null)
{
if (_workflowRuntime.IsStarted)
{
try
{
// Stop the runtime
_workflowRuntime.StopRuntime();
}
catch (ObjectDisposedException)
{
// Already disposed of, so ignore...
} // catch
} // if
} // if
}
}
}
using System.Collections.Generic;
using System.Windows.Forms;
using System.Workflow.Runtime;
using System.Threading;
namespace WorkflowPersister
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
private static AutoResetEvent _waitHandle = new AutoResetEvent(false);
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
WorkflowRuntime workflowRuntime = WorkflowFactory.GetWorkflowRuntime();
workflowRuntime.WorkflowIdled += new EventHandler<WorkflowEventArgs>(workflowIdled);
workflowRuntime.WorkflowCompleted += new EventHandler<WorkflowCompletedEventArgs>(workflowCompleted);
workflowRuntime.WorkflowTerminated += new EventHandler<WorkflowTerminatedEventArgs>(workflowTerminated);
Console.WriteLine("Waiting for workflow completion.");
_waitHandle.WaitOne();
Console.WriteLine("Done.");
}
static void workflowTerminated(object sender, WorkflowTerminatedEventArgs e)
{
Console.WriteLine("Workflow instance terminated, reason: '{0}'.", e.Exception.Message);
_waitHandle.Set();
}
static void workflowCompleted(object sender, WorkflowCompletedEventArgs e)
{
Console.WriteLine("Workflow instance completed.");
_waitHandle.Set();
}
static void workflowIdled(object sender, WorkflowEventArgs e)
{
Console.WriteLine("Workflow instance idled.");
}
}
}
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;
namespace PersistedWorkflow
{
public sealed partial class Workflow1: SequentialWorkflowActivity
{
private DateTime _started = DateTime.MinValue;
public Workflow1()
{
InitializeComponent();
}
private void PreUnload(object sender, EventArgs e)
{
_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
}
private void PostUnload(object sender, EventArgs e)
{
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()));
}
}
}