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

上一篇:【翻译】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方法中执行一些相似的处理工作之外的事情。
      OnActivityExecutionContextLoadOnActivityExecutionContextUnload方法分别表示活动正加载到工作流运行时中和活动正从工作流运行时中移走。在 OnActivityExecutionContextLoad被调用之前以及 OnActivityExecutionContextUnload被调用之后,从WF的角度来看,该活动是处于卸载状态中。它或许是被序列化到一个队列中、保存进一个数据库中或者甚至是在磁盘上等待被加载。但在这些方法( OnActivityExecutionContextLoadOnActivityExecutionContextUnload方法)被调用之前或之后它并不存在于工作流运行时之中。
      CancelHandleFaultCompensate都在显而易见的条件(指取消、失败和补偿条件)激发的时候被调用。尽管 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 有效的执行状态

可重写的方法有效的返回执行状态
CancelActivityExecutionStatus.Canceling和ActivityExecutionStatus.Closed
CompensateActivityExecutionStatus.Compensating和ActivityExecutionStatus.Closed
ExecuteActivityExecutionStatus.Executing和ActivityExecutionStatus.Closed
HandleFaultActivityExecutionStatus.Faulting和ActivityExecutionStatus.Closed
InitializeActivityExecutionStatus.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
     从工作流运行时的角度来看,OnActivityExecutionContextLoadOnActivityExecutionContextUnload界定了活动的生命周期。OnActivityExecutionContextLoad在一个活动刚刚被加载到运行时内存中的时候被调用,而OnActivityExecutionContextUnload在一个活动从运行时中删除的前一刻被调用。
     备注:活动通常从反序列化过程创建而不是由工作流运行时直接调用构造器创建。因此,假如你需要在创建活动的时候为其分配资源的话,OnActivityContextLoad是做这件事情的最好位置,而不是在构造器中。
     尽管从内存的角度来说OnActivityExecutionContextLoadOnActivityExecutionContextUnload指示了活动的创建,但是InitializeUninitialize则表示活动在工作流运行时中执行的生命周期。当工作流运行时调用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-1 FileGrabber用户界面
     和你可能已经知道的一样,不是所有的FTP站点都需要一个FTP用户账户和密码来进行访问。有些允许匿名访问,它使用“anonymous”作为用户名,使用你的电子邮件地址作为密码。该FTP活动也被这样配置,假如你不想提供它们,则用户名默认为anonymous而密码默认为someone@example.com
     因为本示例应用程序是一个Windows Forms应用程序,因此在工作流检索文件的时候我们不想让应用程序看起来被锁定。毕竟工作流实例在不同的线程上执行,因此我们的用户界面应能够继续响应。不过,我们将会禁用某些控制,同时允许其它的一些东西保持活跃状态。一个状态控制将在文件传输正在发生的期间显示出来,一旦文件下载完成,该状态控制将会被隐藏。假如用户在某个文件正在传输时试图退出该应用程序,我们将在取消该工作流实例并退出应用程序之前对用户的决定进行确定。文件下载期间应用程序用户界面的情形如图13-2所示。

     图13-2 FileGrabber在下载某个文件时的用户界面
     为了让你节约一些时间,该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将暴露三个属性:FtpUrlFtpUserFtpPassword。活动的属性几乎总是依赖属性,因此我们将添加三个依赖属性,我们就从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找到该例子

FtpGetFileActivity类
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(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);
            }

        }


        
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);
            }

        }


        
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重写方法。

重写的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.",
                    
100false);
            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."101false);
                errors.Add(err);
            }
 // if
        }
 // if
        else if (!String.IsNullOrEmpty(fget.FtpUrl))
        
{
            ValidationError err 
=
               
new ValidationError("The FTP URL must be a valid FTP URI.",
                      
102false);
            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."200true);
            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."300true);
            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的完整代码     

FtpGetFileActivityValidator类
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."100false);
                    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.",
                                
101false);
                        errors.Add(err);
                    }
 // if
                }
 // if
                else if (!String.IsNullOrEmpty(fget.FtpUrl))
                
