1.概述

由于前面的引文已经对Open Xml SDK做了一个简要的介绍。

这次来点实际的——Word模板操作。

从本质上来讲,本文的操作都是基于模板替换思想的,即,我们通过替换Word模板中指定元素,来完成生成文档的目的。

不罗嗦了,直接进入主题,以下是步骤:

1) 要了解模板的业务背景——建立领域模型

2) 针对每一类进行替换——积累每种Element的操作方式

3) 考虑设计——让你的代码增强可扩展性

4) 逐步测试——保证能够迭代地前进

5) 去除噪音——排除那些不归路

术语约定:

WT——Word Template,指客户提供给开发人员的文档模板,开发人员根据此模板构建代码,在用户需要的时候生成一个产品文档。

待替换元素——指WT中需要被替换的字符或表格或图片等。当待替换元素被全部替换后,将会生成一个客户所需要的文档,可以提供给客户下载(如果是Web App的话)。

2.建立领域模型

领域模型,直接决定了层(Layering)的设计,以及使用的面向对象的思想。

如果一开始没有设计好领域模型,那么编码中容易引起混乱,所以,应该将这个过程重视。

步骤

  • 阅读整个WT文件,标记每个待替换元素,并保证标记为Run文本;

        (点击查看大图)

如上图所示:CustomName表示需要替换的元素,且属于连续文本,格式一致。

其他的如法炮制。

  • 分析WT相关的业务,将待替换元素进行分类(Classification)分层(Layering);

        (点击查看大图)

  • 建立实体模型,用于存储和提供数据;

3.查找和替换元素

有了对WT整体的分析,下一步就要考虑各种实现,这里的实现主要是对待替换元素进行替换。

3.1文本

首先需要了解知道Word内部对象的组织方式:

WordprocessingDocument——> Body——> Paragraph——> Run——> Text

即文档,体,段落,连续文本,文本。

protected void ReplaceTextWithProperty<T>(Body body, T entity)
{
var pas = body.Elements<Paragraph>();
foreach (var pa in pas)
{
foreach (var tmpRun in pa.Elements<Run>())
{
var text = tmpRun.Elements<Text>().FirstOrDefault();
if (text != null)
{
ReplaceTextWithProperty<T>(text, entity);
}
}
}
}

代码解说:我们使用经典的XML查询API来对元素进行查询,要时刻提醒自己,Word的每一个元素就是一个XML Element,那么就不会晕了头。

ps:一个段落包括多个Run,一个Run包括多个Text。那么,什么是连续文本呢?即格式、样式、字体、类型等,需要全部一样,才算连续文本。

连续:asdfasdf

非连续:asdfad#

你好s

Asdfasdfasd

45asd

3.2图片

图片,有一个特殊的对象表示——ImagePart。

protected override void HandleRequestCore(WordprocessingDocument doc)
{
Body body = doc.MainDocumentPart.Document.Body; ReplaceTextWithProperty<PolicyRateEntity>(body, PolicyRate); if (PolicyRate.Image != null)
{
//查找1:通过名称。关于如何获得这个名称,可以在遍历的时候使用Console.WriteLine获得。
var imagePart = doc.MainDocumentPart.ImageParts.Where(zw => zw.Uri.OriginalString.Equals("/word/media/image3.png")).FirstOrDefault();
//查找2:通过索引
//imagePart = doc.MainDocumentPart.ImageParts.ElementAt(1); //替换:使用一个Stream(PolicyRate.Image)进行替换
imagePart.FeedData(PolicyRate.Image);
PolicyRate.Image.Close(); Console.WriteLine(imagePart.Uri.ToString());
}
}

代码解说:如代码中的注释所示。

3.3表格的查找以及行的复制插入

表格、行、单元格:

