2007年4月27日星期五

save pictures from MS Word

作为word文档解析的一部分,图片的提取非常的重要。在google上搜索word图片提取找到的基本是VBA的方式,写一段脚本嵌入word中然后执行这样子,一来不熟悉,二来也不符合我对word文档处理的要求。我这里是需要从word文档外部来解析,也就是用一个程序打开word文件,读取其中的内容,这是一种从外部处理的方式,与VBA这种嵌入的方式有较大差别。其实升级到.net framework2.0之后,VSTO已经提供了很多方便操作office文件的方法,当然也包括word。这里就介绍一下借助VSTO和剪贴板来提取word图片的方法。
业务需求

有一个word文档,里面包含了一些构件描述信息和一些图片,要求找出图片另存到一个目录下,然后将该图片替换成一个指示出了图片位置的标签,比如

[img]img/PORT.doc/picture_2.Jpeg[img]

目的是为了在进一步解析构件信息,并存入数据库之后网站的表示层可以直接根据该标签找到图片并显示。
可行的解决方案


既然是要提取图片,那么首先就得在word中找到图片。图片(Picture)在word中会以两种形式存在——Shape和InlineShape——如果要取出所有的图片一定记住不要漏掉了任何一个。但是不是所有的Shape和InlineShape都是picture,我们需要先做判断:

Shape中有两种类型的picture:
MsoShapeType.msoPicture
MsoShapeType.msoLinkedPicture

InlineShape中有两种类型的picture: WdInlineShapeType.wdInlineShapePicture WdInlineShapeType.wdInlineShapeLinkedPicture

找到所有了的图片之后将它们拷贝到剪贴板,然后就可以保存了。基本步骤如下:

1. 首先打开文档。因为要替换图片,那么要求打开文档的时候是可以编辑的——ReadOnly设为false。
2. 读取所有的shape,包括Shape和InlineShape。
3. 读取一个shape判断是否为Picture,如果是则将其选中,并拷贝到剪贴板。
4. 将剪贴板的图片保存到指定目录下。
5. 找到图片在word中的位置,在其前面插入图片标记
6. 将图片从word中删除
7. 继续读取下一个shape

打开文档

oWordApp = new ApplicationClass();
object readOnly = True;

object o_fileName = fileName;
Document wordDoc;
wordDoc = oWordApp.Documents.Open(ref o_fileName,
ref missing, ref readOnly,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing,
ref missing, ref missing, ref isVisible,
ref missing, ref missing, ref missing, ref missing);
wordDoc.Activate();

读取所有的shape

IList shapes = new ArrayList();
foreach(Shape shape in doc.Shapes)
{
shapes.Add(shape);
}
foreach(InlineShape shape in doc.InlineShapes)
{
shapes.Add(shape);
}



判断是否为Picture


if (isCommonShape)
{
commonShape = (Shape) shape;
isPicture = (commonShape.Type == MsoShapeType.msoPicture ||
commonShape.Type == MsoShapeType.msoLinkedPicture);
}
else if(isInlineShape)
{
inlineShpae = (InlineShape) shape;
isPicture = (inlineShpae.Type == WdInlineShapeType.wdInlineShapePicture ||
inlineShpae.Type == WdInlineShapeType.wdInlineShapeLinkedPicture);
}
选中,并拷贝到剪贴板

if(isCommonShape)
{
commonShape.Select(ref missing);
}
else
{
inlineShpae.Select();
}

wordApp.Selection.CopyAsPicture();

图片保存到指定目录下

System.Windows.Forms.Clipboard.GetImage().Save(fileNameOfPict, ImageFormat.Jpeg);

插入图片标记

object start = oWordApp.Selection.Start; //Shape的起始位置
doc.Range(ref start, ref start).Text = string.Format("[img]{0}[img]", fileNameOfPict);

将图片从word中删除

commonShape.Delete();




完整的代码