{
                    ValidationError err 
=
                        
new ValidationError("The FTP URL must be a valid FTP URI."102false);
                    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."200true);
                    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."300true);
                    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)实际上只不过是一个设计器类,它包含许多的属性,你能设置它们以便去控制你的活动被怎样绘制。你能控制呈现的颜色、边框线的风格及颜色等等。
     你也能控制它在视图设计器中的行为。例如,你能把一些东西添加到使用鼠标右键点击活动时所弹出的快捷菜单中。主题和行为操作两者都要求你去写一个派生于ActivityDesignerCompositeActivityDesigner(针对组合活动)的类。对于我们的例子,我们将创建一个专门的命名为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)类:

FtpGetFileActivityDesignerTheme类
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的名称和图标都将会展示出来。

FtpGetFileActivityDesigner.cs的完整源代码
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来指定它的显示名称。

FtpGetFileActivityToolboxItem类的构造器
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

     备注:你或许会惊讶,漂亮的小FTP位图到哪里去了(取而代之的是一个蓝色齿轮图标),以及你在FtpGetFileActivityToolboxItem类中添加的显示文本为什么没有在工具箱中显示出来。这是因为FtpGetFileActivity由当前解决方案中的一个程序集支持。我将在你完成了该工作流后来描述解决这些问题的办法。
     4.拖拽一个FtpGetFileActivity到你的设计器界面上。
 
     5.带感叹号标记的红点指明了当前存在一些验证错误。并且事实上,假如你把鼠标放到向下的箭头上并单击的话,你将看到详细的验证失败信息。它看起来眼熟吗?哦...它是你在前面章节创建活动验证类时所插入的相关验证错误信息。
 
     6.FileGrabber主应用程序要能够把用户名、密码和文件的URL传进你的工作流中。因此你需要在你的工作流中为所有这些值都提供一个属性。这里有一个很棒的方式来完成这些任务:让Visual Studio为你添加它们。假如你选中FTP活动然后在属性面板中看看它的属性的话,你将看到你为活动添加好的三个属性:FtpUrlFtpUserFtpPassword。为了让你首先清除错误的条件,你需要选中FtpUrl属性以激活浏览(...)按钮。点击该浏览按钮。

     7.这将激活“将‘FtpUrl’绑定到活动的属性”对话框。点击“绑定到新成员”选项卡,在“新成员名称”中输入“FtpUrl”,确保选中的是“创建属性”。最后点击“确定”。(注意现在带感叹号标记的红点消失了。)

     8.按照相同的步骤(步骤6和步骤7)添加一个新的FtpUser属性和一个新的FtpPassword属性。当你完成这些后,属性面板显示的为所有这三个属性的绑定效果如下图所示:

     9.编译该工作流项目,假如存在错误的话,请纠正这些错误。
     我在第三步中提到过,我会描述怎样把FtpGetFileActivity加载进工具箱中去显示出你在前面章节中添加的实际已存在的元数据。下面就是你要做的。

     把FtpGetFileActivity工作流活动加载进工具箱中
     1.对这一工作来说,你必须在工作流视图设计器中有一个工作流。(工具箱会过滤掉不适合的组件。)GrabberFlow工作流应该被加载进工作流视图设计器中了,如没有的话,重新加载它并打开工具箱。在工具箱内容体(不是标题)上点击鼠标右键,这将弹出上下文菜单,然后选择“选择项”。

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

     3.点击“浏览”将打开一个常见的文件对话框。使用导航工具,浏览并定位到你本地文件系统中已编译的FtpActivity项目对应的目录,通常为“\Chapter13\FileGrabber\FtpActivity\bin\Debug\”(或者为“Release\”,这取决于你所选择的生成模式)。选中FtpActivity.dll文件,点击“打开”。然后点击“确定”关闭“选择工具箱项”对话框。

     4.这就把FtpGetFileActivity加载进工具箱中了,并且你应该会看到你在前面所添加的自定义图标和显示文本。

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

     执行FtpGetFileActivity工作流
     1.在代码视图模式下打开Form1.cs文件。
     2.除了真正启动工作流实例所需的代码外,你需要的所有代码我都已经为你添加好了。该代码将放进Get_Click事件处理程序中。因此找到Get_Click事件处理程序,在末尾添加如下代码:

Get_Click事件处理程序
// Process the request, starting by creating the parameters
Dictionary<stringobject> parms = new Dictionary<stringobject>();
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/parallelif.asp中找到。

本章源代码:里面包含本章的练习项目和完整代码

下一篇:【翻译】下一篇:WF从入门到精通(第十四章):基于状态的工作流

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值