/// <summary>
/// 查找到指定的表格;
/// 将表格的第二行作为模板行,复制,替换,插入到尾部;
/// 最后,移除第二行
/// </summary>
/// <param name="doc"></param>
protected override void HandleRequestCore(WordprocessingDocument doc)
{
Body body = doc.MainDocumentPart.Document.Body; //查找:获取第三个表格
var table = body.Elements<Table>().ElementAt(); foreach (var item in AccDetailStat.AccidentDetailItems)
{
//行操作:克隆一行
var row = table.Elements<TableRow>().Last().Clone() as TableRow; for (int ii = ; ii < ; ii++)
{
var cell = row.Elements<TableCell>().ElementAt(ii);
var tmpPa = cell.Elements<Paragraph>().First();
var tmpRun = tmpPa.Elements<Run>().First();
var t = tmpRun.Elements<Text>().First(); switch (ii)
{
case :
t.Text = item.Order.ToString();
break;
case :
t.Text = item.VehicleNumber;
break;
case :
t.Text = item.AccidentDate.ToShortDateString();
break;
case :
t.Text = item.AccidentType;
break;
case :
t.Text = item.Driver;
break;
case :
t.Text = item.ConcludeStatus;
break;
}
} //
var lastRow = table.Elements<TableRow>().Last();
table.InsertAfter<TableRow>(row, lastRow);
}//foreach //删除模板行
table.Elements<TableRow>().ElementAt().Remove();
}

代码解说:

1)复制表格的一个空白行TableRow(带格式的,当然,不用关心这个格式什么的);

2)对这个行的每一个单元格TableCell进行复制;

3)然后将这个行插入到表格的尾部。

整个过程都是用C#代码完成,没有一点操作Word XML标记的痕迹,也不用关心其格式等。

多两句口水:模板,模板,就是为我们提供一个模板,将所有的格式都装在一起,我们只需要查找到这个模板,然后将这个模板给替换,插入到行的尾部就可以了。避免了直接与XML打交道,这是非常幸福的事情。

至此,基本的元素查找和替换都掌握了。下面考虑代码的组织方式。

4.设计

由于我不想去查找很复杂的XML,以及为了修改和扩展都比较方便。

首先,加入我分析了WT之后得出的领域层次是这样的:

  • 全局待替换元素;
  • 业务模块1;
  • 业务模块2;
  • 业务模块3;

那么,如果我写了一个WordTemplateManager的类来完成文档的生成。

我至少需要如下的方法:

ReplaceFacadeInfo()

ReplaceModule1()

ReplaceModule2()

ReplaceModule3()

(点击查看大图)

这样组织代码的意图很明显,垂直结构地组织,缺点很明显,将所有的功能都放在了一个类。

4.1模式分析

这时,我浏览(当然,是在对模式有一定熟悉程度的基础上,这里并不是炫耀,也没有必要炫耀,只是描述事实而已)了一下设计模式,当遇到BuilderChain Of Responsibility 的时候,我心动了。

这两种模式都可以用来将垂直结构的代码组织,变为扁平结构的代码组织。

4.2建造者

Builder的适用场景:将每个元动作(如制造轮胎,制造方向盘)抽象,独立成为一个部件,在需要的时候能够按需组装。

CASE1:需要一辆汽车;

For(1 to 4)

Call 制造轮胎();

End For

Call 制造方向盘();

CASE2:需要一辆自行车

For(1 to 2)

Call制造轮胎();

End For

而我又觉得抽象“元动作”重用率不高,随即考虑使用职责链,是的,最后就组织成为一个单链表。

4.2 职责链

请关注代码中的注释。

接口

/// <summary>
/// 模板处理器
/// </summary>
public interface IWordTemplateHandler
{
/// <summary>
/// 之所以传递一个WordprocessingDocument,考虑到每一个Handler都要处理,不必每次都如下打开: using (WordprocessingDocument wordprocessingDocument = WordprocessingDocument.Open(TemplateFileName, true))
/// </summary>
/// <param name="doc"></param>
void HandleRequest(WordprocessingDocument doc); IWordTemplateHandler Successor { get; set; }
}

基类

