一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t...

安卓手机的朋友们,没事赚点小钱的软件 http://ijubaopen.com:99/ijubaopen.apk 邀请码:egwqnb9,支持一下博主吧
 
 
本章涵盖
■处理故障和调试工作流
■工作流版本
■使用工作流的事件接收器
■接收外部事件可插拔服务
■工作流对象模型工作

 

在本书的前11章都是讨论的SharePoint开发基本原理。我们讨论开箱即用SharePoint工作流,其他无需开发技术SharePoint Designer和Office Visio,Visual Studio工作流和自定义表单。第12章你围绕SharePoint开发技能的几个重点技术。 

这些技术包含如何调试和处理错误在你的工作流,工作流版本,设置事件接收器,发送和接收外部事件,SharePoint工作流对象模型的工作。调试和异常处理明显必须,但是版本不是很好。如果你工作流不正确的版本,闲置的工作流实例可能会打破当他们恢复执行时。
   为什么要学习事件接收器在一个工作流书中?事件接收器能节省你的时间如果他们适合业务需求。事件接收通常更容易并更快的去创建规模小,一次性处理类似工作流程。
发送和接收时间从外部数据源是常常需要一个大的业务流程。这是处理通过一个新的feature在SharePoint2010叫做可插拔(pluggable)工作流服务。使用两个活动和一个新类叫SPWorkflowDataExchangeService,你看轻松沟通和你的组织的业务应用程序。

最后,每个开发人员应看到工作流对象模型在Microsoft.SharePoint.Workflow命名空间。你可能需要去编程方式去开始或者停止一个工作流或者检索一个工作流任务或者历史。 

12.1  错误处理和调试工作流(Fault handling and debugging workflows)

如果你不妥善处理工作流中的异常和发生的错误,你会获得可怕的错误字符串在工作流状态,出了问题没有线索。你可以调试你的工作流,如果你不知道如何调试,你将陷入困境。 

 让我们开始调试。你调试你的工作流和其他你建立的.NET应用程序几乎一样。在你的工作流的代码视图,右击你想开始调试的代码行并选择断点 ->插入断点(图12.1)。另外请注意你可以调试模板上的活动。要做到这点,右击你想调试的活动,并选择断点->插入断点。
一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第1张图片12.1

 

接下来要做的是附加到Visual Studio调试去到W3WP.exe sharepoint进程(图12.2)。在Visual Studio,点击调试菜单下拉列表,选择附加到进程。向下滚动并选择w3wp进程并点击附加。如果有多个w3wp进程,选择所有显示的。如果你没看到进程,导航到SharePoint站点去开始进程,然后回到进程列表并点击刷新按钮。此外还要确保选中所有绘画过程。你附加后,你可以开始你的工作流,然后Visual Studio将自动步进到调试器. 

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第2张图片 图12.2

 

 异常处理与你的标准.NET应用程序稍微有点不同。当你在一个工作流模板,没有明显的地方来添加一个Try/Catch块。许多开发人员没有处理异常和花费时间去调试并试图找到发生错误的位置。最好的办法是使用FaultHandler活动在你的工作流根上(绿色箭头)以及你所有的复合活动(顺序,并行,IfElse,等等)。

在FaultHandler活动,你可以插入活动以适当的方式处理错误。在SharePoint,通常去记录错误,位置和堆栈跟中到工作流的历史记录。这使得确定错误的位置更容易。 
至少,每个工作流应该有一个Fault Handler设置在工作流模板的根。要做到这点,单击下拉列表帮的绿色箭头,然后选择故障处理(Fault Handler)。

 在故障处理部分,你可以拖放一个或多个FaultHandler活动,每个活动有一个属性叫FaultType,你需要去设置这个属性到一个你想去处理的异常类型。为了处理所有异常,设置它为System.Exception,这是最通用的异常(图12.3)。另外,你可以设置它为一个自定义异常去处理你的工作流特定的错误。这是最好的办法;它使调试更加容易以为内你将知道何时为何你的自定义异常被抛出。

 一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第3张图片图12.3

 你指定异常去处理之后,你需要添加动作去对错误进行反应。使用Log to History List活动去记录一个详细的描述错误会有帮助,并跟踪堆栈以帮助调试(图12.4)。

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第4张图片 图12.4

 12.2  工作流版本(Versioning workflows)