public void ProcessAllPicturesOfDoc(Document doc)
{
IList shapes = new ArrayList();
foreach(Shape shape in doc.Shapes)
{
shapes.Add(shape);
}
foreach(InlineShape shape in doc.InlineShapes)
{
shapes.Add(shape);
}
ExtractShape(shapes,doc,oWordApp);
}
public void ExtractShape(IList shapes,Document doc,ApplicationClass wordApp)
{
object missing = Missing.Value;
string pictDirect = "img/" + doc.Name + "/";
int i = 0;

foreach (object shape in shapes)
{
bool isPicture;
bool isCommonShape = shape is Shape;
bool isInlineShape = shape is InlineShape;

Shape commonShape = null;
InlineShape inlineShpae = null;
//check if the shape is a picture
if (isCommonShape)
{
commonShape = (Shape) shape;
isPicture = (commonShape.Type == MsoShapeType.msoPicture ||
commonShape.Type == MsoShapeType.msoLinkedPicture);
}
else if(isInlineShape)
{
inlineShpae = (InlineShape) shape;
isPicture = (inlineShpae.Type == WdInlineShapeType.wdInlineShapePicture ||
inlineShpae.Type == WdInlineShapeType.wdInlineShapeLinkedPicture);
}
else
{
throw new Exception("unknown Shape");
}

if (isPicture)
{

i++;
//select the range of the shape
//Note: the difference between two methods of selection
if(isCommonShape)
{
commonShape.Select(ref missing);
}
else
{
inlineShpae.Select();
}
//compy the picture to clipboard
wordApp.Selection.CopyAsPicture();
if (System.Windows.Forms.Clipboard.ContainsImage())
{
if (!Directory.Exists(pictDirect))
Directory.CreateDirectory(pictDirect);
string fileNameOfPict = pictDirect + "picture_" + i.ToString() + ".Jpeg";
//save picture
System.Windows.Forms.Clipboard.GetImage().Save(fileNameOfPict, ImageFormat.Jpeg);
//insert the img tag just at the start position of the shape
object start = oWordApp.Selection.Start;
doc.Range(ref start, ref start).Text = string.Format("[img]{0}[img]", fileNameOfPict);
//delete the picture
if(isCommonShape)
{
commonShape.Delete();
}
else
{
inlineShpae.Delete();
}
}
else
{
throw new Exception("error occures when copying picture");
}
}
}
}

以上的代码在VS2005+Office2003+xp2下测试通过,

2007年4月4日星期三

MVP: modify data only one time

Martin Fowler提到MVP的两种实现方式,Supervising Controller 和Passive View,它们的主要区别在于View和Model之间的关系,如果View对Model有一定的了解并且可以直接操纵Model,那么就是Supervising Contrller的形式;如果View对Model一无所知,就是Passive View的形式,“MVP vs MVC”中提到的其实是后面这种。

视图的Getter/Setter主要说的是当视图操作DomainModel(或者与DomainModel对应的DTO-Data Transfer Object)的读取操作,如果使用的Passive View的方式就不用考虑这个问题了,因为这时候的View不存在对Model的操作。

Getter/setter指的什么


为什么在使用Supervising Controller的时候需要考虑?回答这个问题之前需要先说明这里说的Getter/setter分别是什么。比如我们定义一个MyView:

