首页 > 编程开发 > Objective-C编程 >
-
WF从入门到精通之打造自定义活动一
学习完本章,你将掌握:
1.了解对于创建一个功能齐全的自定义工作流活动来说哪些组件是必须的
2.创建基本的自定义工作流活动
3.在基本的自定义工作流活动中应用验证规则
4.把基本的自定义工作流活动集成到Microsoft Visual Studio的工作流视图设计器和工具箱中
WF并不可能涵盖到你可能在你的工作流中想要实现的各个方方面面。即使WF对于开发社区来说仍是非常新的技术,但目前已经可以获得许多免费发布的自定义活动,可以肯定商业级的活动最终也会跟进。
在这章中,你将通过创建一个新的工作流活动来了解WF的个中奥妙,这个活动从远程FTP服务器中检索文件。你将看到在创建你自己的活动时哪些东西是必需的,以及其中哪些部分挺不错。你也将更深入地了解活动是怎样和工作流运行时交互的。
备注:只在一章中对自定义活动开发的每一个细节进行探讨是不可能,这儿简化了太多的细节。不过好消息是,对于得到一个完整功能的活动来说是容易的,这不用知道每一个细节。
关于活动的更多知识
在第四章(活动及工作流类型介绍)中,我们初步了解了一下活动并讨论了像ActivityExecutionContext之类一些话题,ActivityExecutionContext用来容纳一些和正执行的活动相关的一些信息,工作流运行时需要不时对这些信息进行访问。我们这里将对WF活动进行更深入一些的了解。
活动的虚拟方法
在创建自定义活动时首先需要了解的是基类为你提供了哪些虚拟的方法和属性。表13-1显示了活动中被普遍使用的可重写的一些方法。(这里没有虚拟属性。)
表13-1Activity中被普遍使用的可重写的虚拟方法
方法 | 功能 |
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。
图13-1FileGrabber用户界面
和你可能已经知道的一样,不是所有的FTP站点都需要一个FTP用户账户和密码来进行访问。有些允许匿名访问,它使用“anonymous”作为用户名,使用你的电子邮件地址作为密码。该FTP活动也被这样配置,假如你不想提供它们,则用户名默认为anonymous而密码默认为someone@example.com。
因为本示例应用程序是一个Windows Forms应用程序,因此在工作流检索文件的时候我们不想让应用程序看起来被锁定。毕竟工作流实例在不同的线程上执行,因此我们的用户界面应能够继续响应。不过,我们将会禁用某些控制,同时允许其它的一些东西保持活跃状态。一个状态控制将在文件传输正在发生的期间显示出来,一旦文件下载完成,该状态控制将会被隐藏。假如用户在某个文件正在传输时试图退出该应用程序,我们将在取消该工作流实例并退出应用程序之前对用户的决定进行确定。文件下载期间应用程序用户界面的情形如图13-2所示。
图13-2FileGrabber在下载某个文件时的用户界面
为了让你节约一些时间,该FileGrabber应用程序已经被写出了。唯一缺少的是一点点配置工作流并让它启动的代码。但是,工作流将执行的这个FTP活动本身并不存在,我们首先就来创建该FTP活动。随着本章的进展,我们将会(逐步)向该活动中添加更多的东西,最后把它放到一个工作流中,FileGrabber能执行该工作流去下载某个文件。
创建一个新的FTP工作流活动
1.该FileGrabber应用程序再次为你提供了两个版本:完整版本和非完整版本。你需要下载本章源代码,打开FileGrabber文件夹中的解决方案。
2.FileGrabber解决方案只包含有一个项目(它是一个Windows Forms应用程序)。我们现在将添加第二个项目,我们将用它来创建我们的FTP活动。为此,向我们的解决方案中添加一个新项目,项目类型选择类库,项目名称为FtpActivity,然后点击确定。
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类的左大括号中输入下面的代码(此时该类没有包含其它代码):
FtpUrl依赖属性
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代码的下面:
FtpUser依赖属性
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依赖属性
public static DependencyProperty FtpPasswordProperty =
DependencyProperty.Register("FtpPassword", typeof(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);
}
}
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的代码:
Execute方法
protected override ActivityExecutionStatus Execute(
ActivityExecutionContext executionContext)
{
// Retrieve the file.
GetFile();
// Work complete, so close.
return ActivityExecutionStatus.Closed;
}
12.Execute调用了GetFile方法,因此在Execute的下面添加如下这些代码:
GetFile方法
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找到该例子。