你建立Visual Studio工作里并部署它到生产环境。但是,几个月后,工作流的业务需求有一个小小的变化。你回到工作流代码,添加一些活动去满足请求,并重新部署工作流到生产环境。因为改动,所有工作流开始破坏!你有点疯了因为你肯定你充分unit-test改变并且没有可能错误。你没有版本管理你的工作流。 

工作流版本管理是一个重要的技术。当一个工作流进入空闲状态,工作流的状态保存到数据库。这个工作流状态保存叫做水化。当工作流回复,状态从数据库中脱水,工作流开始处理。版本孔氏是重要的因为如果你更改程序集当工作流是水化(保存进数据库),它们不能保证工作流,当工作流脱水,它将匹配新程序集构造。如果它不匹配反序列化构造,工作流将挂掉。更改像添加或删除活动或者更改不断变化的属性值。最好的做法是创建一个新的版本每次进行部署程序集。 

思考一下一个新工作流的版本。最基本得技术使你的程序集每次编译时版本号码进行增加。(而不是永远在1.0.0.0)。然后,每次升级,你创建一个新的feature为工作流版本,指向一个新的程序集。你添加一个新的程序集到全局程序集缓存(GAC)在旧程序集旁。最后你指定旧版本无法启动工作流新实例,然后你添加工作流到列表上。这样,旧版本的程序集没有改变,所以没有打破水化工作流当他们脱水的风险。你部署另外一个版本饿程序集并添加新的工作流到列表上,禁用之前的版本。你不想去移除之前版本因为它将孤立这些运行的实例。全套设置程序,按照表12.1的步骤去创建一个新的版本为已存在的工作流。

 老版本重写

如果你不创建一个新的版本,只是升级的解决方案,所有运行的工作流实例将被删除。旧版本的工作流将被删除,新版本将添加0次运行的实例。不要升级,应创建一个新版本除非你完全肯定你不需要保留正在运行的实例。

 表12.1

 一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第5张图片

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第6张图片 

 

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第7张图片 图12.5

 

 12.3 (建立工作流事件接收器)Building workflow event receivers

你可能马上想到你需要一个工作流,事实上一个事件接收器将去做的事情。 两者的区别是工作流是通常长时间运行的,事件接收器是立竿见影的。为什么你会想到事件接收器呢?如果你要做的是执行一段代码,当一个文件被删除。当用户将文件删除你想用代码进行归档文件。这个例子展示了如何用事件接收器是因为删除事件触发你的自定义活动代码。你可以使用Visual Studio工作流的唯一一个活动,但是对于大量的开销事件接收器不好用。表12.2显示工作流和事件接收器的比较。 
表12.2

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第8张图片 

 

你不能说哪个好。这完全取决于你的业务需求。此外,文件被删除时,还有许多其他事件可以回应。事件分成六大类,如表12.3所示。每个类具有几个比较常见的事件,但要注意还有更多的可用事件。 

 表12.3

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第9张图片 

 你在这个表中注意到两件事情。首先,有很多时间你可以自定义代码响应,其次许多时间有之前和之后事件关联。如图12.6,之前事件发送之前更改提交到SharePoint内容数据库。这是帮助你当你想去取消一个更改在保存之前。这个事件接收器为当一个站点开始删除的好例子。站点删除之前,你可以做一些额外的处理如备份等。

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第10张图片 图12.6

 为了演示如何建立事件接收器,你将去使用工作流事件作为实力,表12.3显示有四个事件在列表工作流事件种类中。你可以响应当一个工作流是starting,started,postponed,和completed。为了保证示例简单,让我们写一个事件接收器创建一个announcement当一个新的日历事件创建时。

在Visual Studio2010开始创建一个新的项目。你会发现在SharePoint选项卡有一个新的项目模板叫事件接收器(图12.7)。你可以使用此模板去创建任何的前面提到的事件接收器。 

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第11张图片 图12.7

你创建好项目之后,你将获得一个对话框问你去指定你想去部署和Unit-test你的站点接收器的站点URL,如果你想去选择一个完全信任(farm)或者沙盒解决方案。指定URL,然后选择farm解决方案因为我们示例代码需要完全信任。点击下一步,你将被提示指定你想创建的(图12.8)事件接收器的类型。下拉列表框将包含六种所有事件类型除了feature事件种类。feature事件创建通过右击项目中的feature创建。对于示例,选择列表工作流事件种类。 

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第12张图片 图12.8

 

 你选择事件种类之后,指定事件源。这是告诉feature你将去创建你想去响应的事件。注意在图12.9你可以处理事件从announcements(公告),document libraries,和其他列表和library选项。选择Calendar(日历)源。

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第13张图片 图12.9