public class MyView{
……
public MyEntity{
get{
return myEntity;
}
set{
myEntity=value;
}
……
}

Getter是指由View提供的对某个实体(Domain Model 或 DTO)对象的读操作,presenter通过getter从view中读取实体的数据然后进行保存;Setter当然就是对应的写操作了,presenter将从model从取出的实体数据传给view就通过setter访问器。所以的这里说的getter和setter都是站在从view的外部看view的这个角度,比如说presenter对view的操作就是这种情况。

存在什么问题?


回到前面的问题,如果一个view同时有getter和setter访问器,presenter把entity通过setter传入view之后,view利用UI的数据对entity进行“更新”。注意这里的更新有两种做法,一种是由view将修改过的entity传给presenter,由后者进行更新,一种是view直接调用model中相应的方法进行更新,不要忘记了Supervising Contrller中view对model有一定的了解,它可以操作model。当采用第二种方式更新的时候view对model的了解太多,会导致view和数据访问层的联系过于紧密。初此之外,还有一个问题同时存在于这两种更新方式之中,就是对entity的修改可能既存在于view也存在于presenter,散布在多处的修改很容易导致不一致性,而且难以理解和维护。

DTO的好坏


使用DTO可以解决上面的问题。DTO是用于在层与层之间传递数据的一种中间数据形式,它的数据来自domain object,它的使用使得view不再依赖于具体的domain model,因为这时候的view只需要知道与presenter之间通讯的数据形式即可。使用DTO之后,view端得到和修改的都是DTO,presenter从view获得DTO之后进行一次DTO-Domain Object的转换,最后将转换得到的domain object存入数据库。这样一来view和presenter的分工明确,而且view对dto的修改不会影响到最终数据,因为真正的数据修改实际上只在presenter内部发生,dto只是做了临时存储之用。

使用dto也有它的坏处,如果系统很大,domain object很多,那就意味着DTO需要很多,对domain 的修改都要相应地在dto上实施,使得维护工作量变大。

用Update Method 替代Getter


这是Billy McCafferty在他的blog中提出来的方法。他提出保留view的setter访问器而“去掉getter访问器,其而代之的是由view提供一个Update方法”。这样view既可以从presenter处得到domain model的数据,又可以利用自己的Update方法更新来修改这些数据以反应用户的输入。注意这里的“Update”并不是写回了数据库,而只是是修改了Domain object的数据,还存在于内存中。以保存操作为例,基本步骤如下(Presenter:表示都是发生在presenter内部):

Presenter: view.Update(myEntity)
Presenter: presenter.Validate(myEntity)
Presenter: customerDao.SaveOrUpdate(myEntity)
可以看出对数据的修改发生在view,而presenter内部只是进行逻辑控制和数据验证。

总结


实际上DTO方式和Update Method 方式的目的都是为了控制对数据的修改,严格限制数据的修改只发生在一个地方不至于散布在各处,让view和presenter分工明确,理解和维护起来都容易。

我比较喜欢Passive View方式,因为View和Model完全分开,但是有时候Supervising的方式也很不错,比如在winform中要使用数据绑定,在asp.net中要使用Object Data Source,用后者就很方便。总之没有绝对的好坏,视情况而定。

2007年4月2日星期一

Removing the "Legacy" from your code

什么是遗留代码(Legacy Code)?

"To summarize, Legacy Code is code that is difficult, inefficient, or risky to change, but too important and useful to throw away or ignore. You have to deal with it. You can ignore it and keep going, but it might be a lot smarter to pay down the Technical Debt that you've accrued to remove friction in your development environment."

——Jeremy Miller

“Legacy” 在计算机领域的意思是“【电脑】(用于过时的软件或硬件产生的数据等) 遗留下来又难以更新的; 老化的”。Legacy code是指以前的系统中遗留下来的那些难以理解,效率低,而且维护起来有风险的代码。遗留代码通常很重要和也很有用,我们不能弃之不用。

遗留代码有什么可怕的?

