2007年3月30日星期五

Upgrade to support multi validator attribute

简介

昨 天我在“validate a control with only one line code”中介绍了如何利用属性来实现类似asp.net中的validator的功能,今天花一些时间将这一功能进行升级,使得它可以支持对一个控件使 用多种检查,类似在asp.net中可以同时给一个控件使用多个validator。

        [RegularValidator("^[0-9]$")] //attribute的尾巴可以省略
[RequiredField]
private System.Windows.Forms.TextBox textBox2;

这样检验一个textbox的值不可为空并且满足正则表达式的约束,是不是很简单也很清楚呢?


详情见我在Spaces.Live上的文章: “Upgrade to support multi validator attribute” on Spaces


blog spot好了才两天又挂掉了:(

2007年3月29日星期四

validate a control in winform

在asp.net中如果要检查页面上的一个输入框是否为空,即需要用户输入一些东西,我们可以使用RequiredFieldValidator,做一些简单的设定就可以保证我们在后台读取这个输入框的时候它的值不会为空,但是不明白为什么winform里没有这样东西。winform中虽然提供一个errorprovider但是仍然需要我们自己编写validating的代码,反反复复的写了很多检验代码,既烦琐又不容易维护。借助Attribute的特性,其实我们可以将类似的检验做的更加简单,比如:
[RequiredFieldAttribute]
public System.Windows.Forms.TextBox textBox1;
只需要在控件上加上[RequiredFieldAttribute],我们就可以让检验变得自动化。

首先我们需要一个验证用的属性Attribute,关于该属性的介绍可以参看MSDN。
public class RequiredFieldAttribute:Attribute,IDisposable
{
public RequiredFieldAttribute()
{
Validating_EventHandler = new CancelEventHandler(Validate);
}

private void Validate(object sender, CancelEventArgs e)
{
Control ctr = sender as Control;
e.Cancel = (ctr.Text == "");
}

public CancelEventHandler ValidatingDelegate
{
set{
Validating_EventHandler = value;
}
get
{
return Validating_EventHandler;
}
}
CancelEventHandler Validating_EventHandler;

#region IDisposable 成员

public void Dispose()
{
Validating_EventHandler = null;
}

#endregion
}
在定义好属性之后,就可以像前面说的那样将属性附加到我们要验证的控件上去:
[RequiredFieldAttribute]
public System.Windows.Forms.TextBox textBox1;
不过这里要求该控件的定义是“public”的,后面会讲到为什么。附加的属性不会自己起作用,除非是.net framework中默认那些,对于我们自己定义的属性CLR并不了解,所以要使自己定义的属性起作用,还需要自己做一些工作。下面我们定义一个ValidatorEngine的类:
public class ValidatorEngine
{
public ValidatorEngine(ContainerControl container)
{
this.container = (ContainerControl)container;
this.typeOfContainer = container.GetType();
}

public void Active()
{
Active(this.container);
}
private void Active(ContainerControl container)
{
//get all public fields
FieldInfo[] fields = typeOfContainer.GetFields();//1
foreach (FieldInfo fi in fields)
{
//get attributes assigned to the field
object[] attributes = fi.GetCustomAttributes(false);//2
foreach (Attribute attr in attributes)
{
RequiredFieldAttribute validator = attr as RequiredFieldAttribute;//3
if (validator != null)
{
Control[] childControls = container.Controls.Find(fi.Name, false);//4
//the number of child controls should always be "1"
if(childControls.Length>0)
//register validator defined in RequiredFieldAttribute for our controls
childControls[0].Validating += validator.ValidatingDelegate;//5
}
}
}
}
Type typeOfContainer;
ContainerControl container;
}
engine做了些什么工作?
  1. 取得容器控件(比如我们的Form)的所有公开(public)的字段。设为public的可以取到,设计器默认的private好像取不到,试验了加上BindingFlags的枚举好像也不行,如果有知道的请留言告知:)
  2. 对于一个字段,读取附加到它上面的自定义属性。参数是表示是否要搜说该字段所继承的属性,这里不需要所以设为false。
  3. 判断找到的属性是不是我们所设定的,即RequiredFieldAttribute,如果是那说明这个字段表示的控件是不许为空的。
  4. 如果控件前面有我们附加的属性,那么通过字段名找到这个控件。本例子中的控件就在容器控件的第一级子控件集合中,所以Find的第2个参数为false,不在更深的层次中搜索。
  5. 给控件注册Validating事件。例子中使用了我们在RequiredFieldAttribute类中设定的处理方法,这样可以让各种类似的控件使用相同的验证代码。如果要自定验证方法,可以通过:
    public CancelEventHandler ValidatingDelegate 属性传入,但是这样我们附加属性的代码就与前面的略微有一些不同了,需要使用到属性的命名参数,请参考MSDN:
    [RequiredFieldAttribute(ValidatingDelegate =yourValidatingHandler)]
    public System.Windows.Forms.TextBox textBox1;