接下来,你指定哪个工作流事件你想去处理。指定工作流有开始事件。现在,你的代码将每次执行在一个日历上工作流启动时(图12.10)。 

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第14张图片 图12.10

 

 当你创建项目后,你将被发送一个方法名字叫Workflow-Stared.这是你可以添加你的代码到创建公告。输入列表12.1的代码创建公告。

 

Listing 12.1 Event receiver that creates an announcement 

/*************************************************/

if (properties.ActivationProperties.List.Title ==  " Main Calendar ")
{
string siteurl = properties.ActivationProperties.Site.Url;
SPSecurity.RunWithElevatedPrivileges( delegate()
{
using (SPSite site =  new SPSite(siteurl))
{
using (SPWeb web = site.RootWeb)
{
SPListItem item = web.Lists[ " Announcements "].Items.Add();
item[ " Title "] =  " The workflow has started! ";
item.Update();
}
}
});

} 

 

首先,此代码块激活属性去确认哪个日历的事件来源。记住你的源是日历,这意味着任何站点上的日历将抛出此事件当工作流Started。如果你有一个以上的日历在站点,你将想去确认来自哪个日历事件 。

接下来,提升运行用户的服务账户。你不知道运行用户对公告的权限,所以你提升他的权限。接下来,你创建公告,指定表体制,并提交公告到数据库。 

有了这个代码,你部署解决方案。右击项目名字,点击部署。这将部署feature和程序集。接下来,创建一个新日历项,并开始一个工作流。因此一个新的公告将显示在该网站的公告列表上。 

 

12.4 可插拔工作流服务(Pluggable workflow services )

可插拔工作流服务是SharePoint2010最受期待的工作流功能之一。这是因为SharePoint2007工作流缺乏与外界沟通的能力。最基本的情况就是工作流进入空闲状态,等待一个独立系统的消息,像一个业务应用程序如客户管理系统。另外渴望内部工作流通信技术,当一个工作流需要发送一个消息到另外的工作流。第三是在长期运行处理。如果你有一个计算或者一个服务调用了30分钟来执行,将需要保证工作流实例在内存中没有意义。当它处理完成它可能脱水。 

 所有这些例子不容易在SharePoint2007上实现。现在windows workflow foundation在.NET 3.5 Framework有能力去满足这些需求,通过工作流通信服务。由于SharePoint2007是3.5 Framework,你会觉得它不是问题。由于SharePoint托管提供者,他们没有提供方便的类获取工作流实例和抛出事件到那些实例工作流可以监听。这在2010中有所变化,引入新的类,SPWorkflowExternalDataExchangeService。 

 正如在SharePoint2010工作流,在工作流通信服务,你可以创建一个本地服务,你的工作流可以使用来互相通讯。使用CallExternalMethod活动和HandleExternalEvent活动,sharePoint工作流和.NET应用程序可以相互发送和接收(图12.11)。

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第15张图片 图12.11

 

我们进入如何设置一个本地服务使用SharePoint外部数据交换服务之前,让我们简要的谈一下例子。示例你要建立一个Hello world。一个工作流要去说Hello Event Handler!通过创建一个新的公告列表通过创建一个新的公告去创建一个事件接收器。然后事件接收器去说Hello Workflow!回到工作流。 

要做到这点,工作流将调用一个本地服务。本地服务然后创建一个通知在公告列表。然后一个事件接收器响应新的通知通过抛出一个事件通过本地服务,工作流监听。这个例子将演示SharePoint工作流可以沟通.NET应用程序。按照表12.4的步骤去建立HELLO World!可插拔工作流服务。 

 表12.4

 

一个本地服务由两部分组成,一个服务接口和一个服务类。服务接口允许发送和接送方知道什么类型的数据发送给对方。这是通过声明一个发送人调用的方法和一个事件接收器监听。要挖掘外部数据交换服务,接口必须声明ExternalDataExchange特性。 

 表12.4 续

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第16张图片 

 

 

Listing 12.2 HelloHost local service method 

 