  • 代码没有经过自动化测试。且不说缺少测试本身就是个问题,更重要的是没有自动化测试也意味着系统可能是无法测试,比如系统各部分的耦合度太紧密。“MVP vs MVC”提到过的Asp.net的CodeBehind方式,由于业务逻辑的代码都嵌入到了UI中,要想自动测试的困难很大。
  • 代码很难懂。比如结构不够清晰、一个方法需要你PageDown好几下才能看完、命名乱七八糟毫无意义(会带来误解的更让人头疼)、缺少必要的注释等等。
  • 难以调式。这是后继开发人员和维护人员很头疼的问题,为什么这段代码会出现异常呢?为什么输出的结果不对呢?这报的个什么错误啊,完全猜不出来是从哪里抛出来的……
  • 部署困难。系统对环境的依赖不够清楚,代码又难懂又难调式,部署起来能容易吗?
  • 反馈慢。这是自然的了,因为你要把代码编译通过都很困呢,加上测试和部署的过程又困难重重,那将是一个很长的过程。

也 许是程序员经验不足,也许是工期太紧而没时间顾及代码的质量,这些都是导致遗留代码存在的因素。已经存在的遗留代码我们只能硬着头皮去改了,但是千万不能 让我们现在正在编写着的代码又变成遗留代码了,在设计开发的过程中一旦意识到技术性问题的存在就应该尽早地进行更正,包括更新设计和重构代码,如果让它们 存在的越久那以后的修改就越困难。

"Constant small refactorings improve efficiency. Waiting too long and making a refactoring expensive is inefficient. In other words, don't allow technical debt to build up, and avoid Michael's "Tipping Point." The interest rates from Technical Debt are a killer.

"——Jeremy Miller

2007年4月1日星期日

the order of attributes reflected

更正一个错误

在前一篇“Upgrade to support multi validator attributes”中的最后一个部分中提到需要注意反射得到的属性之间的先后顺序问题:

需要注意的地方

[RegularValidator("^[0-9]$")]
[RequiredField]
private System.Windows.Forms.TextBox textBox2;

看两个属性的位置,它们不是随意摆放,要求属性之间有一定的先后顺序。这主要是因为MemberInfo.GetAttributes方法返回的属性是按一定的顺序排序的,当我们在注册事件的时候,会按照GetAttributes返回的属性数组中属性的先后顺序来为控件注册属性中的ValidatingDelegate。具体的顺序是这样的:

“离属性的目标字段(也就是要附加属性的字段)越近的属性,在反射得到的集合中位置越靠前”

也就是说如果我们要让某个属性最先起作用,那么就得让它里目标字段最近,比如例子中的RequiredField

可 是后来我发现把UserControl加入之后,Form中属性的顺序是遵从上面说的,但是UserControl的属性似乎不是那么严格。在发表这篇文 章的时候测是通过的,可是在我昨天的使用中发现UserControl中不正常的问题,不知道是因为环境的不同还是其他的什么原因。但是不管怎么样我们总 是不能依赖属性的顺序了,为了不造成误导,我必须将上面的话删除。

新的解决办法

在不能依赖属性的顺序的情况下,如何保证附加到控件上的属性所做的限定都能得到满足呢?根据上一篇中的阐述,一个属性实际上是对应到了控件的一个Validating事件,要限定属性的顺序主要是为了限定控件的Validating执行的顺序。

Validating事件乱序带来的问题

还是使用前面的例子,要给一个TextBox加2个输入的限定:
[RegularValidator("^[0-9]$")]
[RequiredField]
private System.Windows.Forms.TextBox textBox2;

我们希望这个文本框的内容不为空而且是0-9之间的一位数字。我们希望的是先执行RequiredField的检查,然后执行RegularValidator("^[0-9]$")的检查。但是如果乱序话,有可能RegularValidator("^[0-9]$")的检查在前面,这样会产生什么问题呢?比如我们输入的是“not empty”:

protected  void Validate(object sender, CancelEventArgs e)
{
e.Cancel =(!IsValid(sender));
}

上面的检查会执行两次。RegularValidator先执行,由于输入不是0-9的数字,所以检验失败:
e.Cancel=false;
然后RequiredField执行,由于输入不是空的,检验合法了:
e.Cancel=true;
看 起来似乎两次检验是两个事件,不会互相影响,但是实际上不是这样的。事件注册使用的操作符"+="实际上是将同类型事件的多个处理者 (EventHandler)串成了一个链【参考Delegate.Combine(params Delegate[] delegates)】,一旦数据源出发一个事件这个链中的处理者会相继得到事件的引用然后进行各自的处理,这也是多播委托的特性。这么说来虽然是进行多次的处理,但是它们处理的仍然是同一个事件的引用。 回到例子中,虽然是两次检验,但是是对于文本框的同一次触发事件的处理,也就是说它们处理的“e”是同一个对象。这样看来“e.Cancel”的值是先被 修改为false,然后又被置为true,那么检验应该是合法的。实施也证明了输入“not empty”的时候不会报告检验失败。

不依赖验证顺序

乱许的时候出现错误,究其原因主要是后面一次的处理覆盖了前面的处理结果,只要我们在后一次的验证中把前面已经得到的结果一起验证就可以了。对Validate处理代码稍作修改即可:

protected  void Validate(object sender, CancelEventArgs e)
{
//should pass every validation
e.Cancel =(e.Cancel|| !IsValid(sender));
   }
现在的验证要想pass,除了满足本次检验以外以前的检验也必须是pass的。这样的修改不再依赖于验证的顺序。

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);
}
}
}
上面的方法虽然是对于错误处理提出来的,但是同样可以用于其他的用途,比如我希望返回一个操作成功的提示消息。

2007年3月14日星期三

中消协与全国律协:房贷律师收费谁委托谁付费