public abstract class WordTemplateHandlerBase : IWordTemplateHandler
{
public virtual void HandleRequest(WordprocessingDocument doc)
{
this.HandleRequestCore(doc);
this.TransmitNext(doc);
} /// <summary>
/// 参考MVC Controller的设计,也是AOP的一种思想体现。只需要被子类实现
/// </summary>
/// <param name="doc"></param>
protected abstract void HandleRequestCore(WordprocessingDocument doc); public IWordTemplateHandler Successor
{
get;
set;
} /// <summary>
/// 查找等效的属性名称进行替换
/// </summary>
/// <typeparam name="T">实体类型</typeparam>
/// <param name="text">文本对象</param>
/// <param name="entity">真正的实体</param>
private void ReplaceTextWithProperty<T>(Text text, T entity)
{
var type = entity.GetType();
string name = text.Text.Trim();
var propertyInfo = type.GetProperty(name);
if (propertyInfo == null) return; text.Text = propertyInfo.GetValue(entity, null).ToString();
} protected void ReplaceTextWithProperty<T>(Body body, T entity)
{
var pas = body.Elements<Paragraph>();
foreach (var pa in pas)
{
foreach (var tmpRun in pa.Elements<Run>())
{
var text = tmpRun.Elements<Text>().FirstOrDefault();
if (text != null)
{
ReplaceTextWithProperty<T>(text, entity);
}
}
}
} /// <summary>
/// 传递
/// </summary>
/// <param name="doc"></param>
private void TransmitNext(WordprocessingDocument doc)
{
if (this.Successor != null)
{
this.Successor.HandleRequest(doc);
}
}
}

其中的一个子类

/// <summary>
/// 整体外观处理
/// </summary>
public class FacadeHandler : WordTemplateHandlerBase
{
public FacadeInfoEntity HeaderInfo { get; set; } protected override void HandleRequestCore(WordprocessingDocument doc)
{
Body body = doc.MainDocumentPart.Document.Body; ReplaceTextWithProperty<FacadeInfoEntity>(body, HeaderInfo);
}
}

引擎代码

ublic static void Start(string fileName)
{
var handler = SetupHandlersChain(); using (WordprocessingDocument wordprocessingDocument =
WordprocessingDocument.Open(fileName, true))
{
handler.HandleRequest(wordprocessingDocument);
}
} private static IWordTemplateHandler SetupHandlersChain()
{
//整体
var facadeHandler = new FacadeHandler();
facadeHandler.HeaderInfo = new FacadeInfoEntity()
{
CustomName = "哈哈",
PolicyEnd = DateTime.Now.AddMonths(),
PolicyStart = DateTime.Now,
PolicyStartYear = ,
PolicyStartMonth = ,
PolicyStartDay = ,
PolicyEndYear = ,
PolicyEndMonth = ,
PolicyEndDay = ,
CurrentDay = DateTime.Now.Day,
CurrentMonth = DateTime.Now.Month
}; //模块1:
var m1 = new PolicyRateHandler();
//模块2:
var m2 = new AccidentCategoryHandler();
//模块3:
var m3 = new DriverAndVehicleNoStatHandler();
//模块4:
var m4 = new AccidentMonlyStatHandler();
//模块5:
var m5 = new AccidentDetailHandler(); facadeHandler.Successor = m1;
m1.Successor = m2;
m2.Successor = m3;
m3.Successor = m4;
m4.Successor = m5; return facadeHandler;
}

5. 逐步测试

关于TDD的好处,不是说说就能得到的,也许真的一开始感觉不到TDD的好处,但是尝试了几次耗时的开发练习之后,会发现对目标的掌握越来越清晰。

老板今天说了一句话,“大部分外国程序员都觉得他人写的代码很垃圾,包括自己回头看自己写的也觉得很垃圾”。

我觉得应该对这句话进行补充,不能因为这句话而让很多人逃避责任。

首先,这句话是现状;

其次,补充一句“而不进行测试和重构代码是垃圾中的战斗机”。

