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的。这样的修改不再依赖于验证的顺序。