人民网的消息:
本报北京3月13日讯 记者王比学今天从全国律师协会了解到:近来,广大消费者对房屋交易中抵押贷款环节上律师服务收费的支付多有质疑,为此中消协与全国律协申明,律师在为个人 购房抵押贷款提供法律服务过程中,要坚持谁委托谁付费的原则,即银行委托向银行收费,购房消费者委托由消费者付费,不违背消费者意愿转嫁服务收费。   为了维护广大购房抵押贷款人的合法权益,构建公平正义、诚信守规、安定有序的和谐消费环境,推动行业自律,发挥律师在房地产市场中的积极作用, 敦促相关的经营者在个人购房抵押贷款中规范自身的经营行为,中消协和全国律协今天还联合倡议:律师在代理个人购房抵押贷款业务过程中,要恪守律师职业道德 和执业纪律规范,勤勉尽责,维护当事人的合法权益;消费者在申请购房抵押贷款时,有权自主委托律师,也有权拒绝向非自主委托的律师付费。

2007年3月12日星期一

A MVP Framework for .net

Today, I find a project about MVP pattern on CodePlex.com. That project is named NMVP which means a Model-View-Presenter Framework for .Net. Here is the description from the NMVP Home:
"NMVP is an acronym for .Net Model-View-Presenter Framework.
The NMVP Framework is a framework for helping in building Model-View-Presenter (MVP) based UI architectures.

Even though it´s possible to create very simple MVP Architectures using NMVP, it really shines when used to build Composite MVP Architectures. If you don´t know MVP yet, please check out our Links page. There are several good articles about it there."
I think it's a very interesting and useful framework.Click here to know more about it .

2007年3月8日星期四

Blogger is ready for me

好久没上Google的BLogger了,进去知道才知道原来google已经将blog升级为正式版了。

Check out the new Blogger

We're out of beta and ready to go.

Google没将“beta”标记去掉,而是选择在它上面划上一个“叉”。

Cool,Google总是与众不同!

新版的Blogger已经可以与Gmail帐户绑定了,到现在为止Google的服务基本上都可以使用统一的帐户来访问了。我本来就有Gmail帐户的,只需要激活一下就可以使用Blogger了,非常的方便:
Now, I'm here!

使用之后感觉比Msn space强太多了,总体来说:功能强大,使用方便,访问速度也快,帮住中心更是提供了中文的帮助文档。
对于我所耿耿于怀的问题Blogger都给出了很好的解决方式:
1、在日志中插入图片。msn知道怎么想的,还要自己编辑html,提供了Windows Live Writter却不能上传图片,抓狂!而Blogger可以直接插入图片,也可以选择用Picassa上传。
2、Label标签或说Tag,space的简直太鸡肋了,就一个分类。
3、无法直接访问某一篇blog文章,因为它没有为blog文章生成页面,只能通过什么****entity!……,我估计很多都还不知道可以这样呢!
4、添加扩展功能非常麻烦,不像Blogger有一个简单的GUI,比如要把我的del.cio.us融入blogger只需一步就搞定了,简洁明了
5、存档管理清楚地显示了按年、月、周存档的文章信息,而space呢就一个某年某月,如果某个月没有文章俺们还得傻傻地点开来才知道呢。

还有好多呢,不说了,你自己去体会吧

MVP vs MVC