当然,前文对单元测试的目的做了简要的分析,虽然有点理论化,全是几个月生生死死,迷迷糊糊,忽然大块的体会啊!

6. 去除噪音

思路,难免会出错,但不要次次都错就行。这里提供一种参考。

6.1工具Open XML SDK Productivity Tool For Microsoft Office

这个工具,就是一个巨坑,怎么都填不满,不小心使用了一下,心痛啊。

(请点击查看大图)

第一次遇到的时候,大喜。

以为通过对文档的反射,生成相应的代码,然后查找到其中的元素的地方,将其替换,然后生成即可。

1)殊不知,一个8页的文档,反向生成了3W+行代码;

2)所有的代码在一个文档里面;

3)很多重复的代码,一直堆到底;

4)但试图重构生成的代码时,发现格式种类很多,不容易重构,如果重构好了,客户修改模板之后,推到重来,那时哭都哭不出来了;

5)编辑代码时,滚动到2W行左右的时候,在VS2013中编辑器卡死;

不太甘心、舍不得之下,果断放弃。

6.2 XML替换

一开始,了解到docx的本质就是一堆XML,想到,用XML的API(如Linq To Xml,XmlDocument)可以遍历、替换、保存。然后,就可以给用户下载了。

于是,按照这种思路尝试,当然,以前也见过石旺大神通过这种方式生成周报的饼图。但是,那时没有看懂。

最后,我还是放弃了。

1)docx的XML的文档结构不是一般的复杂,有很多部件Parts,样式Styles等;

2)当我去找一个文本时(如:asdfbSsdf),竟然找不到。被分隔成几个部分(asdf,bS,sdf),完全不知道怎么替换(后来才明白这是连续文本Run的原因);

3)况且,我还需要记住诸多的带<w:*>前缀的XML标记;

6.3 选择到一个觉得正确的方案

最终,在排除前两个方案的基础上,我选择了用SDK打开一个文档之后,用OpenElement对象去进行替换吧。

实践证明,这个选择没偏离方向。

6.4图片占位替换的方式

1)BaseString存储。

由于很久以前,我就知道docx中的图片可以用Base64String的方式存储,所以,一直想把一个图片转换为一个Base64String,然后替换到Word XML中。

但是需要直接用XML操作的方式,我已经被那么多恐怖的XML标签吓到。(石旺大神曾经就是这样做的,)

2)直接用Stream。

如果有一个函数能够提供Stream类型的返回值,那么,用它吧。

7.总结

过程艰辛,但是坚持不懈!

还要注重与实际进行联系,积累是一点一滴的,思考也是不断完善成型的。~~