/****************************************************/

public  event EventHandler HelloWorkflow;
public  void HelloHost( string message)
{
SPWeb web =  this.CurrentWorkflow.ParentWeb;
SPList list = web.Lists[ " Announcements "];
SPListItem item = list.Items.Add();
item[ " Title "] = message;
item[ " Instance "] = WorkflowEnvironment.WorkflowInstanceId.ToString();
item.Update();

}

 

在HelloHost方法你想去创建两件事,事件处理和接口中定义的方法。在这个方法中,你创建一个新的公告和传递工作流实例ID到公告Instance列。这是事件接收器将知道去哪个工作流发送消息。 

添加完列表12.1并在第二次和第三活动的代码,你可能会注意到编译器无法找到HelloWorldEventArgs类。这个类还没定义,但是它将允许你的事件接收器去发送一个自定义消息给你的工作流。 

续12.4表

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第17张图片 

 

注意你的自定义参数取两个值,一个GUID将存储工作流的实例ID和答案。此消息最终会被记录到工作流的历史列表。 

 有件事你必须在你的本地服务前完成,你可以建立工作流和事件接收器。你需要添加三个方法以满足SPExternalDataExchangeService接口要求。

续12.4表

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第18张图片 

 

 Listing 12.3 SPExternalExchangeService interface methods

/****************************************************/

public  override  void CallEventHandler(Type type,  string eventName,
object[] parameters, SPWorkflow workflow,  string identity,
System.Workflow.Runtime.IPendingWork handler,  object item)
{
switch (eventName)
{
case  " HelloWorkflow ":
var args =  new HelloWorldEventArgs(workflow.InstanceId);
args.Answer = parameters[ 0].ToString();
this.HelloWorkflow( null, args);
break;
}
}
public  override  void CreateSubscription(
MessageEventSubscription subscription)
throw  new NotImplementedException(); }
public  override  void DeleteSubscription(Guid subscriptionId)

throw new NotImplementedException(); } 

 

 

CallEventHandler方法是调用每次事件在本地服务的请求。首先,你检查哪个事件是被请求的。如果它是你的Helloworkflow事件,你创建一个新的HelloworldEventArgs实例并传递工作流的实例ID。这将让事件知道哪个工作流去调用事件。接下来,你传递字符串消息从事件接收器,最后调用事件。

现在本地服务已完成,你可以开始建立工作流,事件接收器进行工作。继续表12.4的步骤去建立工作流和事件接收器。 

续表12.4

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第19张图片 

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第20张图片 

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第21张图片 

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第22张图片 

 

Listing 12.4 ItemAdded event receiver method 

/************************************************/

if (properties.ListTitle ==  " Announcements ")
{
Guid instance =  new Guid(properties.ListItem[ " Instance "].ToString());
string answer =  " Hello Workflow! ";
SPWorkflowExternalDataExchangeService.RaiseEvent(
properties.Web, instance,  typeof(IHelloWorldService),
" HelloWorkflow "new  object[] { answer });

} 

 

  这个列表代码首先获取工作流的实例ID从Instance列。这个GUID作为RasieEvent方法的参数。这是RaiseEvent方法知道 哪个工作流发送消息。其他参数是工作流运行在哪个站点,事件调用(HelloWorld事件),和你给工作流的消息。最后参数是一个对象数组, 这样你可以装入任何可序列化对象。

 在测试之前,注册你的本地服务到运行时。你要添加Web.config的条目。按照最后步骤去注册你的服务。

表12.4续

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第23张图片 

 

开始测试,生成并部署你的项目。导航到你的SharePoint站点,在站点内容视图下面,点击站点工作流。开始你的可插拔工作流 (应该命名为PluggableWorkflowService-workflow1)。导航到公告列表,你应该看到一个新的公告叫Hello Event Handler! 见图12.12. 

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第24张图片 图12.12

 

回到站点工作流,点击Completed状态,你应该看到事件接收器响应HelloWorkflow(图12.13)。 

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第25张图片 图12.13

 

 12.5 SharePoint workflow object model