这两天一直在看MVP相关的东西,什么是MVP,怎么实现MVP可以参考下面两篇文章:
http://msdn.microsoft.com/msdnmag/issues/06/08/DesignPatterns/default.aspx
(中文版:http://www.microsoft.com/china/msdn/library/architecture/architecture/architecturetopic/MVP.mspx?mfr=true)
http://www.codeproject.com/useritems/ModelViewPresenter.asp
MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会从直接Model中读取数据而不是通过 Controller。Alex在他的blog中对于这两者之间的比较很直观也比较清楚,原文可以下面的地址找到:
http://ameleta.spaces.live.com/blog/cns!5F6316345A821420!163.entry


【译文】:

Model View Presenter vs Model View Controller

简介

在我工作中经常需要处理一些由于开发人员没能很清楚地理解MVC和MVP模式的区别的情况下使用它们而产生的问题。在这篇文章中我将会阐述一下我对两者之间区别的一些理解。
在N层体系结构中MVC/P模式仅仅只是用于表示层(presentation layer),理解这一点很重要。这两个模式并不是关于怎么构建数据层(data layer)和服务层(service layer)的,而是关于怎么将数据(data)从用户接口(view)中分离出来,以及用户接口如何与数据进行交互的。这些模式的使用让解除你的程序中表示层对对数据和控制逻辑的依赖,从而可以自由的变更表示层。

这两种模式中三个部分的一般理解

1、模型(Model)表示数据模型和业务逻辑(business logic)。模型并不总是DataSet,DataTable之类的东西,它代表着一类组件(components)或类(class),这些组件或类可以向外部提供数据,同时也能从外部获取数据并将这些数据存储在某个地方。简单的理解,可以把模型想象成“外观类(facade class)”。【译注:这里的外观是指“外观模式”中所说的外观。外观的一般作用是为一个复杂的子系统提供高层次的简单易用的访问接口,可以参看下面的图来理解它的原理:


2、视图(View)将数据层现给用户。一般的视图都只是包含用户界面(UI),而不包含界面逻辑。比如,Asp.net中包含控件的页面(page)就是一个视图。视图可以从模型中读取数据,但是不能修改或更新模型。
3、层现器(Presenter)/控制器(Controller)包含了根据用户在视图中的行为去更新模型的逻辑。视图仅仅只是将用户的行为告知控制器,而控制器负责从视图中取得数据然后发送给模型。

MVC/P模式的核心是为了将模型从视图/控制器中分离出来,从而使得模型独立于它们,因此模型不包含对视图和控制的引用。

什么是MVC(Model View Presenter)模式?

1、为了使得视图接口可以与模型和控制器进行交互,控制器执行一些初始化事件
2、用户通过视图(用户接口)执行一些操作
3、控制器处理用户行为(可以用观察着模式实现)并通知模型进行更新
4、模型引发一些事件,以便将改变发告知视图
5、视图处理模型变更的事件,然后显示新的模型数据
6、用户接口等待用户的进一步操作

这一模式的有一下几个要点:
1、视图并不使用控制器去更新模型。控制器负责处理从视图发送过来的用户操作并通过与模型的交互进行数据的更新
2、控制器可以和视图融合在一块。Visual Studion中对Windows Forms的默认处理方式就是这样的。【译注:比如我们双击一个Button,然后在它的事件里写处理逻辑,然后将处理的数据写回模型中。这里处理逻辑时间应该是控制器的功能,但是我们并没有专门写一个控制器来做这件事情而是接受了VS的默认处理方式,将它写在Form的代码中,而这里的Form在MVC中它就是一个View。所以这说vs默认的处理方式是将把控制器和视图融合在一起的。】
3、控制器不包含对视图的渲染逻辑(rendering logic)

“主动—MVC”模式,也是通常意义下的MVC模式

【译注:为什么说是主动的?View不是等Controller通知它Model更新了然后才从Model取数据并更新显示,而是自己监视Model的更新(如果用观察者模式)或主动询问Model是否更新。前面那种等待Controller通知的方式是下面所介绍的“被动—MVC”的实现方式。】

“被动—MVC”模式
与主动MVC的区别在于:
1、模型对视图和控制器一无所知,它仅仅是被它们使用
2、控制器使用视图,并通知它更新数据显示
3、视图仅仅是在控制器通知它去模型取数据的时候它才这么做(视图并不会订阅或监视模型的更新)
4、控制器负责处理模型数据的变化
5、控制器可以包含对视图的渲染逻辑

MVP模式

与“被动—MVC模式”很接近,区别在于“视图并不使用模型”。在MVP模式中视图和模型是完全分离的,他们通过Presenter进行交互。
Presenter与控制器非常相似,但是它们也有一些的区别:
1、Presenter处理视图发送过来的用户操作(在MVC中视图自己处理了这些操作)
2、它用更新过的数据去更新模型(在被动MVC中控制器只是通知视图去更新过的模型中去取新的数据,而主动MVC中模型通知视图去更新显示,控制器不需要做工作)
3、检查模型的更新(与被动MVC一样)
4、(与MVC的主要区别)从模型中取数据然后将它们发送到视图中
5、(与MVC的主要区别)将所做的更新告知视图
6、(与MVC的区别)用Presenter渲染视图

MVP的优势

1、模型与视图完全分离,我们可以修改视图而不影响模型
2、可以更高效地使用模型,因为所以的交互都发生在一个地方——Presenter内部
3、我们可以将一个Presener用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。
4、如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)

MVP的问题

由于对视图的渲染放在了Presenter中,所以视图和Persenter的交互会过于频繁。

还有一点你需要明白,如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了。比如说,原本用来呈现Html的Presenter现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。