最后来看一下如何使用Engine。Engine不会自己执行自己,而且他需要一个容器控件作为参数传入,于是我们可以考虑在任何的容器控件初始化之后来启动Engine,这里就以Form为例:
private void FormBase_Load(object sender, EventArgs e)
{
//give engine the reference of a ContainerControl
engine = new ValidatorEngine(this);
//start then engine ,it will process our custom attributes
engine.Active();
}
//define a engine
ValidatorEngine engine;

这个例子很简单,只有一个自定义属性,但是它已经可以让我们看到Attribute的威力了。

2007年3月28日星期三

ReadOnlyCollection :threadsafe and readonly

为了让对象是只读的可以将其标示为ReadOnly的,但是如果这样做的话我们的对象就只能在构造函数中初始化,在其他的地方没办法操作了,这往往都不是我们所希望的。有一种做法是我们让对象是可以修改的,但是当没有权限操作(或说不应该修改)它的对象获得它的引用(一定是引用,值传递没这个问题)的时候我们就返回一个对象那个的Copy,比如

public abstract class OperationBase : IOperation
{
public OperationBase(AppPart appPart)
{
this.appPart = appPart;
}

……

public AppPart AppPart
{
get
{
//protect this appPart from being modified
return (AppPart)appPart.Clone();
}
//set
//{
// appPart = value;
//}
}

……

protected AppPart appPart;

……

}

这样其实不是真正的只读(ReadOnly),只不过对“Clone对象”的修改都不会影响到“源对象”而已。对于简单的对象这样做没有问题,但是如果“大对象”比如集合,要Clone的代价就比较大了,所以我们不简单的返回Clone。我们可以自己设计一个集合让它实现IEnumerable接口,但是内部存储使用一个集合,但是这样又带来一个问题,很多集合的操作我们无法使用了,因为IEnumarable接口提供的方法太少。其实.net2.0提供了一个解决方法——“ReadOnlyCollection”,

An instance of the ReadOnlyCollection generic class is always read-only. A collection that is read-only is simply a collection with a wrapper that prevents modifying the collection; therefore, if changes are made to the underlying collection, the read-only collection reflects those changes.

Public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

A ReadOnlyCollection can support multiple readers concurrently, as long as the collection is not modified.
上面是MSDN的介绍,我们可以看到它不仅是只读的,而且静态实例对于“读操作还是线程安全”,前提是读的同时没有线程去修改该集合内部的集合(正真存储了数据的集合)。ReadOnlyCollection其实只是对普通结合的一个包装,将一些修改的操作屏蔽掉了。
public class ReadOnlyCollection : IList, ICollection, IEnumerable, IList, ICollection, IEnumerable
ReadOnlyCollection的定义说明它实现了像Ilsit,ICollection这样的接口,但是他们是有修改操作的比如Add,Insert,Remove,如果我们对ReadOnlyCollection的实例做强制类型转换在操作会有什么结果:
ReadOnlyCollection readonlyColl = new ReadOnlyCollection(userList);
((IList)readonlyColl).Add(new User());
如果运行这段代码会报错“Not supported exception”,这说明该集合的实现与一般的集合有所不同,CLR的实现者对这个做了特殊的处理。

最后说一个问题,如何对获得只读集合进行排序?我们无法排序,因为无法操作集合,如果要排序的话只能将集合的元素复制到自己的新集合中,在排序:
readonlyColl.CopyTo(new User[readonlyColl.Count],0);

2007年3月27日星期二

smart class or dumb data class

