上一篇:WF从入门到精通(第十八章):在你的工作流中调用Web服务
学习完本章,你将掌握:
1.了解要把你的工作流暴露为XML Web服务来使用的话,各个工作流活动该怎样进行设计
2.了解在ASP.NET中宿主工作流需要些什么
3.看看在基于XML Web服务的工作流中如何进行错误(fault)处理
4.针对各种情况对你的基于XML Web服务的工作流进行配置
在前一章“在你的工作流中调用Web服务”中,你看到了如何从你客户端一侧的工作流中使用WF所提供的InvokeWebService活动来调用XML Web服务。但是,在那章的应用程序范例中的XML Web服务是一个典型的ASP.NET的XML Web服务——没有什么特别的。
在这最后一章中,你将学会怎样对工作流进行处理并自动地把工作流暴露为XML Web服务以让客户去使用。这并不像创建一个工作流程序集库然后从一个Web服务项目中引用它那样简单,但是话又说回来,一旦你理解了一些基本概念并在一个应用程序范例中看看它的实现后,要做到也并不困难。
备注:本章的焦点是把WF作为XML Web服务集成到ASP.NET中使用。但是,在暴露XML Web服务的时候你应该意识到许多关键的问题,最大的问题是安全。对安全的充分讨论远远超过了我可以在此做的介绍,但是下面这个链接应该可以为你带来帮助:http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/THCMCH12.asp。假如你想把你的工作流暴露为一个XML Web服务的话,我强烈建议你查阅一下ASP.NET安全方面的最佳实践,尤其是围绕XML Web服务的实践。
把工作流暴露为一个XML Web服务
你不能在ASP.NET环境中直接执行工作流的部分原因其实在本书中你已经了解过,默认情况下,工作流运行时是以异步的方式去执行工作流实例的。事实上,在非Web应用程序中使用工作流的时候,这是一个很重要的特性。
但是在基于Web的环境下,这会带来一个问题。如果一个ASP.NET请求发来后,不管它是一个XML Web服务还是一个ASP.NET Web页面,工作流实例都要开始执行并且运行时要把控制权返回给ASP.NET。你的XML Web服务或者ASP.NET页面会立即并持续执行准备输出并可能会在工作流实例完成之前结束。因为工作流实例是异步的,它和你的ASP.NET应用程序并行执行,因此你的ASP.NET代码可能会很快地完成并返回一个工作流处理过程并未完成的响应结果。
提示:在ASP.NET Web页面中正确地执行工作流实例实际上就是对ASP.NET异步Web页面进行调用,该话题超出了本书的范围。但是下面这个链接能带给你一些具体的细节:http://msdn.microsoft.com/msdnmag/issues/05/10/WickedCode/。
这个问题至少会给我们带来两个挑战。首先,我们需要禁用,或者至少要对我们的工作流的异步执行方式进行变通。我们需要它们以同步的方式执行,就像它们和我们的页面或者XML Web服务中使用的是同一个线程一样,以让该Web应用程序在我们结束之前是不会把响应结果返回给调用者的。当然,这并未解决长时间运行的工作流的问题,这是我们将需要去克服的第二个挑战性难题。
长时间运行的工作流所带来的难题紧紧地和基于Web的应用程序自身的性质联系在一起。在这最后一章中你知道了Web应用程序从本质上是无状态的。所发送的在数毫秒间就断开的请求是完全意识不到对方的,除非我们创建一个框架来提供这种能力。Web应用程序也在Web服务器上执行,它通常是非常昂贵的系统,旨在为许许多多的客户端提供服务。假如一个工作流要花费大量时间才能完成,那么它会完全占有Web服务器并降低应用程序的可伸缩性(指的是为越来越多的客户端请求提供服务的能力)。
解决的办法是进行状态管理以及对长时间运行的工作流进行持久化。假如你的工作流程要在超过一个以上的基于Web的调用(ASP.NET页面请求或者XML Web服务)后才能完成的话,你必须持久化该工作流实例并在下一个执行周期期间重新加载它。这也是为什么我在前一章中的“长时间运行的XML Web服务”一节中提到重新生成保存了session状态的cookie的原因。因为客户端也必须意识到有这种可能性并考虑到会有超过一个以上的请求-响应的情况。
Internet信息服务(IIS)特别擅于节约系统资源。在一个典型的客户端应用程序中,其实在本书中到目前为止你所看到过的每一个应用程序当中,工作流运行时是在应用程序开始执行的时候被启动并贯穿该应用程序的生命周期。然而,IIS要收回服务器的资源。我们作为ASP.NET的程序员,这就意味着下面的两件事。
首先,我们必须以某种方式来决定在什么地方以及怎样来启动工作流运行时。在同一个ASP.NET应用程序中的不同请求会被放到不同的线程上进行处理,但是它们在同一个应用程序域(AppDomain)中执行。就像你或许还记得的,在每一个AppDomain只能有唯一的一个WF工作流运行时的实例在执行。因此它并不像在你的ASP.NET应用程序每收到一个请求的时候就创建一个工作流运行时的实例那样简单。这样做可能会导致工作流运行时产生异常。
其次,我们需要找到一种方法来让我们的工作流以同步的方式执行。或者,如果我们的工作流需要长时间运行的话,我们必须在开始执行工作流实例的时候、停止执行的时候、持久化工作流实例的时候以及把工作流当前的状态返回给客户端的时候进行同步。为此,我们需要替换默认的工作流运行时的线程调度服务。要替换该线程调度服务,我们需要重新配置工作流运行时。
创建工作流运行时
如果你看了前一章的XML Web服务的话,你会看到我向该ASP.NET范例添加了一个特殊的文件:Global.asax。这个文件包含了一些在ASP.NET应用程序生命周期中的一些重要的事件处理程序。在Global.asax中,其中的一个事件处理程序是应用程序启动事件,它在创建一个HttpApplication实例的时候通过ASP.NET触发。假如你想的话,你可以在这个Application_Start事件处理程序中来启动工作流运行时。(在前一章中我做了一些类似的工作,我创建了一个静态的Dictionary对象以便进行股票的查询。)
备注:假如你想学习关于ASP.NET中应用程序生命周期方面的知识的话,可以看看msdn2.microsoft.com/en-us/library/aa485331.aspx提供的一些细节。
你将面对的问题是,对于个别要访问工作流运行时的Web资源的请求来说,你需要为其提供一些机制。完成这件事最显而易见的解决办法是从你创建的一个容器类中去引用工作流运行时,然后把它放到ASP.NET的缓存(cache)中。事实上,这几乎就差不多了。
WF团队知道人们想在ASP.NET应用程序中使用工作流运行时,他们也知道启动工作流运行时会是困难的,因此他们已经为我们创建了WorkflowWebRequestContext类。
WorkflowWebRequestContext巧妙地为我们解决了两个难题。首先,它维护了一个工作流运行时的单一实例。当你使用它的CurrentWorkflowRuntime访问器的时候,假如该工作流运行时的单一实例为null的话,它就会为你创建该工作流运行时。如果该工作流运行时已经被创建过,就将返回被缓存的这个工作流运行时。这是我们贯穿本书一直在使用的,从第2章“工作流运行时”所引入的WorkflowFactory类的正确的模式。它所解决的另一个难题是所有的Web资源都能访问到WorkflowWebRequestContext,并能由此获得访问工作流运行时的能力。
配置服务
有了一个恰当的机制来检索我们的ASP.NET应用程序(页面或者XML Web服务)所要使用的这个唯一的工作流运行时,我们现在需要转到添加我们需要的服务上来。我们至少需要一个服务,但更多的服务有可能也是必须。我们需要添加到运行时的这个服务是手动线程调度服务(manual thread scheduling service),但是持久化服务和事务服务也是通常要进行添加的。
你最初可能会认为这些服务的添加方式和我们贯穿本书添加其它服务所使用的添加方式是同一种方式。也就是说,你可能会认为下面的方法也能够工作:
string connString = ConfigurationManager.ConnectionStrings[ " MyPersistenceDB " ].ConnectionString;
workflowRuntime.AddService( new SqlWorkflowPersistenceService(connString));
但是,在ASP.NET环境中这会失败。到我们访问工作流运行时的时候,它已经被WorkflowWebRequestContext启动了。一旦工作流被启动后,你就不能再添加特殊的服务,持久化就是其中一个。而且,在ASP.NET环境中,其它的工作流实例可能已经正使用工作流运行时,因此你也不能停止它来添加想要的服务然后对它进行重启。因此,必须有一些其它的方式来进行工作流运行时的配置。
事实上,术语“配置(configuration)”比你可能猜想的还要贴切。要被添加的服务从ASP.NET应用程序的配置文件Web.config中读出。一个特殊的节被专门用来进行工作流的配置,被
我将马上为你展示一个典型的Web.config文件,但是在此之前,还有一点需要考虑。由谁来创建WorkflowWebRequestContext对象呢?这个对象并不是拆盒即用(out-of-the-box)的ASP.NET实现的一部分,因此必须由某物在某处来对它的生命周期进行负责。XML Web服务对特定的工作流实例的调用也是这种情况(回忆起了SessionId了吗?),它必须被映射到这些实例上,并且假如实例碰巧被持久化,它们也必须被重新加载以便执行。
完成所有这些任务的对象是一个被称作WorkflowWebHostingModule的ASP.NET HTTP 模块(HttpModule)。ASP.NET HTTP 模块,正如你可能知悉的,它们是一些扩展对象,你能把它们放进你的基于Web的应用程序的请求-响应路径中。它们就是用在这种类型的任务上:为你的ASP.NET应用程序增加扩展性功能。碰巧,通过你应用程序的Web.config文件也能对附加的ASP.NET HTTP 模块进行配置。
备注:为了学习关于HTTP 模块的更多知识,以及他们怎样增强ASP.NET HTTP的请求-响应管线,可以看看http://msdn.microsoft.com/en-us/library/ms178468(zh-cn,VS.80).aspx。
清单19-1为你提供了一个基本的Web.config文件,里面包含了许多基于WF的应用程序宿主到ASP.NET环境中去所需进行配置的公共组件。注意它包含的信息指出了要把WF宿主到你的ASP.NET应用程序中:你仍然需要为你的应用程序添加一些恰当的ASP.NET配置(就像是
备注:清单19-1只适用于当前WF的正式发布版本。但是,如果新的版本发布后,你配置文件中的对应的版本号和公开密钥的值也将需要进行更新。你或许也需要修改连接字符串以便和你所安装的SQL Server相匹配。
清单19-1 进行工作流扩展的Web.config
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<configSections>
<section name="WorkflowRuntime" type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
configSections>
<WorkflowRuntime Name='WorkflowServiceContainer">
<Services>
<add type="System.Workflow.Runtime.Hosting.ManualWorkflowSchedulerService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add type="System.Workflow.Runtime.hosting.DefaultWorkflowTransactionService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
Services>
WorkflowRuntime>
<appSettings />
<connectionStrings>
<add name="MyPersistenceDB" connectionString="server=(local)\SQLEXPRESS;database=WorkflowStore;Integrated Security=true" />
connectionStrings>
<system.web>
<httpModules>
<add type="System.Workflow.Runtime.Hosting.WorkflowWebHostingModule, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" name="WorkflowHost" />
httpModules>
system.web>
configuraton>
工作流的内务处理(Husekeeping)
在本章中目前为止讨论的所有内容都围绕着ASP.NET的架构以及它宿主基于WF的应用程序的必备条件展开。在我们接触WF的基于XML Web服务的活动之前,我们先花点时间探讨一下怎样架构基于工作流的XML Web服务的应用程序。
当你使用ExternalDataExchange和本地通信服务的时候,对在本书中所描述过的过程来说,第一步无论无何都是创建一个接口。这个接口对将被用来在你的应用程序和工作流之间传递信息的方法和数据进行识别,它是整个通信过程的服务基础。
XML Web服务的工作流也遵循相似的模式。你创建一个接口,它的方法将成为基于Web服务的方法(当我们使用.NET的时候,我们称它们为Web方法)。接口和工作流通常都被放到一个单独的程序集(assembly)中去,你再在ASP.NET宿主应用程序中引用它们。(ASP.NET具有运行时动态编译代码的能力。我们将通过使用一个预编译的程序集来回避这一点。)然后,当我提及WF的基于XML Web服务的活动和接口、方法进行交互的时候,我所提到的那个接口正是我将生成的这个接口。
要记住的另外一件事是通常.NET的XML Web服务通过一个.asmx文件暴露出来。这个文件,或者它的代码后置文件,将包含一个派生自System.Web.Services.WebService基类的类定义。个别标识了System.Web.Services.WebMethod特性的方法将被指定为Web方法。.asmx文件本身包含了一个<%@WebService%>标示语句,它用来告知ASP.NET这是一个XML Web服务而不是一个Web页面,并且它还提供了一些其它的特定信息。
就像任何其它的XML Web服务一样,基于WF的XML Web服务也需要.asmx文件,它也包含Web服务的标示语句,因为ASP.NET和IIS要凭此来识别XML Web服务。好消息是我们并不需要亲自去创建真正的ASP.NET XML Web服务项目。WF向Microsoft Vislual Studio中增加了一个把工作流发布为ASP.NET项目的菜单项并为我们集成到了我们的解决方案中。这真是一个棒极了的功能,我们将在本章的晚些时候体验一番。
使用WebServiceInput活动
好吧,我们继续深入。基于WF的XML Web服务必须至少有一个WebServiceInput活动以及一个或更多的WebServiceOutput活动。输入活动和输出活动是相互联系的:每个输出活动必须和一个输入活动关联。(WebServiceFault活动也是一样的。)你不能只拖入一个WebServiceInput而没有输出活动(WebServiceOutput)或者失败活动(WebServiceFault),同样,你也不能只有输出活动或者失败活动而没有最起码的一个WebServiceInput。每种情况都会使你导致验证失败和编译错误。
当你把一个WebServiceInput活动放进你的工作流后,你需要设置几个属性,也许还要至少去处理一个事件。其中重要的属性显示在表19-1中。InputReceived事件让你能为工作流进行环境的初始化工作。而下面这些属性用来对WebServiceInput活动进行配置。
表19-1 WebServiceInput活动的重要属性
属性 | 功能 |
InterfaceType | 获取或者设置接口的数据类型,它可用来识别出潜在的可作为Web方法使用的方法。 |
IsActivating | 获取或者设置该活动的激活状态。该值指示是否应该在接收数据的时候启动工作流,假如你的工作流中存在多个WebServiceInput活动的话,你可以通过设置其中一个活动的这个属性为true来确定让它去启动工作流。在你的工作流中必须有一个WebServiceInput活动设置它的这个属性为true。假如有超过一个以上的WebServiceInput活动都把这个属性设置为true,那么会有不止一个的Web服务调用也能启动工作流。假如两个WebServiceInput活动共用一个session cookies,那么第二个调用会被阻塞(等待),一直到第一个完成。 |
MethodName | 获取或者设置被暴露为Web方法的方法名称。这个方法也必须是指定的接口所定义的方法。 |
IsActivating可以让你能控制次Web服务对你的工作流进行调用。主调用,也就是启动工作流的Web服务方法调用,应该把它的 IsActivating设置为 True。假如你的工作流允许客户端进行次调用,就像在基于状态的工作流中接受输入事件一样,次 WebServiceInput活动应该把 IsActivating设置为 False否则会导致工作流重启。假如你允许你的工作流启动第二个实例,如果它和第一个工作流实例共享同一个 session状态的话,第二个实例将会被阻塞并可能会导致死锁。因此,使用多个activating的 WebServiceInput活动是非常谨慎的。
当你指定 MethodName时要记住,和该方法相关的方法参数需要进行绑定或者以其它方式指定。在Visual Studio中,当你为 MethodName选择了一个方法后,该方法的参数会自动为你在活动的属性面板上呈现出来,你可以使用贯穿本书已经用过的属性绑定对话框对这些参数进行绑定。当开始学习本章的范例应用程序时你也将看到这些内容。
使用WebServiceOutput活动
WebServiceOupput活动通过返回WebServiceInput活动中的MethodName的值来完成XML Web服务的处理。出于这个因素,你必须指定WebServiceOutput的InputActivityName属性为你工作流中所存在的WebServiceInput活动中的一个。错误地指定输入活动的名称会导致验证失败和编译错误。
WebServiceOutput只有一个方法参数要进行绑定:如果有的话,它正是接口中所标出的,绑定到和本输出活动相联系的WebServiceInput活动的方法的返回结果。如果该方法返回的是void,则没有要绑定的返回值。然后WebServiceOutput活动会迫使为这个Web方法调用发送工作流处理结束的信号。
有趣的是,对于一个输入你可以有多个输出结果。例如,假如多个输出活动被放到了Parallel活动的各个执行路径中,或者被放在IfElse活动的不同分支内,这些都可能导致上述的情况。通过你工作流的不同路径就可能导致不同的输出结果。你不能在一条执行路径上放置多个输出活动。(对于WebServiceFault活动也是一样。)假如WF识别出在同一条执行路径上有一个以上的WebServiceOutput或者WebServiceFault与WebServiceOutput之间的组合的情况,WF会使这些活动无效,并且你需要通过移开或者删除那些违反规则的活动来纠正执行逻辑上的错误。
使用WebServiceFault活动
WebServiceFault活动和WebServiceOutput活动关系密切。它们二者碰巧都象征着对特定调用进行处理的工作流的终止。而且事实证明,WebServiceFault可以像WebServiceOutput一样去使用。
WebServiceFault也只和一个WebServiceInput活动发生联系。就像它的近亲WebServiceOutput活动一样,WebServiceFault也有一个唯一的输出属性Fault需要你必须去进行绑定,Fault要被绑定到一个基于System.Exception的字段或者属性,它代表了要向客户端报告的异常。最终,Fault会被转换成一个SoapException并通过通信线路发送给客户端。但是,允许你去绑定任何你想要的异常。ASP.NET会自动地把你提供的异常转换为SoapException。
创建一个Web服务项目
本书的这最后一个应用程序范例我们将完全从零开始创建。我们通过创建一个简单的控制台应用程序来起步,它不需要引用WF程序集。这是因为工作流将由ASP.NET XML Web服务承载,我们将会在接下来的一节创建这个工作流程序集。在准备好工作流程序集后,我们将把该工作流发布成一个ASP.NET Web服务项目并做一些调整,接着会在这个原始的控制台应用程序中添加调用该工作流的代码。
创建基本工作流应用程序
1.打开Visual Studio,依次点击“文件”、“新建”、“项目”菜单项。
2.当打开“新建项目”对话框后,在“项目类型”面板中选择“Windows”。然后在模板列表中选中“控制台应用程序”。
3.在“名称”字段中输入QuoteGenerator,在“位置”字段中输入\Workflow\Chapter19。然后点击“确定”。
这会创建你将用来测试XML Web服务的控制台应用程序和一个你可以继续添加更多项目的解决方案文件。
添加顺序工作流库
1.在Visual Studio的解决方案资源管理器上右键点击QuoteGenerator解决方案的名称,这将弹出其快捷菜单。从快捷菜单中依次点击“添加”、“新建项目”。
2.这会打开“添加新项目”对话框。如果Visual C#树状控件节点还没有展开的话请展开它,然后在项目类型面板中选择“Workflow”并从模板列表中选择“顺序工作流库”。在“名称”字段中输入GeneratorFlow,最后点击“确定”。
3.Visual Studio会在QuoteGenerator解决方案中添加该顺序工作流项目并为你自动打开工作流视图设计器。在创建工作流前,我们先来创建你将会用到的接口。为此右键点击GeneratorFlow项目,然后在呈现的菜单中依次点击“添加”、“类”。当打开“添加新项”对话框后,在“名称”字段中输入IGenerateQuote.cs并点击“确定”。
4.修改Visual Studio为你自动创建的该类的定义,内容如下:
public interface IGenerateQuote
5.为IGenerateQuote添加下面的方法,然后保存该文件:
decimal GetQuote(string symbol);
6.现在你创建好了一个接口,然后在GenerateFlow项目中选中Workflow1.cs,并在解决方案资源管理器的右键菜单上点击“视图设计器”回到视图设计器界面上来。
7.从Visual Studio的工具箱中拖拽一个WebServiceInput活动到你的工作流定义中。
8.现在我们来设置webServiceInputActivity1的属性。首先点击InterfaceType属性去激活浏览(...)按钮。然后点击该浏览按钮,这会打开“浏览并选择 .NET 类型”的对话框。GeneratorFlow.IgenerateQuote应该已经被添加到“类型名称”字段中因为它属于当前的项目并且也只有这一个接口。最后点击“确定”。
9.点击IsActivating属性去激活其下拉箭头。点击该下拉箭头,从列表的可选项中选择True。
10.选中MethodName属性去激活其下拉箭头。点击该下拉箭头,选中其唯一的一个选项:GetQuote。一旦你选中它后,请注意,symbol属性会被添加到该活动的属性中。
11.选中symbol属性会像前面一样呈现出你熟悉的浏览(...)按钮。点击该浏览按钮打开“将‘symbol’绑定到活动的属性”对话框。点击“绑定到新成员”选项卡,在“新成员名称”中输入Symbol。你需要确认你选中的是“创建属性”选项,最后点击“确定”。该webServiceInputActivity1活动此时会指出存在验证错误(也就是说,仍然呈现出一个包围了惊叹号的红色小圆圈)。这是因为你的输入和输出(活动)没有成对,你很快将解决该问题。
12.我们现在需要在XML Web服务第一次执行的时候做一些初始化的工作,因此在webServiceInputActivity1的InputReceived属性中输入CreateStocks,然后按下回车键。Visual Studio会自动为你插入CreateStocks事件处理程序并为你自动切换到代码编辑状态下。你需要回到工作流视图设计器界面上来继续完成该工作流。
13.从工具箱中拖拽一个Code活动到webServiceInputActivity1的下面。在它的ExecuteCode属性中输入UpdateMarketValues。一旦Visual Studio为你插入UpdateMarketValues事件处理程序后再次回到工作流视图设计器界面上来。
14.接下来要插入的工作流活动是IfElse活动。把它拖拽到设计器界面上来并放到你刚刚放入的Code活动的下面。
15.选中ifElseActivity1的左边的分支,点击它的Condition属性去激活其下拉箭头。从它的选项列表中选择代码条件然后展开其后面的加号(+),这会出现另一个Condition属性,然后在第二个Condition属性中输入TestKnownStock并按下回车键。一旦在Visual Studio再次为你添加完事件处理程序后,你需要重新回到工作流视图设计器界面上来。
16.拖拽一个Code活动到设计器界面上,把它放到ifElseActivity1的左边分支中。在它的ExecuteCode属性中输入RecordStockValue,在Visual Studio添加了其事件处理程序后重新回到工作流视图设计器界面上来。
17.现在拖拽一个WebServiceOutput活动到你刚刚插入的Code活动的下面。
18.指定webServiceOutputActivity1的InputActivityName属性为webServiceInputActivity1,方法是点击InputActivityName属性去激活其下拉箭头,然后点击该下拉箭头选中webServiceInputActivity1活动。注意(ReturnValue)会被添加到webServiceOutputActivity1的属性列表中。
19.因为我们想返回某只股票的价值,因此我们需要把某个工作流绑定到webServiceOutputActivity1的(ReturnValue)属性。选择这个(ReturnValue)属性去激活其浏览(...)按钮,然后点击该浏览按钮。当“将‘(ReturnValue)’绑定到活动的属性”对话框打开后,选择“绑定到新成员”选项卡并在“新成员名称”中输入StockValue。确保选中的是“创建属性”选项,然后点击“确定”。
20.你最后一个要添加的活动是WebServiceFault。拖拽一个WebServiceFault活动到工作流视图设计器界面上并把它放到ifElseActivity1的右边分支中。
21.就像webServiceOupputActivity1一样,你需要为webServiceFaultActivity1指定InputActivityName属性。选中它的InputActivityName属性去激活其下拉箭头,然后从列表中选择webServiceInputActivity1选项。(只有这唯一的一个选项可供选择。)
22.你接下来需要为webServiceFaultActivity1提供恰当的错误(fault)处理,因此选中它的Fault属性去激活其浏览(...)按钮。点击该浏览按钮打开“将‘Fault’绑定到活动的属性”对话框。点击“绑定到新成员”选项卡并在“新成员”名称中输入StockFault。确保选中的是“创建属性”选项,然后点击“确定”。
23.该工作流现在在视图设计器上的设计工作就已经完成了,现在要打开Workflow1.cs文件进行代码编辑。在解决方案资源管理器中选中Workflow1.cs文件,然后点击“查看代码”工具条按钮。
24.首先要添加的代码是两个可让我们访问ASP.NET的using语句。把它们添加到现有using语句的后面:
using System.Web.Caching;
25.接下来让我们来完成CreateStocks事件处理程序。找到CreateStocks,然后为该方法添加下面的代码:
System.Collections.Generic.Dictionary<string, decimal> stockVals =
HttpContext.Current.Cache["StockVals"] as
System.Collections.Generic.Dictionary<string, decimal>;
if (stockVals == null)
{
// Create and cache the known stock values.
stockVals =
new System.Collections.Generic.Dictionary<string, decimal>();
stockVals.Add("CONT", 28.0m);
stockVals.Add("LITW", 22.0m);
stockVals.Add("TSPT", 24.0m);
// Add to the cache.
HttpContext.Current.Cache.Add("StockVals", stockVals, null,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Normal, null);
} // if
26.找到UpdateMarketValues事件处理程序。为UpdateMarketValues处理程序添加下面的更新市值的模拟代码:
// Iterate over each item in the dictionary and decide
// what its current value should be. Normally we'd call
// some external service with each of our watch values,
// but for demo purposes we'll just use random values.
//
// Note this is essentially the same simulation code as
// found in Chapter 10
Random rand = new Random(DateTime.Now.Millisecond);
System.Collections.Generic.Dictionary<string, decimal> currentStockVals =
HttpContext.Current.Cache["StockVals"] as
System.Collections.Generic.Dictionary<string, decimal>;
System.Collections.Generic.Dictionary<string, decimal> newStockVals =
new System.Collections.Generic.Dictionary<string, decimal>();
foreach (string key in currentStockVals.Keys)
{
// Pull the item's value
decimal currentPrice = (decimal)currentStockVals[key];
// Set up the simulation
decimal newPrice = currentPrice;
decimal onePercent = currentPrice * 0.1m;
Int32 multiplier = 0; // no change
// We'll now roll some dice. First roll: does the
// market value change? 0-79, no. 80-99, yes.
if (rand.Next(0, 99) >= 80)
{
// Yes, update the price. Next roll: will the
// value increase or decrease? 0-49, increase.
// 50-99, decrease
multiplier = 1;
if (rand.Next(0, 99) >= 50)
{
// Decrease the price.
multiplier = -1;
}
// Next roll, by how much? We'll calculate it
// as a percentage of the current share value.
// 0-74, .1% change. 75-89, .2% change. 90-97,
// .3% change. And 98-99, .4% change.
Int32 roll = rand.Next(0, 99);
if (roll < 75)
{
// 1% change
newPrice = currentPrice + (onePercent * multiplier * 0.1m);
}
else if (roll < 90)
{
// 2% change
newPrice = currentPrice + (onePercent * multiplier * 0.2m);
}
else if (roll < 98)
{
// 3% change
newPrice = currentPrice + (onePercent * multiplier * 0.3m);
}
else
{
// 4% change
newPrice = currentPrice + (onePercent * multiplier * 0.4m);
}
}
else
{
// No change in price
newPrice = currentPrice;
}
// Update the data store
newStockVals.Add(key, newPrice);
}
// Add to the cache
HttpContext.Current.Cache["StockVals"] = newStockVals;
27.我们需要添加验证股票代码的逻辑,因为它可能不是我们所能识别的代码。搜索Workflow1.cs代码找到TestKnowStock事件处理程序,然后为该方法体添加下面的代码:
// Retrieve the cached stock values.
System.Collections.Generic.Dictionary<string, decimal> stockVals =
HttpContext.Current.Cache["StockVals"] as
System.Collections.Generic.Dictionary<string, decimal>;
// Check to see if the ticker symbol is in the
// known stock list. Fault if not
e.Result = true;
if (String.IsNullOrEmpty(Symbol) || !stockVals.ContainsKey(Symbol))
{
// We don't have the desired stock in our list,
// so return a fault.
e.Result = false;
StockFault = new System.Exception("The desired stock ticker symbol" +
" is unknown.");
}
28.你最后要添加的几行代码是把股票市值指定给返回值属性。RecordStockValue方法对此进行处理,因此定位到RecordStockValue方法并添加下面的代码:
// Place updated stock value in the dependency property
// for return to the caller.
System.Collections.Generic.Dictionary<string, decimal> stockVals =
HttpContext.Current.Cache["StockVals"] as
System.Collections.Generic.Dictionary<string, decimal>;
StockValue = (decimal)stockVals[Symbol];
29.该工作流现在就完成了。保存所有打开的文件并编译该工作流。在继续之前纠正任何你可能遇到的编译错误。
工作流现在就做好了集成进ASP.NET中去的准备,以便外部客户端能连接到该服务并发出请求获取股票市值。尽管你可以创建一个新的ASP.NET应用程序并亲自进行这些所有魔术般的配置,但WF团队热心地为你提供了一个极好的生成ASP.NET项目的功能,这不会出现差错,而且使用起来也相当容易。
创建ASP.NET Web应用程序
1.这是如此的容易,他们应该考虑给想出这个主意的WF开发人员加薪。直接右键点击解决方案资源管理器中的GeneratorFlow项目,然后选择“作为 Web 服务发布”即可。然后Visual Studio将弹出一个确认对话框。点击“确定”关闭该确认对话框。
2.这简直太容易了,让我们来重命名这个.asmx文件。右键点击解决方案资源管理器中最新生成的GeneratorFlow.Workflow1_WebService.asmx文件的名称,然后选择“重命名”菜单项。把它的名称修改为QuoteService.asmx。
3.按下Shift+F6或者在“生成”主菜单中选择“生成 Web 站点”。这一步骤可以不需要,但是它将预编译该Web应用程序,这样,当你引用它去检索它的WSDL的时候,你将在此时节约一些时间。假如存在任何错误,你应该纠正它们,但你不应该会发现有什么错误。假如你收到什么警告信息的话,选择忽略它们。
最后的任务是回到最初创建的程序文件中,向其所在的项目添加Web引用并测试该XML Web服务。
创建XML Web服务客户端应用程序
1.在解决方案资源管理器中右键点击QuoteGenerator项目的名称,然后选择“添加 Web 引用”。
2.当“添加 Web 引用”对话框打开后,点击“此解决方案中的 Web 服务”链接。
3.因为在QuoteGenerator解决方案中只有唯一的一个XML Web服务,因此只会出现QuoteService XML Web服务这唯一的一个服务。点击QuoteService链接继续前进。
4.Visual Studio会显示它所找到的QuoteService XML Web服务的方法。在这个例子中,只有这唯一的一个方法。假如你乐意的话,你可以点击该方法进行测试。现在,我们点击“添加引用”把该Web引用添加到QuoteGenerator项目中。
备注:“Web 引用名”中包含了localhost字符串。假如你有多个Web引用,你可把它修改为更富有含义的字符串以便更加容易维护并对Web服务进行定向。
5.添加Web引用后,打开Program.cs文件进行编辑。在解决方案资源管理器中选中Program.cs文件,然后点击“查看代码”工具条按钮。
6.寻找Main方法。为其添加下面这些代码:
// Create the proxy.
QuoteGenerator.localhost.Workflow1_WebService ws =
new QuoteGenerator.localhost.Workflow1_WebService();
try
{
// Call the service a few times to
// test its logic.
decimal val = ws.GetQuote("CONT");
Console.WriteLine("The latest value for CONT is: {0}",
val.ToString("C"));
val = ws.GetQuote("LITW");
Console.WriteLine("The latest value for LITW is: {0}",
val.ToString("C"));
val = ws.GetQuote("TSPT");
Console.WriteLine("The latest value for TSPT is: {0}",
val.ToString("C"));
// Error test
val = ws.GetQuote("ABC");
Console.WriteLine("The latest value for ABC is: {0}",
val.ToString("C"));
}
catch (Exception ex)
{
Console.WriteLine("Error checking stock value: {0}", ex.Message);
}
7.编译该解决方案,纠正任何编译错误。
8.按下Shift+F5或者直接按下F5以调试模式来执行本应用程序。你应看到下面的输出结果。假如输出结果滚动得太快,控制台窗口在你能看到输出之前就被关闭的话,可以在Main方法中设置一个断点并一步一步地前进,直到你看到所有的输出结果。
本章源代码:里面包含本章的练习项目和完整代码
全书完