Open Xml SDK Word模板开发最佳实践(Best Practice)的更多相关文章

  1. [转]Android开发最佳实践

    ——欢迎转载,请注明出处 http://blog.csdn.net/asce1885 ,未经本人同意请勿用于商业用途,谢谢—— 原文链接:https://github.com/futurice/and ...

  2. Android开发最佳实践

    Android开发最佳实践 摘要 ●使用 Gradle 和它推荐的工程结构 ●把密码和敏感数据放在gradle.properties ●不要自己写 HTTP 客户端,使用Volley或OkHttp库 ...

  3. Hadoop MapReduce开发最佳实践(上篇)

    body{ font-family: "Microsoft YaHei UI","Microsoft YaHei",SimSun,"Segoe UI& ...

  4. Android和PHP开发最佳实践

    Android和PHP开发最佳实践 <Android和PHP开发最佳实践>基本信息作者: 黄隽实丛书名: 移动应用开发技术丛书出版社:机械工业出版社ISBN:9787111410508上架 ...

  5. iOS应用开发最佳实践

    <iOS应用开发最佳实践> 基本信息 作者: 王浩    出版社:电子工业出版社 ISBN:9787121207679 上架时间:2013-7-22 出版日期:2013 年8月 开本:16 ...

  6. Web前端开发最佳实践(7):使用合理的技术方案来构建小图标

    大家都对网站上使用的小图标肯定都不陌生,这些小图标作为网站内容的点缀,增加了网站的美观度,提高了用户体验,可是你有没有看过在这些网站中使用的图标都是用什么技术实现的?虽然大部分网站还是使用普通的图片实 ...

  7. Web前端开发最佳实践(1):前端开发概述

    引言 我从07年开始进入博客园,从最开始阅读别人的文章到自己开始尝试表达一些自己对技术的看法.可以说,博客园是我参与技术讨论的一个主要的平台.在这其间,随着接触技术的广度和深度的增加,也写了一些得到了 ...

  8. web前端开发最佳实践笔记

    一.文章开篇 由于最近也比较忙,一方面是忙着公司的事情,另外一方面也是忙着看书和学习,所以没有时间来和大家一起分享知识,现在好了,终于回归博客园的大家庭了,今天我打算来分享一下关于<web前端开 ...

  9. 【读书笔记】iOS-微信公众平台开发最佳实践

    一,微信是由腾讯公司广州研发中心产品团队开发,该团队经理张小龙被称为“微信之父”,公司总裁马化腾确定该产品名称为“微信”. 二,常见问题及解决方案. 1,请求URL超时. 这种情况一般是由于服务器网速 ...

随机推荐

  1. [ZJOI3527][Zjoi2014]力

    [ZJOI3527][Zjoi2014]力 试题描述 给出n个数qi,给出Fj的定义如下: 令Ei=Fi/qi.试求Ei. 输入 包含一个整数n,接下来n行每行输入一个数,第i行表示qi. 输出 有n ...

  2. Linux下tomcat服务

    一:Linux下tomcat服务的启动.关闭与错误跟踪,使用PuTTy远程连接到服务器以后,通常通过以下几种方式启动关闭tomcat服务:切换到tomcat主目录下的bin目录(cd usr/loca ...

  3. luarocks install with lua5.1 and luajit to install lapis

    # in luarocks source directory...git clone https://github.com/archoncap/luarockscd luarocks ./config ...

  4. 【OpenStack】OpenStack系列17之OpenStack私有云设计一

    [软件系统] 1.操作系统(Minimal最小化安装): CentOS-6.6-x86_64,CentOS 6最后一个版本,官方建议版本. 相对于6.5版本: 强化对 SCSI 设备的处理,有助应付某 ...

  5. 【leetcode】Excel Sheet Column Number

    Excel Sheet Column Number Related to question Excel Sheet Column Title Given a column title as appea ...

  6. 【转】Oracle数据库中Sequence的用法

    在Oracle数据库中,sequence等同于序列号,每次取的时候sequence会自动增加,一般会作用于需要按序列号排序的地方. 1.Create Sequence (注释:你需要有CREATE S ...

  7. 32.C++不能被继承的类[C++ Final Class]

    [题目] 用C++设计一个不能被继承的类. [分析] 这是Adobe公司2007年校园招聘的最新笔试题.这道题除了考察应聘者的C++基本功底外,还能考察反应能力,是一道很好的题目. 在Java中定义了 ...

  8. 23.跳台阶问题[Fib]

    [题目] 一个台阶总共有n级,如果一次可以跳1级,也可以跳2级.求总共有多少总跳法,并分析算法的时间复杂度. [分析] 首先我们考虑最简单的情况.如果只有1级台阶,那显然只有一种跳法.如果有2级台阶, ...

  9. 修改Tomcat服务器的默认端口号

    tomcat服务器的默认端口号是8080,我们也可以修改为其他端口号,并且在没有启动Apache,IIS等占用80端口的web服务时,我们也可以设置为80端口,这样在生产中域名之后就可以不带端口号了, ...

  10. BestCoder14 1002.Harry And Dig Machine(hdu 5067) 解题报告

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5067 题目意思:给出一个 n * m 的方格,每一个小方格(大小为1*1)的值要么为 0 要么为一个正 ...