谈 到business object不得不考虑的一个问题就是对象的行为应该放在哪里的问题,也就是说我们的BO是应该只包含对象,还是也应该包含一些操作。Smart Class说的是第二种情况,这样的业务对象不仅包含数据同时也包含了一些方法逻辑,它们了解一些对自身的操作,比如读取数据,修改保存、判断合法性之 类。第一种情况中业务对像仅仅只包含数据,它们被称之为Dumb Data Object,再这种情况下我们通常都要另外定义一个对象来管理执行于这些数据对象之上的操作,称之为Data Object Manager。
Smart Class于我们通常使用的类一样,有数据也有操作,Rockford Lhotka的《C# 2005 Business Objects》 里面有详细的介绍,他比较推崇这种对象,并且为它们定义了非常丰富的操作(如果没有请留言我可以email给你电子版,大小为9m,所以最好有支持大附件的邮箱)。而Dumb Data Object的最大的好处是程序中的其它的逻辑层可以很好地与该类型的业务对象一起工作,比如Business layer和Data Access Layer可以引用并复用它们,这样我们定义的业务对象可以很容易地在系统的各部分传递,而不会出现任何问题。
Data Object Manager主要完成以下几个数据操作的功能:
  • GetItem
  • GetList
  • Insert
  • Update
  • Delete
这也是对应了基本的数据库操作,而业务对象的操作一般来说还包含一些业务处理方法,比如:
  • Search
  • Deactivate
  • Clone
  • Filter
其实现在有了像NHibernate这样的东西,业务对象(或manager)基本上已经不包含什么数据库的操作了,而是主要包含列表二中那些操作。如果是配合使用代码生成工具的人肯能更愿意使用Dumb Data Object。

2007年3月24日星期六

AutoUpdate:overview


AutoUpdate是一个基于webservice和ftp的更新工具,其中webservice用来发现和更新该工具中的更新引擎,而ftp用来传输更新文件。
左边的图显示了自动更新工具AutoUpdate的几个主要部件的协作关系。





  1. client启动,首先检查本地更新引擎的版本,如果引擎不存在则简单地返回“0.0.0.0”,也就是最低版本;如果引擎存在,则读取它的AssemblyVersion。
  2. client调用服务器端的updateservice,并将本地引擎版本传递给服务器
  3. server检查发布的最新引擎版本,如果新引擎版本高于client的版本,则返回需要更新引擎的消息给client
  4. client调用服务器的updateservice获得最新的引擎。引擎被序列化为二进制的数据,到本地由client反序列化并保存在客户机上。接着client启动更新引擎。
  5. 更新引擎调用服务器的web service获得更新描述文件updatemanifest.xml,然后解析该xml文件生成操作(opreation)序列。操作序列中的每个操作都包含了操作的文件对象和操作类型,比如是增加还是删除之类。
  6. 操作序列中的具体操作被顺序执行。如果有新增(覆盖也被归为新增)操作会先从服务器上下载文件。
我总结了一下,自动更新应该具有以下几个特点:
  • 更新设置应该在服务端设定,应为我们不可能去客户端更改设置
  • 更新的启动程序应该尽可能简单,最好只有一个功能:检查现有的更新引擎是否需要更新,如果需要则先下载该引擎
  • 真正的更新由更新引擎去执行,这样可以自由变更更新引擎
  • 更新内容有配置文件设定,这里下载xml文件来描述,具有通用性
  • 更新引擎一般比较小,可以通过webservice的方式直接传递到客户机
  • 更新文件通过ftp方式下载,可以保证快速更新
  • 支持从不同的低版本升级到最新版本。在服务端定义一个版本升级策略的映射文件,根据客户端传递过来的不同的版本号取不同的升级文件,比如oldversion_to_latestversion.xml

2007年3月16日星期五

MVP实践之:单元测试

MVP模式的应用中很重要一点是“方便了测试”。MVP中的Model层不与界面发生任何关系,按照一般的测试方法进行测试即可。但是Presenter需要view,至少需要一个View的契约,也就是它多少需要对view有一些了解,那么到底如何在没有界面的情况下测试呢?这里借用上一篇中讲到的winform的例子,以测试SearchPictOfComp为目标来实践一下。这里假设Model层的代码都经过了测试。

