WF 从入门到精通(第十三章):打造自定义活动

学习完本章,你将掌握:

1.了解对于创建一个功能齐全的自定义工作流活动来说哪些组件是必须的

2.创建基本的自定义工作流活动

3.在基本的自定义工作流活动中应用验证规则

4.把基本的自定义工作流活动集成到Microsoft Visual Studio 的工作流视图设计器和

工具箱中

WF 并不可能涵盖到你可能在你的工作流中想要实现的各个方方面面。即使WF 对于开发

社区来说仍是非常新的技术,但目前已经可以获得许多免费发布的自定义活动,可以肯定商

业级的活动最终也会跟进。

在这章中,你将通过创建一个新的工作流活动来了解WF 的个中奥妙,这个活动从远程

FTP 服务器中检索文件。你将看到在创建你自己的活动时哪些东西是必需的,以及其中哪些

部分挺不错。你也将更深入地了解活动是怎样和工作流运行时交互的。

备注:只在一章中对自定义活动开发的每一个细节进行探讨是不可能,这儿简化了太多

的细节。不过好消息是,对于得到一个完整功能的活动来说是容易的,这不用知道每一个细

节。

关于活动的更多知识

在第四章(活动及工作流类型介绍)中,我们初步了解了一下活动并讨论了像

ActivityExecutionContext 之类一些话题,ActivityExecutionContext 用来容纳一些和正执

行的活动相关的一些信息,工作流运行时需要不时对这些信息进行访问。我们这里将对WF 活

动进行更深入一些的了解。

活动的虚拟方法

在创建自定义活动时首先需要了解的是基类为你提供了哪些虚拟的方法和属性。表13-1

显示了活动中被普遍使用的可重写的一些方法。(这里没有虚拟属性。)

表13-1 Activity 中被普遍使用的可重写的虚拟方法

方法 功能

Cancel 在工作流被取消时被调用。

Compensate

这个方法实际上并不来自于Activity 基类, 它实际上需要由

ICompensatableActivity 接口提供,许多活动都从该接口派生。因此,不管

出于什么目的和意图,都把它当作Activity 的方法。你将实现这个方法以

便对失败的事务进行补偿。

Execute 被用来执行活动要去完成的对应的工作。

HandleFault

在活动内部代码抛出一个未经处理的异常时被调用。注意一旦该方法被调

用将没有办法重启该活动。

Initialize 在活动被初始化时被调用。

OnActivityExecutionContextLoad

在活动完成了它的工作流程后被调用。当前执行上下文(current execution

context)正在转移到另一个活动。

Uninitialize 在活动要被反初始化时被调用。


在你的活动已经被加载到工作流运行时中但在执行之前的时候,假如你需要进行一些特定的处理工作,

一个极好的位置是在Initializze 方法中做这些事情。你或许也会在Uninitialize 方法中执行一些相似的处理

工作之外的事情。

OnActivityExecutionContextLoad 和OnActivityExecutionContextUnload 方法分别表示活动正加载到工作流

运行时中和活动正从工作流运行时中移走。在OnActivityExecutionContextLoad 被调用之前以及

OnActivityExecutionContextUnload 被调用之后,从WF 的角度来看,该活动是处于卸载状态中。它或许是

被序列化到一个队列中、保存进一个数据库中或者甚至是在磁盘上等待被加载。但在这些方法

(OnActivityExecutionContextLoad 和OnActivityExecutionContextUnload 方法)被调用之前或之后它并不存

在于工作流运行时之中。

Cancel、HandleFault 和Compensate 都在显而易见的条件(指取消、失败和补偿条件)激发的时候被调

用。尽管Compensate 真正用在执行你的事务补偿的地方(看看第15 章:工作流和事务),但它们主要的

用途都是去执行一些你想去执行的额外的工作(例如日志)。牢记这些方法被调用的时候都太晚了,因为

到你的活动被要求对失败进行补偿的时候,你不能对事务进行恢复;你也不能撤销一个未经处理的异常或

者终止一个取消(cancle)的请求。所有你能做的是去执行一些清理或者其它处理的请求,就Compensate

来说,实际上是为失败的事务提供补偿功能。

Execute 是最有可能被重写的Activity 的虚拟方法,这只不过是因为这个方法需要你重写以去执行活动应

当要去执行的工作。

活动组件

尽管毫无疑问你需要亲自去写自定义活动代码,完整开发的WF 活动都带有一些额外的

支持和工作流无关的行为的代码,但通常在工作流可视化设计器中 都为开发者提供了更丰富

的开发体验。例如,你可能想要提供一个验证器对象以便对不适当的活动配置进行检查并返

回错误信息;或者你可能需要提供一个ToolboxItem 或者ToolboxBitmap 以便更好地和Visual

Studio 工具箱集成。不管你是否相信,通过使用一个专门的设计器类来修改活动的主题,你

实际上能够调整你的活动放到工作流视图设计器中的呈现样式。在本章中的示例实现了所有

这些东西以对它们的功能和效果进行演示。

执行上下文(Execution Contexts)

你可能还记得,有两种类型的活动:基本(单一功能)活动和组合(容器)活动。你可

能会认为它们之间的主要区别是其中一个是单一的活动,而另一个能容纳可嵌入活动。这毫

无疑问是一个主要的区别。

但是还有其它重要的区别,尤其是活动在执行上下文(execution context)中怎样工

作这一点上。活动执行上下文在第4 章中介绍过,它是WF 去记载一些重要事情的一种简单方

法,就像是一个正在工作的活动来自于哪个 工作流队列一样。但它也为活动控制提供了一个

机制,为WF 在那些正执行的活动之间实施规则提供了一种手段。活动执行上下文的一个有趣

的地方是你的工作流实 例启动的上下文可能并不是你的自定义活动中正被使用的上下文。活

动执行上下文能被克隆并传给子活动,对于迭代(iterative)类型的活动来说总会发 生这

种情况。

但是对我们这里的目的而言,可能最重要的事情是要记住创建自定义活动的时候,至少

要记住活动执行上下文。活动执行上下文保存了当前的执行状态,并且当你重写了

System.Workflow.Activity 中的那些虚拟方法的时候,它只有某些状态值是有效的。表13-2

显示了哪些执行状态值能应用到System.Workflow.Activity 中的方法的重写中。Compensate