SharePoint工作流对象模型在Microsoft.SharePoint.Workflow命名空间。你可以利用此对象模型对你的工作流进行编程。你可以启动和停止一个工作流,检查一个工作里的状态或者历史,或者检索一个任务列表关联的工作流。本节将介绍工作流中对象模型的常见用途。参照完成的SDK在www.msdn.microsoft.com.命名空间有很多类,但是按照以下前两名: 

 ■ SPWorkflow—此类描述一个工作流在站点或者项上的实例。它能被使用去看见谁开始工作流(Author属性)和获取工作流的状态.

■ SPWorkflowManager—此类有许多的helper方法的类,你可以在工作流上使用。最有用的方法包含在以下: 

– GetItemActiveWorkflows  

– GetItemWorkflows

– GetWorkflowTasks

– RemoveWorkflowFromListItem

– StartWorkflow

 

虽然这些类很有用,他们协同工作在一个host在同一个命名空间的其他类中。表12.5显示类的列表和他们的SDK描述。 

 表12.5

一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t..._第26张图片 

 

 现在你有一个高级别的类,让我们看看几个常见的使用工作流对象模型。下面的代码片段显示五种常见的例子。

第一个片段显示编程的方式启动工作流:
/*************************************************/
foreach (SPWorkflowAssociation association in
splistitem.ParentList.WorkflowAssociations)
{
if (association.AllowManual)
{
splistitem.Web.Site.WorkflowManager.StartWorkflow(
splistitem, association, association.AssociationData, true);
}
 
这个代码片段首先获取所有列表中关联的工作流。这可能是比较简单的一个内容类型或者站点为站点工作流。SPWorkflowAssociated对象包含属性例如工作流启动选项。接下来,代码块检查是否允许授通过用户界面手工启动。如果是这样,通过工作流管理的Startworkflow方法。 
第二个片段显示如何停止工作流:
SPWorkflow workflow = splistitem.Workflows[1];
web.Site.WorkflowManager.RemoveWorkflowFromListItem(workflow);
 
停止工作流很简单。使用工作流管理并调用RemoveWorkflowFormListItem方法并传递你要终止的工作流。工作流再次帮助去检索列表上活动的工作流,像下面的片段: 
  SPWorkflowCollection runningWFs =
web.Site.WorkflowManager.GetItemActiveWorkflows(splistitem);
Console.WriteLine("Names of Running Workflows:");
foreach (SPWorkflow workflow in runningWFs)
{
Console.WriteLine(workflow.ParentAssociation.Name);
}
 
  工作流管理者的GetItemActiveWorkflows方法检索所有运行在项上的工作流集合。在项上的工作流包含所有的工作流,不管他们运行与否。有些可能已经完成或者出现故障。你可以选择使用GetItemWorkflows并传递给SPWorkflowFilter参数通过指定SPWorkflowState对象。通过使用过滤器和State,你可以只检索孤立的工作流。 激活工作流,你可能想获取工作流任务。下面的代码片段显示如何做到这点:
  SPWorkflow workflow = splistitem.Workflows[1];
Console.WriteLine("Titles of Workflow's Tasks:");
foreach (SPWorkflowTask task in workflow.Tasks)
{
Console.WriteLine(task["Title"].ToString());
}
  每个工作流的任务属性是一个SPWorkflowTaskCollection对象。你可以编译通过每个工作流任务去检索所有的任务。这可以选择通过工作流管理的GetWorkflowTasks方法,你可以传递进SPWorkflowFileter参数去过滤任务。对应的任务是工作流历史列表。下面的片段显示如何以编程的方法检索工作流历史:
SPWorkflow workflow = splistitem.Workflows[1];
SPList historyList = workflow.HistoryList;
SPQuery query = new SPQuery();
query.Query =
"" +
"" +
"{"+ workflow.InstanceId.ToString() +"}" +
"";
SPListItemCollection historyItems = historyList.GetItems(query);
foreach (SPListItem historyItem in historyItems)
{
Console.WriteLine(historyItem["Description"].ToString());
}
 
每个工作流有一个历史列表属性指向SPList对象。这个列表可以查询历史项目。查询通过SPQuery对象或者LINQ to SharePoint 完成。
输入CAML查询,并通过工作流实例ID,去获取工作流历史列表项。历史列表存储的描述列将被返回。

转载于:https://www.cnblogs.com/meiweijun/archive/2011/10/31/2230322.html

你可能感兴趣的:(一步一步学习sharepoint2010 workflow 系列第三部分:自定义SharePoint代码工作流 第12章 工作流开发技巧包 (A bag of workflow developer t...)