我们编写一个测试用例,如下:
[Test]
public void SearchArtifactOfCompTest()
{
MockViewListDescPict view;
view=测试用的View;
view.ComponentId = "265f1e5d-2c75-4368-9a3a-5da5d82270c5";
ListDescPictPresenter presenter = new ListDescPictPresenter(view);
view.AttachPresenter(presenter);
view执行click button的动作;
Assert.IsTrue(((IList)view.descPictList).Count == 6);
}
先解释一下这个测试用例的意思。首先创建一个测试用的view,然后用它初始化presenter,当view执行一个点击button的动作之后,presenter会去响应这个操作,将取出的数据传给view的列表控件显示,如果成功则列表控件的数据源中应该有6条纪录。
现在运行这个测试用例肯定是失败的,因为标记为红色的代码都是没有实现的伪代码。为了让测试通过,我们尝试实现红色标记处所要求的功能。
  • view=测试用的View
    这里实际上是要求一个满足契约IViewListDescPict的view,我们虽然没有真正的view,但是我们可以编写一个模拟的view(mock view)——可以联想在模块化测试时编写桩(stub)模块的情形。MockViewListDescPict是一个模拟的view,它实现了IViewListDescPict接口。那么一个问题就解决了:
    view=new MockViewListDescPict();
  • view执行click button的动作
    上一篇中讲到presenter是采用观察者模式,它监听button的事件。既然只要事件,那我们只要模拟一个事件就行了,并不需要真的用一个button。搞清楚button的click是产生一个普通的event,那我们定义一个:
    public EventHandler searchArtsOfComp_Event;
    事件的触发很简单:searchArtsOfComp_Event.Invoke(null, null);
    我们把触发的代码放在一个方法中,以便在测试的时候模拟:
    public void MockClickSearchButton()
    {
    searchArtsOfComp_Event.Invoke(null, null);
    }
    接下来我们要让presenter注册对该事件的监听,以便在invoke触发事件的时候能被presenter捕捉到,我们把这一步放在AttachPresener中进行:
    public void AttachPresenter(ListDescPictPresenter presenter)
    {
    this.presenter = presenter;
    searchArtsOfComp_Event = presenter.SearchPictOfComp_EventHandler;
    }
    其中presenter.SearchPictOfComp_EventHandler是一个指向方法SearchPictOfComp(见应用篇)的委托。
    到此,第二个问题也解决了:
    view.MockClickSearchButton();
现在运行一下测试用例,pass了自然高兴,可是“(IList)view.descPictList”是怎么回事?其实这是为了测试读取的数据是不是正确地传给了view的列表控件,在解释测试用例的意思已经说过了。但是我们现在使用的view都是模拟(Mock)的,button事件也是假的,那么列表也同样可是假的了。我们并不关心列表控件是不是正确的显示了数据,那是控件自己的事情,我们只关心是不是传递了正确的数据,因此这里可以模拟一个接受数据的行为。控件的datasource是object的,那我们就在mock view的内部定义一个objcect的对象来获得对传递数据的引用:
public object descPictList;
public void SetPictListDataSource(IList pictList)
{
descPictList = pictList;
}
当我们模拟了button的click事件之后,presenter根据它的内部逻辑会去调用view的
SetPictListDataSource方法,将数据传递给我们设定的接收对象descPictList,现在我们只需要检查descPictList所获得的数据是否正确就行了。如果你愿意的话可以逐条检查descPictList中的项是否正确,但其实我们只需简单地检查项的个数就可以了,因为在presenter内部未对数据做修改,只要Model层能保证取出的数据无误,这里就不会有错。

总结一下对presenter的测试,一共有三点:
1、不需要真的view,可以使用类似Mock Object这样的技术模拟一个view,只要模拟的对象满足IView的契约即可。
2、如果要从view中获得用户的数据,比如要从textbox中取得用户输入的查询条件,又或者需要将处理的结果数据发回view显示,比如将一个list的集合发送到ListBox显示,这两类问题我们都可以使用对字段或者属性的存取操作来模拟。因为我们不关心显示,只要数据正确传送即可,将显示的任务交给view,可是使得presenter的功能独立于显示层。
3、对于像用户点击button或者选择chechbox这样的交互行为,我们不需要真的用控件来测试。.net允许我们手动触发事件,比如EventHandler.Invoke(),因此我们可以用手动触发事件的方式来模拟用户的交互行为。

2007年3月15日星期四

MVP实践之:Winform应用

上面的界面显示了程序最终运行的状态,主要完成两个功能:
  1. 输入一个构件的ID号,根据Id号到ftp服务器上查找该构件所有相关的图片,并且将这些图片的名称显示在一个ListBox中。
  2. 改变选中的图片(列表中选图片名)时将图片显示在右边的PictureBox中。