稍微有点例外,因为它不是System.Workflow.Activity 的虚拟方法,它来自于

ICompensatableActivity,可它由活动实现,就返回状态值而言这条规则仍然适用于

Compensate。返回任何无效状态值(例如从Execute 中返回

ActivityExecutionStatus.Faulting)其结果就是运行时抛出一个

InvalidOperationException。

表13-2 有效的执行状态

可重写的方

有效的返回执行状态

Cancel     ActivityExecutionStatus.Canceling 和ActivityExecutionStatus.Closed

Compensate     ActivityExecutionStatus.Compensating 和ActivityExecutionStatus.Closed

Execute     ActivityExecutionStatus.Executing 和ActivityExecutionStatus.Closed

HandleFault     ActivityExecutionStatus.Faulting 和ActivityExecutionStatus.Closed

Initialize

ActivityExecutionStatus.Initialized。和其它状态值不一样,在此时工作流活动被初始化,并没

有任何东西去关闭它,因此ActivityExecutionStatus.Closed 不是可选的。

通常,你要分别为这些虚拟方法的任务进行处理并返回ActivityExecutionStatus.Closed。返回其它另外的

有效值表明需要由工作流运行时或者一个包含它的活动(指它的父活动)来采取更进一步的行动(操作)。

例如,假如你的活动有子活动,当你的主活动的Execute 方法完成后还有子活动没有完成的话,主活动的

Execute 方法就应当返回ActivityExecutionStatus.Executing 。否则, 它就应该返回

ActivityExecutionStatus.Closed。

活动生命周期

那么这些方法是在什么时候由工作流运行时执行呢?表13-1 中的方法以下面的顺序被

执行:

1.OnActivityExecutionContextLoad

2.Initialize

3.Execute

4.Uninitialize

5.OnActivityExecutionContextUnload

6.Dispose

从工作流运行时的角度来看,OnActivityExecutionContextLoad 和

OnActivityExecutionContextUnload 界定了活动的生命周期。

OnActivityExecutionContextLoad 在一个活动刚刚被加载到运行时内存中的时候被调用,而

OnActivityExecutionContextUnload 在一个活动从运行时中删除的前一刻被调用。

备注:活动通常从反序列化过程创建而不是由工作流运行时直接调用构造器创建。因此,

假如你需要在创建活动的时候为其分配资源的话,OnActivityContextLoad 是做这件事情的

最好位置,而不是在构造器中。

尽管从内存的角度来说OnActivityExecutionContextLoad 和

OnActivityExecutionContextUnload 指示了活动的创建,但是Initialize 和Uninitialize

则表示活动在工作流运行时中执行的生命周期。当工作流运行时调用Initialize 方法的时

候,你的活动就准备就绪了。当Uninitialize 被执行的时候,从工作流运行时的角度来看你

的活动就已经完成了并准备从内存中移出。Dispose 这个.NET 对象的原型销毁方法对于释放

静态资源是很有用的。

当然,工作流并不能总是控制其中一些方法的执行。例如Compensate,它仅在一个可

补偿的事务失败时才被调用。这些剩下的方法实际上在Execute 时会被不确定地调用(不一

定会被调用)。

创建一个FTP活动

为了对本章中目前为止我所描述的一些东西进行演示,我决定创建一个活动,我们当中

许多写行业处理软件的人都希望找到的一个有用的东 西:FTP 活动。这个

FtpGetFileActivity 活动,使用.NET 中基于Web 的FTP 类来从远程FTP 服务器中检索文件。

使用这些相同的类 来把文件写到远程FTP 资源中也是可行的,但我把这样的活动作为练习留

给你去创建。

备注:我将以你知道(并正确地配置 过)FTP 站点的前提下开始我的工作。为了我们

此处的目的进行讨论,我将使用众所周知的IP 地址127.0.0.1 作为服务器的IP 地址(当然,

这代表的 是localhost)。你也可自由地把这个IP 地址替换为你喜欢的任何有效的服务器

IP 地址或者主机名。对于FTP 安全的问题和服务器配置方面的内容超 出了本章的范围,假

如你正使用的是IIS 并需要了解关于FTP 配置方面的更多信息的话,可看看

http://msdn.microsoft.com/en-us/library/6ws081sa.aspx。

为了宿主该FTP 活动,我创建了一个名称为FileGrabber 的示例应用程序(它的用户界

面如图13-1 所示。)。有了它,你就 能提供出一个FTP 用户帐户和密码以及你想检索的FTP

资源。我将下载的资源是一个Saturn V 运载火箭移到发射位置的图像文件,我已经在本书的

CD 中为你提供了该图片,你也可把它放到你的FTP 服务器上。假设你的FTP 服务器在你的本

机上,该图 片的URL 是ftp://127.0.0.1/SaturnV.jpg。假如你不使用我的图片文件,你就

需要修改你的本地服务器上所能获取的某个文件的URL 以和我所提供的地址匹配,或者另外

使用任何你能下载的文件的有效URL。

image.png

图13-1 FileGrabber 用户界面

和你可能已经知道的一样,不是所有的FTP 站点都需要一个FTP 用户账户和密码来进行

访问。有些允许匿名访问,它使用“anonymous”作为用户名,使用你的电子邮件地址作为密

码。该FTP 活动也被这样配置,假如你不想提供它们,则用户名默认为anonymous 而密码默

认为someone@example.com。

因为本示例应用程序是一个Windows Forms 应用程序,因此在工作流检索文件的时候我

们不想让应用程序看起来被锁定。毕竟工作流实例在不同的线程上执行,因此我们的用户界

面应能够继续响 应。不过,我们将会禁用某些控制,同时允许其它的一些东西保持活跃状态。

一个状态控制将在文件传输正在发生的期间显示出来,一旦文件下载完成,该状态控制 将会

被隐藏。假如用户在某个文件正在传输时试图退出该应用程序,我们将在取消该工作流实例

并退出应用程序之前对用户的决定进行确定。文件下载期间应用程序 用户界面的情形如图

13-2 所示。

image.png

图13-2 FileGrabber 在下载某个文件时的用户界面

为了让你节约一些时间,该FileGrabber 应用程序已经被写出了。唯一缺少的是一点点

配置工作流并让它启动的代码。但是,工作流将执行的 这个FTP 活动本身并不存在,我们首

