首页 > 编程开发 > Objective-C编程 >
-
WF从入门到精通之打造自定义活动二
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.",
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的完整代码 提供工具箱位图
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.", 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)类:
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;
3.你正创建的类必须从ActivityToolboxItem派生。因此,你需要修改Visual Studio为你创建的默认的类定义。用下面的内容替换该类的类定义。
using System.Runtime.Serialization;
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添加的一组特性的下面。
其中接下来要做的一个更重要的事情是创建一个自定义验证器。尽管你可以使用该FTP活动了,因为它现在已经存在,但此时它是不完整的引入到工作流视图设计器中的。它所缺少的是属性验证。我们就来看看怎样添加一个验证器。
创建一个自定义ActivityValidator
现在,我确信你已经看到过小红色圆圈内包含一个感叹号的标记出现在那些在工作流视图设计器中没有完成相应配置的活动中。
例如,在Code活动中假如没有为它设置ExecuteCode属性的话将显示这个指示标记。原因是什么呢?
答案是活动验证器强迫这样做。验证器检查和它相关联的活动的属性并在需检查的任何属性缺失和无效的时候就把错误添加进一个错误集合中。当设计器的状态发生改变(换句话说,就是在添加了新活动或者属性发生改变的时候)以及工作流被编译的时候会要求验证器重新对它适用的活动的属性进行判定。
验证器能选择是否对属性的配置不进行验证,它也能把它们标记为警告或者是不可接受的错误。FTP活动有三个属性,其中一个很关键(就是URL)。其它两个可以不管,这将产生默认(匿名)用户的身份验证。但我们实现我们的验证器时,我们将把缺少URL的情况(或者在主工作流活动中缺少对URL属性的绑定)标记为一个错误。假如省略了用户名或密码的话我们将产生警告信息来提示将使用匿名登录。
为FtpGetFileActivity工作流活动创建一个验证器
1.WF中的活动验证器其实是一个类,因此我们要在FtpActivity项目中添加一个新类。类的名称命名为“FtpGetFileActivityValidator.cs”。
2.在源文件中添加下面的名称空间:
using System.Workflow.ComponentModel.Compiler;