上面说的那两个功能,基本上可以说是这个程序的需求了,根据要求先画出界面来。注意这里画界面仅仅只是为了明确所需要的功能,既然是MVP模式的实践,我们就必须摆脱双击控件然后直接编写代码的习惯。将MVP的三要素从这个程序剥离出来,分析清楚它们各自的角色和职责,以及如何进行交互才是最首要的工作。
三要素:
View:是一个Form,包含了一些控件
Model:这里的功能就是访问ftp取得图片信息
Presenter:从界面获得构件id,根据id从model取得图片名称列表,然后传给界面显示;从界面获得图片名,根据图片名从model取得图片,将图片传给界面显示。

右边的图是实现该程序的类图,有两点需要先说明一下:
  1. Model在哪里?
    前面的文章“MVP vs MVC”中讲到过,MVP中的Model并非DomainModel,其中甚至包含了业务逻辑,就如在本程序中充当Model角色的是一个Service类,它负责从数据源取得数据并向外提供,需要的时候会先对数据做一定的处理。这里的数据源是一个ftp服务器。
  2. IView***是干什么的?
    使用MVP的一个很重要的原因是为了方便测试,将界面与数据和控制逻辑分离之后,可以在不依赖于具体的界面的情况下对View以下的逻辑层(V、P层)的代码进行测试。但是如果没有了具体的View,presenter与View的通信如何完成呢?引入“契约”可以解决这个问题。契约的基本思想是:一个对象它保证为它的使用者(调用它的对象)提供它所承诺的功能,而承诺的功能就是契约中所规定的功能。比如有这样一个规定:
    string SelectedPictName { get;}
    意思是所有的使用者都能够得到一个PictName,而所有遵守这个契约的对象都保证提供一个PictName。
    那么,本例中的IViewListDescPict就是这样一个契约,它规定了所有实现了这一接口的View都必须保证提供接口中所规定的功能(类实现接口,就必须实现接口中所有的功能)。这样presenter就不需要一个具体的View了,它只需要一个view的契约,通过调用契约中的功能来帮助自己完成任务。
以根据id查找构件所有的图片为例,看一下是如何工作的:

步骤1-4都是一些初始化工作,窗体作为视图负责初始化自己,然后创建一个它需要使用的presenter并且将自己与presenter邦定(实际上就是获得一个对presenter的引用)。
private void FormListDescPict_Load(object sender, EventArgs e)
{
presenter =new ListDescPictPresenter(this);
AttachPresenter(presenter);//this.presenter=presenter;
}
步骤5中view接受一个用户响应,但是它自己并不做处理而是在步骤6中将处理委托给presenter。
//register the presenter as a observer
btnSearch.Click +=presenter.SearchPictOfComp_EventHandler;
步骤6-11是一套控制逻辑,全部在presenter内部处理。presenter监视view中的按钮(观察者模式),当检测到用户点击事件之后,先从view取得componentid,从service取得数据,然后刷新view的数据显示。
private void SearchPictOfComp(Object sender,EventArgs e)
{
try
{
IList descPicts = pictService.GetPictsOfComp(view.ComponentId);
view.SetPictListDataSource(descPicts);
}
catch (Exception ex)
{
//View will show the error msg to the end user
view.Message = ex.Message;
}
}
我们可以看到view也就是这里的窗体form仅仅只是将用户的输入向下层传递,它甚至不需要知道自己什么时候该刷新,只是定义了刷新的方法,真正的刷新命令由presenter发出。view中没有逻辑处理,这样可以使得view尽量的简化。复杂的控制逻辑都在presenter层处理,因此这些控制也就变得可以复用了,意味着我们可以将这一套逻辑处理用于多个view,理想的情况下可以同时支持winform的和asp.net的程序。

最后要提一下异常处理问题。由于view没有处理逻辑,使得我们在view中没有捕获异常的机会,因此只能在presenter层捕捉了。那么如何将捕获的异常消息通知view以显示给用户呢?本例中是在IView这个契约里定义了一个Message属性,presenter将捕获的异常消息写入这个属性中,只要实现IView接口的view都可以获取这个消息,并进行处理。
public string Message
{
get
{
return msg;
}
set
{
if (!string.IsNullOrEmpty(value))
{
msg = value;
//you can use the msg in the way you like
MessageBox.Show(msg);
}
}
}
上面的方法虽然是对于错误处理提出来的,但是同样可以用于其他的用途,比如我希望返回一个操作成功的提示消息。