先就来创建该FTP 活动。随着本章的进展,我们将会(逐步)向该活动中添加更多的东西,

最后把它放到一个工作流 中,FileGrabber 能执行该工作流去下载某个文件。

创建一个新的FTP工作流活动

1.该FileGrabber 应用程序再次为你提供了两个版本:完整版本和非完整版本。你需要

下载本章源代码,打开FileGrabber 文件夹中的解决方案。

2.FileGrabber 解决方案只包含有一个项目(它是一个Windows Forms 应用程序)。我

们现在将添加第二个项目,我们将用它来创建我们的FTP 活动。为此,向我们的解决方案中

添加一个新项目,项目类型选择类库,项目名称为FtpActivity,然后点击确定。

image.png

3.一旦该新的FtpActivity 项目添加完成后,Visual Studio 会自动地打开它在本项目

中创建好的Class1.cs 文件。首先做一些准备工作,把“Class1.cs”文件的名称重命名为

“FtpGetFileActivity.cs”,同时Visual Studio 也会自动的把类的名称为你从Class1 重

命名为FtpGetFileActivity。

4.确实,我们正创建的是一个WF 活动,但是却没有添加相应的引用,我们不会离题太

远。当我们添加WF 引用的时候,我们也将为我们本章将执行的任务添加其它的引用。因此在

解决方案资源管理器的FtpActivity 项目上点击鼠标右键,然后选择添加引用。当打开“添

加引用”对话框后,从“.NET”选项卡列表中选中下面所有程序集,然后点击确定:

a.System.Drawing

b.System.Windows.Forms

c.System.Workflow.Activities

d.System.Workflow.ComponentModel

e.System.Workflow.Runtime

5.现在我们就可以添加我们需要的名称空间了。添加下面的名称空间:

using System.IO;
using System.Net;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Activities;
using System.Drawing;

6.因为我们正创建的是一个活动,因此我们需要使FtpGetFileActivity 派生自一个恰

当的基类。修改当前的类定义如下:

public sealed class FtpGetFileActivity : System.Workflow.ComponentModel.Activity

备 注:因为我们正创建的是一个基本活动,因此该FTP 活动派生自

System.Workflow.ComponentModel.Activity。但是, 假如你正创建的是一个组合活动的话,

它应当派生自System.Workflow.ComponentModel.CompositeActivity。

7.对于本例子,FtpGetFileActivity 将暴露三个属性:FtpUrl、FtpUser 和FtpPassword。

活动的属性几乎总是依赖属性,因此我们将添加三个依赖属性,我们就从FtpUrl 开始。在

FtpGetFileActivity 类的左大括号中输入下面的代码(此时该类没有包含其它代码):

public static DependencyProperty FtpUrlProperty =
DependencyProperty.Register("FtpUrl", typeof(System.String),
typeof(FtpGetFileActivity));
[Description ("Please provide the full URL for the file to download.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[ValidationOption(ValidationOption.Required)]
[Browsable(true)]
[Category("FTP Parameters")]
public string FtpUrl
{
get
{
return ((string)
(base.GetValue(FtpGetFileActivity.FtpUrlProperty)));
}
set
{
Uri tempUri = null;
if (Uri.TryCreate(value, UriKind.Absolute, out tempUri))
{
if (tempUri.Scheme == Uri.UriSchemeFtp)
{
base.SetValue(FtpGetFileActivity.FtpUrlProperty,
tempUri.AbsoluteUri);
}
}
else
{
// Not a valid FTP URI
throw new ArgumentException("The value assigned to the" +
" FtpUrl property is not a valid FTP URI.");
};
}
}

备注:完整地描述所有的设计器特性,并理解这些特性使FtpGetFileActivity 在工作

流的视图设计器上怎样呈现出来 方面的内容超出了本章的范围。不过,话虽如此,我还是要

简要的描述一下。Description 特性提供了关于指定属性的相关说明,在该属性被选中的时

候 将在Visual Studio 的属性面板中显示出对应的这些相关说明。

DesignerSerializationVisibility 特性指定属性对设计时序列化程序所 具有的可见性。(在

本例中,该属性将由代码生成器生成。)Browsable 特性告知Visual Studio 把所修饰的属

性以编辑框的形式显示出来。Category 特性指明了所修饰的属性将呈现在哪种类别的属性组

中(本例中是自定义类别)。 ValidationOption 特性是WF 所特有的,它告知工作流视图设

计器它所修饰的属性的验证选项。(在本例中,FTP URL 是必须执行验证的。值必须存在并

将对其验证。)稍后当我们添加一个自定义活动验证器的时候我们将会需要这个特性。

http://msdn2.microsoft.com/en-us/library/a19191fh.aspx 为你提供了设计器特性和它们

的使用的一些概述信息以及相关更多信息的链接。

8.接下来为FtpUser 属性添加代码。把下面的代码放到你前一步所插入的FtpUrl 代码

的下面:

public static DependencyProperty FtpUserProperty =
DependencyProperty.Register("FtpUser", typeof(System.String),
typeof(FtpGetFileActivity));
[Description("Please provide the FTP user account name.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[ValidationOption(ValidationOption.Optional)]
[Browsable(true)]
[Category("FTP Parameters")]
public string FtpUser
{
get
{
return ((string)(
base.GetValue(FtpGetFileActivity.FtpUserProperty)));
}
set
{
base.SetValue(FtpGetFileActivity.FtpUserProperty, value);
}
}

9.现在在你刚插入的FtpUser 代码的下面放入最后的一个属性FtpPassword:

FtpPassword 依赖属性

10.正像你可能知道的,一些FTP 服务器允许匿名访问。虽然许多服务器都要求用户注

册,但也有其它的FTP 站点被配置为公共的存取权限。在公共存取权限的情况下,用户名通

常是anonymous, 并且用户的电子邮件地址被作为密码使用。我们将为FtpGetFileActivity

指定一个FTP URL 地址,但用户名和密码从应用程序的角度来看将是可选的。然而,从FTP

的角度来看,我们必须提供一些东西。因此我们现在添加了一些常量字符串,以便 稍后我们

为FTP 进行身份验证时添加代码的时候使用它。因此,在你刚刚添加的FtpPassword 属性的

下面,添加下面这些常量字符串:

private const string AnonymousUser = "anonymous";
private const string AnonymousPassword = "someone@example.com";

11.根据你想让你的自定义活动去做的事情,你通常将重写基类Activity 所暴露的一个

或多个虚拟方法。虽然严格意义上不是必须的,但你通常都可能想至少去对Execute 进行重

写,因为在Execute 中要完成的工作将得以实现。在你插入到FtpGetFileActivity 源文件的

常量字符串的下面,添加这些重写Execute 的代码:

protected override ActivityExecutionStatus Execute(
ActivityExecutionContext executionContext)
{
// Retrieve the file.
GetFile();
// Work complete, so close.
return ActivityExecutionStatus.Closed;
}

12.Execute 调用了GetFile 方法,因此在Execute 的下面添加如下这些代码:

private void GetFile()
{
// Create the Uri. We check the validity again
// even though we checked it in the property
// setter since binding may have taken place.
// Binding shoots the new value directly to the
// dependency property, skipping our local
// getter/setter logic. Note that if the URL
// is very malformed, the Uri constructor will
// throw.
Uri requestUri = new Uri(FtpUrl);
if (requestUri.Scheme != Uri.UriSchemeFtp)
{
// Not a valid FTP URI
throw new ArgumentException("The value assigned to the" +
"FtpUrl property is not a valid FTP URI.");
} // if
string fileName =
Path.GetFileName(requestUri.AbsolutePath);
if (String.IsNullOrEmpty(fileName))
{
// No file to retrieve.
return;
} // if
Stream bitStream = null;
FileStream fileStream = null;
StreamReader reader = null;
try
{
// Open the connection
FtpWebRequest request =
(FtpWebRequest)WebRequest.Create(requestUri);
// Establish the authentication credentials
if (!String.IsNullOrEmpty(FtpUser))
{
request.Credentials =
new NetworkCredential(FtpUser, FtpPassword);
} // if
else
{
request.Credentials =
new NetworkCredential(AnonymousUser,
!String.IsNullOrEmpty(FtpPassword) ?
FtpPassword : AnonymousPassword);
} // else
// Make the request and retrieve response stream
FtpWebResponse response =
(FtpWebResponse)request.GetResponse();
bitStream = response.GetResponseStream();
// Create the local file
fileStream = File.Create(fileName);
// Read the stream, dumping bits into local file
byte[] buffer = new byte[1024];
Int32 bytesRead = 0;
while ((bytesRead = bitStream.Read(buffer, 0, buffer.Length)) > 0)
{
fileStream.Write(buffer, 0, bytesRead);
} // while
} // try
finally
{
// Close the response stream
if (reader != null) reader.Close();
else if (bitStream != null) bitStream.Close();
// Close the file
if (fileStream != null) fileStream.Close();
} // finally
}

备注:不可否认,假如我能找到能完成我所需要任务的现成代码而不是从零开始写的话,

我会每次都这样去做。(事实上,一位大学教授 曾经告诉过我这是软件工程的一个重大原则。)

我重用的大部分代码都来自于Microsoft 的示例。我提到这些是以防你想去创建这个把文件

发送到FTP 服 务器或者甚至可能去删除它们的活动。(对于这些操作的代码Microsoft 的示

例也已经提供了)你可以在

http://msdn.microsoft.com/en-us/library/system.net.ftpwebrequest.aspx 找到该例子。

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Activities;
using System.Drawing;
namespace FtpActivity
{
[Designer(typeof(FtpGetFileActivityDesigner), typeof(IDesigner))]
[ToolboxBitmap(typeof(FtpGetFileActivity), "FtpImage.bmp")]
[ToolboxItem(typeof(FtpGetFileActivityToolboxItem))]
[ActivityValidator(typeof(FtpGetFileActivityValidator))]
public sealed class FtpGetFileActivity : System.Workflow.ComponentModel.Activity
{
public static DependencyProperty FtpUrlProperty = DependencyProperty.Register("FtpUrl", typeof(System
.String), typeof(FtpGetFileActivity));
[Description("Please provide the full URL for the file to download.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[ValidationOption(ValidationOption.Required)]
[Browsable(true)]
[Category("FTP Parameters")]
public string FtpUrl
{
get
{
return ((string)(base.GetValue(FtpGetFileActivity.FtpUrlProperty)));
}
set
{
Uri tempUri = null;
if (Uri.TryCreate(value, UriKind.Absolute, out tempUri))
{
if (tempUri.Scheme == Uri.UriSchemeFtp)
{
base.SetValue(FtpGetFileActivity.FtpUrlProperty, tempUri.AbsoluteUri);
}
}
else
{
// Not a valid FTP URI
throw new ArgumentException("The value assigned to the FtpUrl property is not a valid FTP URI."
);
}
}
}
public static DependencyProperty FtpUserProperty = DependencyProperty.Register("FtpUser", typeof(Syst
em.String), typeof(FtpGetFileActivity));
[Description("Please provide the FTP user account name.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[ValidationOption(ValidationOption.Optional)]
[Browsable(true)]
[Category("FTP Parameters")]
public string FtpUser
{
get
{
return ((string)(base.GetValue(FtpGetFileActivity.FtpUserProperty)));
}
set
{
base.SetValue(FtpGetFileActivity.FtpUserProperty, value);
}
}
public static DependencyProperty FtpPasswordProperty = DependencyProperty.Register("FtpPassword", ty
peof(System.String), typeof(FtpGetFileActivity));
[Description("Please provide the FTP user account password.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[ValidationOption(ValidationOption.Optional)]
[Browsable(true)]
[Category("FTP Parameters")]
public string FtpPassword
{
get
{
return ((string)(base.GetValue(FtpGetFileActivity.FtpPasswordProperty)));
}
set
{
base.SetValue(FtpGetFileActivity.FtpPasswordProperty, value);
}
}
private const string AnonymousUser = "anonymous";
private const string AnonymousPassword = "someone@example.com";
protected override ActivityExecutionStatus Execute(
ActivityExecutionContext executionContext)
{
// Retrieve the file.
GetFile();
// Work complete, so close.
return ActivityExecutionStatus.Closed;
}
private void GetFile()
{
// Create the Uri. We check the validity again
// even though we checked it in the property
// setter since binding may have taken place.
// Binding shoots the new value directly to the
// dependency property, skipping our local
// getter/setter logic. Note that if the URL
// is very malformed, the Uri constructor will
// throw.
Uri requestUri = new Uri(FtpUrl);
if (requestUri.Scheme != Uri.UriSchemeFtp)
{
// Not a valid FTP URI
throw new ArgumentException("The value assigned to the FtpUrl property is not a valid FTP URI.");
} // if
string fileName =
Path.GetFileName(requestUri.AbsolutePath);
if (String.IsNullOrEmpty(fileName))
{
// No file to retrieve.
return;
} // if
Stream bitStream = null;
FileStream fileStream = null;
StreamReader reader = null;
try
{
// Open the connection
FtpWebRequest request =
(FtpWebRequest)WebRequest.Create(requestUri);
// Establish the authentication credentials
if (!String.IsNullOrEmpty(FtpUser))
{
request.Credentials =
new NetworkCredential(FtpUser, FtpPassword);
} // if
else
{
request.Credentials =
new NetworkCredential(AnonymousUser,
!String.IsNullOrEmpty(FtpPassword) ?
FtpPassword : AnonymousPassword);
} // else
// Make the request and retrieve response stream
FtpWebResponse response =
(FtpWebResponse)request.GetResponse();
bitStream = response.GetResponseStream();
// Create the local file
fileStream = File.Create(fileName);
// Read the stream, dumping bits into local file
byte[] buffer = new byte[1024];
Int32 bytesRead = 0;
while ((bytesRead = bitStream.Read(buffer, 0, buffer.Length)) > 0)
{
fileStream.Write(buffer, 0, bytesRead);
} // while
} // try
finally
{
// Close the response stream
if (reader != null) reader.Close();
else if (bitStream != null) bitStream.Close();
// Close the file
if (fileStream != null) fileStream.Close();
} // finally
}
}
}

其中接下来要做的一个更重要的事情是创建一个自定义验证器。尽管你可以使用该FTP

活动了,因为它现在已经存在,但此时它是不完整的引入到工作流视图设计器中的。它所缺

少的是属性验证。我们就来看看怎样添加一个验证器。

创建一个自定义ActivityValidator

现在,我确信你已经看到过小红色圆圈内包含一个感叹号的标记出现在那些在工作流视

图设计器中没有完成相应配置的活动中。

例如,在Code 活动中假如没有为它设置ExecuteCode 属性的话将显示这个指示标记。

原因是什么呢?

答案是活动验证器强迫这样做。验证器检查和它相关联的活动的属性并在需检查的任何

属性缺失和无效的时候就把错误添加进一个错误集合中。当设计器 的状态发生改变(换句话

说,就是在添加了新活动或者属性发生改变的时候)以及工作流被编译的时候会要求验证器

重新对它适用的活动的属性进行判定。

验证器能选择是否对属性的配置不进行验证,它也能把它们标记为警告或者是不可接受

的错误。FTP 活动有三个属性,其中一个很关键(就是 URL)。其它两个可以不管,这将产

生默认(匿名)用户的身份验证。但我们实现我们的验证器时,我们将把缺少URL 的情况(或

者在主工作流活动中缺少对 URL 属性的绑定)标记为一个错误。假如省略了用户名或密码的

话我们将产生警告信息来提示将使用匿名登录。

为FtpGetFileActivity工作流活动创建一个验证器

1.WF 中的活动验证器其实是一个类,因此我们要在FtpActivity 项目中添加一个新类。

类的名称命名为“FtpGetFileActivityValidator.cs”。

2.在源文件中添加下面的名称空间:

using System.Workflow.ComponentModel.Compiler;

3.当创建了FtpGetFileActivityValidator 的新类创建后,它是一个private 类型的类。

而且,WF 活动验证器必须使用ActivityValidator 作为基类。因此在源文件中对该类添加

public 关键字以及一个ActivityValicator 基类来更改类的定义:

public class FtpGetFileActivityValidator : ActivityValidator

4.为了实际去执行验证,你必须重写Validate 方法。这里我们将对属性进行检查,假

如它们没有(设置)的话,你将把一个错误添加到设计器提供的错误集合中。下面是你需要

添加到FtpGetFileActivityValidator 类中去的完整的Validate 重写方法。

public override ValidationErrorCollection
Validate(ValidationManager manager, object obj)
{
FtpGetFileActivity fget = obj as FtpGetFileActivity;
if (null == fget)
throw new InvalidOperationException();
ValidationErrorCollection errors = base.Validate(manager, obj);
if (null != fget.Parent)
{
// Now actually validate the activity
if (String.IsNullOrEmpty(fget.FtpUrl) &&
fget.GetBinding(FtpGetFileActivity.FtpUrlProperty) == null)
{
ValidationError err =
new ValidationError("Note you must specify a URL " +
"(including filename) for the FTP server.",
100, false);
errors.Add(err);
} // if
Uri tempUri = null;
if (Uri.TryCreate(fget.FtpUrl, UriKind.Absolute, out tempUri))
{
if (tempUri.Scheme != Uri.UriSchemeFtp)
{
ValidationError err =
new ValidationError("The FTP URL must be set to an" +
" FTP endpoint.", 101, false);
errors.Add(err);
} // if
} // if
else if (!String.IsNullOrEmpty(fget.FtpUrl))
{
ValidationError err =
new ValidationError("The FTP URL must be a valid FTP URI.",
102, false);
errors.Add(err);
} // else if
if (String.IsNullOrEmpty(fget.FtpUser) &&
fget.GetBinding(FtpGetFileActivity.FtpUserProperty) == null)
{
ValidationError err =
new ValidationError("The 'anonymous' user account will " +
"be used for logging into the FTP server.", 200, true);
errors.Add(err);
} // if
if (String.IsNullOrEmpty(fget.FtpPassword) &&
fget.GetBinding(FtpGetFileActivity.FtpPasswordProperty) == null)
{
ValidationError err =
new ValidationError("The default anonymous password " +
"'someone@example.com' will be used for logging " +
"into the FTP server.", 300, true);
errors.Add(err);
} // if
}
return errors;
}

5.FtpGetFileActivityValidator 类现在就完成了,但我们实际上并没有通知WF 去执

行验证。为此,回到FtpGetFileActivity 类中,在该类定义的前面为该类添加下面的特性标

记:

[ActivityValidator(typeof(FtpGetFileActivityValidator))]

6.生成FtpActivity 项目,修正可能出现的任何错误。

完整的验证器代码如清单13-2 所示。现在,当你拖拽该FtpGetFileActivity 到你的工

作流中去的时候,假如你忘了指定该URL 或者你没有为提供的URL 创建绑定的话,工作流不

能编译。并且,假如你没有提供用户名或密码,或者你甚至没有在Visual Studio 中使用属

性面板对它们进行绑定的话,你将收到警告信息。

清单13-2 FtpGetFileActivityValidator.cs 的完整代码

using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.ComponentModel.Compiler;
namespace FtpActivity
{
public class FtpGetFileActivityValidator : ActivityValidator
{
public override ValidationErrorCollection
Validate(ValidationManager manager, object obj)
{
FtpGetFileActivity fget = obj as FtpGetFileActivity;
if (null == fget)
throw new InvalidOperationException();
ValidationErrorCollection errors = base.Validate(manager, obj);
if (null != fget.Parent)
{
// Now actually validate the activity
if (String.IsNullOrEmpty(fget.FtpUrl) &&
fget.GetBinding(FtpGetFileActivity.FtpUrlProperty) == null)
{
ValidationError err =
new ValidationError("Note you must specify a URL " +
"(including filename) for the FTP server.", 100, false);
errors.Add(err);
} // if
Uri tempUri = null;
if (Uri.TryCreate(fget.FtpUrl, UriKind.Absolute, out tempUri))
{
if (tempUri.Scheme != Uri.UriSchemeFtp)
{
ValidationError err =
new ValidationError("The FTP URL must be set to an FTP endpoint.",
101, false);
errors.Add(err);
} // if
} // if
else if (!String.IsNullOrEmpty(fget.FtpUrl))
{
ValidationError err =
new ValidationError("The FTP URL must be a valid FTP URI.", 102, false);
errors.Add(err);
} // else if
if (String.IsNullOrEmpty(fget.FtpUser) &&
fget.GetBinding(FtpGetFileActivity.FtpUserProperty) == null)
{
ValidationError err =
new ValidationError("The 'anonymous' user account will " +
"be used for logging into the FTP server.", 200, true);
errors.Add(err);
} // if
if (String.IsNullOrEmpty(fget.FtpPassword) &&
fget.GetBinding(FtpGetFileActivity.FtpPasswordProperty)
== null)
{
ValidationError err =
new ValidationError("The default anonymous password " +
"'someone@example.com' will be used for logging " +
"into the FTP server.", 300, true);
errors.Add(err);
} // if
}
return errors;
}
}
}

提供工具箱位图

我们下面将在我们的活动中做的事情是为它提供一个工具箱位图。这不是一个严格意义

上的WF 任务。这种功能被集成到.NET 中,主要用于为Visual Studio 设计器提供支持。它

也并不难做到。

为FtpGetFileActivity工作流活动指定一个工具箱位图

1.下载本章的示例代码,你将找到一个名称为FtpImage 的位图文件。把FtpImage 文件

从Windows Explorer 窗口中拖拽到FtpActivity 项目的树形控制节点下面,这会把该文件复

制并添加到你的项目目录中。

2.然后,你必须把该位图作为资源编译进你的程序集中。在解决方案资源管理器的

FtpActivity 项目中选中FtpImage 文件以激活它的属性。更改“生成操作”属性,把它从“编

译”改为“嵌入的资源”。

3.和验证器一样,只是把一个位图编译进你活动的程序集中是不够的。你也必须通知

Visual Studio 该活动有一个相关的工具箱位图。和先前一样,你使用一个特性来通知Visual

Studio 这件事。把下面的特性添加到FtpGetFileActivity 类的定义中(就像你在前面一节

添加ActivityValidator 一 样):

[ToolboxBitmap(typeof(FtpGetFileActivity), "FtpImage.bmp")]

备注:ToolboxBitmapAttribute 不是WF 所特有的。它可以用到任何的控件。看看

http://msdn2.microsoft.com/en-us/library/4wk1wc0a(VS.80).aspx 可获得更多信息。

4.生成FtpActivity 项目。不应出现任何错误,假如有的话,修正它们。

假如你此刻创建一个顺序工作流并把这个活动拖拽到该工作流中去的话,这个活动会以

相当普通的外观呈现出来。默认呈现出的外观是一个以黑色圆角作为边框并用白色填充的矩

形。想做得更好吗?看看下面怎么做。

修改活动在工作流视图设计器中的外观

工作流视图设计器其实基于通用的Visual Studio 设计器。自.NET 1.0 以来,.NET

Framework 中就有组件帮助你把你的自定义对象集成到通用功能的设计器中。这些组件中的

一个就是Designer 特性,它嵌入代码中被执行,视图设计器使用它去控制如对象的展示和外

观之类的事情。

WF 通过提供一种机制延伸了这一概念,它通过一个theme 来为可视化活动的展现提供

支持。主题(theme)实际上只不过是一个设计器类,它包含许多的属性,你能设置它们以便

去控制你的活动被怎样绘制。你能控制呈现的颜色、边框线的风格及颜色等等。

你也能控制它在视图设计器中的行为。例如,你能把一些东西添加到使用鼠标右键点击

活动时所弹出的快捷菜单中。主题和行为操作两者都要求你去写一个派生于

ActivityDesigner 或CompositeActivityDesigner(针对组合活动)的类。对于我们的例子,

我们将创建一个专门的命名为FtpGetFileActivityDesinger 的设计器类。

添加一个可视化设计器到FtpGetFileActivity工作流活动中

1.这里你将以和前一节同样的方式开始我们的工作:创建一个新类。为此,向

FtpActivity 项目中添加一个名称为FtpGetFileActivityDesigner.cs 的类文件。

2.向该源文件中插入下面的名称空间,把它们放到已存在的名称空间语句的下面:

using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Workflow.Activities;
using System.Workflow.ComponentModel.Design;

3.因为你正创建的设计器类派生自ActivityDesigner,因此你需要修改源文件中的类

定义。使用下面的类定义来替换Visual Studio 为你自动生成的类定义:

public class FtpGetFileActivityDesigner : ActivityDesigner

备注:再重复一次,因为这是一个基本活动,因此你正创建的设计器类派生自

ActivityDesigner。但是,假如这个活动是一个组合活动的话,你应该使用

CompositeActivityDesigner 类来作为基类。

4.ActivityDesigner 提供了几个虚拟的属性和方法,你能重写它们以便把行为外观添

加到视图设计器中。例如Verbs 属性可以让你添加上下文选择菜单。做这些相当地简单,从

行为外观的角度来看FTP 活动不需要特别的支持,但它在调节视觉方面是很不错的。为做这

些,首先在FtpGetFileActivityDesigner 类定义的前面添加下面的特性标记:

[ActivityDesignerThemeAttribute(typeof(FtpGetFileActivityDesignerTheme

))]

5.你刚刚添加的特性指定了一个包含绘制属性任务的设计器主题类,我们现在就来创建

这个类。寻找FtpGetFileActivityDesigner 类的结束(右边)大括号,在该大括号的下面添

加如下这个内部(internal)类:

internal sealed class FtpGetFileActivityDesignerTheme :
ActivityDesignerTheme
{
public FtpGetFileActivityDesignerTheme(WorkflowTheme theme)
: base(theme)
{
this.BorderColor = Color.Black;
this.BorderStyle = DashStyle.Solid;
this.BackColorStart = Color.Silver;
this.BackColorEnd = Color.LightBlue;
this.BackgroundStyle = LinearGradientMode.Horizontal;
}
}

备注:组合活动也有它们自己的设计器主题类:CompositeDesignerTheme。那是因为组

合活动需要去呈现子活动,并且你可能想在视觉外观上进行更严格的控制。

6.在有了验证器和工具箱位图后,你需要为FtpGetFileActivity 类添加一个特性来通

知工作流视图设计器你有为展示你的活动所需的基于ActivityDesigner 的信息。

[Designer(typeof(FtpGetFileActivityDesigner), typeof(IDesigner))]

7.编译该项目,修正任何出现的编译错误。

FtpGetFileActivityDesigner 的完整文件如清单13-3 所示。假如我们需要的话,我们

能在设计器类上做更多的工作,但在这个例子中,该设计器类的存在仅仅只是添加主题。该

活动将在工作流视图设计器中以银色到白蓝色的颜色水平渐变并以实心边框线的风格呈现出

来。

清单13-3 FtpGetFileActivityDesigner.cs 的完整源代码 还剩下一个细节:当它

加载进工具箱后,FtpGetFileActivity 的名称和图标都将会展示出来。

using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Workflow.Activities;
using System.Workflow.ComponentModel.Design;
namespace FtpActivity
{
[ActivityDesignerThemeAttribute(typeof(FtpGetFileActivityDesignerTheme))]
public class FtpGetFileActivityDesigner : ActivityDesigner
{
}
internal sealed class FtpGetFileActivityDesignerTheme : ActivityDesignerTheme
{
public FtpGetFileActivityDesignerTheme(WorkflowTheme theme)
: base(theme)
{
this.BorderColor = Color.Black;
this.BorderStyle = DashStyle.Solid;
this.BackColorStart = Color.Silver;
this.BackColorEnd = Color.LightBlue;
this.BackgroundStyle = LinearGradientMode.Horizontal;
}
}
}

把自定义活动集成到工具箱中

如你所知,当你的活动被装到Visual Studio 工具箱中后,ToolboxBitmapAttribute

会显示一个和你的活动关联的图标。但碰巧的是,你能做比刚刚显示一个位图更多的事。

例如组合活动,通常要为其正常运行创建所必须的子活动。一个极好的例子是IfElse

活动。当你拖拽一个IfElse 活动到你的工作流中的时候,它会自动填充一个左右分支活动。

在这里我不会显示怎样做这些,因为我们正创建的是一个基本活动。但在这节的末尾我将提

供一个获取更多信息的链接以及创建组合活动并预先用子活动来填充它们的示例代码。

因此,如果我们不添加子活动的话,我们还需要完成些什么事才能把我们的活动集成到

工具箱中呢?对这件事来说,如没有其它的指示,Visual Studio 将把你的活动加载进工具

箱中并使用类的名字作为它显示的名字。因为有其它的WF 活动没有使用它们的类名来作为显

示名,因此我们将对默认的行为 进行重写并提供一个更像是真正的标准WF 元素的显示名(不

使用类名来作为显示名)。尽管我们所有要调整的事情就是这些,但你也能修改像包含一个

描述信息、 你的公司名称以及一个版本号在内的其它事情。

你也能提供过滤,以便让你的活动只在基于工作流中使用时呈现,但你很快将使用的

ActivityToolboxItem 基类为你提供了这种行为。

为FtpGetFileActivity工作流活动添加工具箱集成

1.和前面两节一样,你要创建一个新类,在FtpActivity 项目中添加一个名称为

FtpGetFileActivityToolboxItem.cs 的类文件。

2.添加下面的名称空间,把它们放到现存的名称空间的下面:

using System.Workflow.ComponentModel.Design;
using System.Runtime.Serialization;

3.你正创建的类必须从ActivityToolboxItem 派生。因此,你需要修改Visual Studio

为你创建的默认的类定义。用下面的内容替换该类的类定义。

class FtpGetFileActivityToolboxItem : ActivityToolboxItem

4.FtpGetFileActivityToolboxItem 类必须被标记为可序列化的,因此在刚才类定义的

前面添加Serializable 特性。

[Serializable]

5.现在添加该类的主要部分。你需要三个构造器:一个默认的构造器,一个带参数的构

造器和一个序列化构造器。每一个构造器都将调用InitializeComponent 来指定它的显示名

称。

public FtpGetFileActivityToolboxItem()
{
// Initialize
InitializeComponent();
}
public FtpGetFileActivityToolboxItem(Type type)
: base(type)
{
// Initialize
InitializeComponent();
}
private FtpGetFileActivityToolboxItem(SerializationInfo info, StreamingContext context)
{
// Call base method to deserialize.
Deserialize(info, context);
// Initialize
InitializeComponent();
}
protected void InitializeComponent()
{
// Assign the display name
this.DisplayName = "FTP File Get";
}

备注:有一个虚拟方法Initialize,你可以重写它去指定要显示的名称。但是这个方

法并不总会被调用。因此提供我们自己的InitializeComponent 方法是确保指定的显示名称

在所有情况下都有效的最好方式。

6.为确保你刚刚创建的ToolboxItem 被FTP 活动使用,需要把下面的特性添加到你已经

为FtpGetFileActivity 添加的一组特性的下面。

[ToolboxItem(typeof(FtpGetFileActivityToolboxItem))]

7.编译FtpActivity 项目,修正任何可能出现的编译错误。

随着这最后的一个步骤,你的自定义活动就完成了。但是,FileGrabber 应用程序是不

完整的。你需要添加一个使用FtpGetFileActivity 的工作流,并为FileGrabber 应用程序添

加必须的代码以便调用该工作流。我们首先创建该工作流。

添加一个工作流并使用FtpGetFileActivity工作流活动

1.右键点击FileGrabber 解决方案,然后选择“添加”。从子菜单中选中“新建项目”。

2.新建的项目类型选择“顺序工作流库”,名称命名为“GrabberFlow”。

3.Visual Studio 添加了一个新的顺序工作流库后会打开工作流视图设计器,让你可直

接开始编辑你的工作流。打开Visual Studio 工具箱后,你应该在那里能找到

FtpGetFileActivity。

image.png

备 注:你或许会惊讶,漂亮的小FTP 位图到哪里去了(取而代之的是一个蓝色齿轮图

标),以及你在 FtpGetFileActivityToolboxItem 类中添加的显示文本为什么没有在工具箱

中显示出来。这是因为 FtpGetFileActivity 由当前解决方案中的一个程序集支持。我将在

你完成了该工作流后来描述解决这些问题的办法。

4.拖拽一个FtpGetFileActivity 到你的设计器界面上。

image.png

5.带感叹号标记的红点指明了当前存在一些验证错误。并且事实上,假如你把鼠标放到

向下的箭头上并单击的话,你将看到详细的验证失败信息。它看起来眼熟吗?哦...它是你在

前面章节创建活动验证类时所插入的相关验证错误信息。

image.png

6.FileGrabber 主 应用程序要能够把用户名、密码和文件的URL 传进你的工作流中。

因此你需要在你的工作流中为所有这些值都提供一个属性。这里有一个很棒的方式来完成这

些任 务:让Visual Studio 为你添加它们。假如你选中FTP 活动然后在属性面板中看看它的

属性的话,你将看到你为活动添加好的三个属性:FtpUrl、FtpUser 和FtpPassword。为了让

你首先清除错误的条件,你需要选中FtpUrl 属性以激活浏览(...)按钮。点击该浏览按钮。

image.png

7.这将激活“将‘FtpUrl’绑定到活动的属性”对话框。点击“绑定到新成员”选项

卡,在“新成员名称”中输入“FtpUrl”,确保选中的是“创建属性”。最后点击“确定”。

(注意现在带感叹号标记的红点消失了。)

image.png

8.按照相同的步骤(步骤6 和步骤7)添加一个新的FtpUser 属性和一个新的

FtpPassword 属性。当你完成这些后,属性面板显示的为所有这三个属性的绑定效果如下图

所示:

image.png

9.编译该工作流项目,假如存在错误的话,请纠正这些错误。

我在第三步中提到过,我会描述怎样把FtpGetFileActivity 加载进工具箱中去显示出

你在前面章节中添加的实际已存在的元数据。下面就是你要做的。

把FtpGetFileActivity工作流活动加载进工具箱中

1.对这一工作来说,你必须在工作流视图设计器中有一个工作流。(工具箱会过滤掉不

适合的组件。)GrabberFlow 工作流应该被加载进工作流视图设计器中了,如没有的话,重

新加载它并打开工具箱。在工具箱内容体(不是标题)上点击鼠标右键,这将弹出上下文菜

单,然后选择“选择项”。

image.png

2.这将打开“选择工具箱项”对话框。单击“活动”选项卡然后点击“浏览”按钮。

image.png

3.点击“浏览”将打开一个常见的文件对话框。使用导航工具,浏览并定位到你本地文

件系统中已编译的FtpActivity 项目对应的目录,通常为

“\Chapter13\FileGrabber\FtpActivity\bin\Debug\”(或者为“Release\”,这取决于你

所选择的生成模式)。选中FtpActivity.dll 文件,点击“打开”。然后点击“确定”关闭

“选择工具箱项”对话框。

image.png

4.这就把FtpGetFileActivity 加载进工具箱中了,并且你应该会看到你在前面所添加

的自定义图标和显示文本。

image.png

我们最后的任务是去添加在主应用程序中开始该工作流所需的代码。

执行FtpGetFileActivity工作流

1.在代码视图模式下打开Form1.cs 文件。

2.除了真正启动工作流实例所需的代码外,你需要的所有代码我都已经为你添加好了。

该代码将放进Get_Click 事件处理程序中。因此找到Get_Click 事件处理程序,在末尾添加

如下代码:

// Process the request, starting by creating the parameters
Dictionary<string, object> parms = new Dictionary<string, object>();
parms.Add("FtpUrl", tbFtpUrl.Text);
parms.Add("FtpUser", tbUsername.Text);
parms.Add("FtpPassword", tbPassword.Text);
// Create instance.
_workflowInstance = _workflowRuntime.CreateWorkflow(typeof(GrabberFlow.Workflow1), parms);
// Start instance.
_workflowInstance.Start();

3.因为你正使用的工作流来自于GrabberFlow 名称空间,因此你需要添加对该工作流程

序集的项目级引用。

4.按下F5(或者Ctrl+F5)执行FileGrabber。假如你提供了一个有效的FTP URL 文件

地址,该文件会被下载吗?(注意该文件将被下载并放到和你的应用程序的可执行文件相同

目录的位置中。)

在这一章,我们创建了一个基本活动。这些必需的步骤对于创建一个组合活动来说也是

相似的,只是稍微多了一些调用。(例如,你创建的ToolBoxItem 就多些额外的代码去方便

对所容纳的活动进行添加。)假如你想阅读关于组合活动创建的更多资料的话,你可以在

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnlong/html/par

allelif.asp 中找到。

源码下载 http://files.cnblogs.com/gyche/WF%20Step%20by%20Step/Chapter13.rar


--------------------- 

作者:hackpig

来源:www.skcircle.com

版权声明:本文为博主原创文章,转载请附上博文链接!



本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2024年4月    »
1234567
891011121314
15161718192021
22232425262728
2930
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864