Open Xml SDK Word模板开发最佳实践(Best Practice)

1.概述

    因为前面的引文已经对Open Xml SDK作了一个简要的介绍。html

    此次来点实际的——Word模板操做。程序员

 

    从本质上来说,本文的操做都是基于模板替换思想的,即,咱们经过替换Word模板中指定元素,来完成生成文档的目的。设计模式

 

    不罗嗦了,直接进入主题,如下是步骤:编辑器

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

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

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

     4) 逐步测试——保证可以迭代地前进单元测试

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

 

 

     术语约定:字体

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

     待替换元素——指WT中须要被替换的字符或表格或图片等。当待替换元素被所有替换后,将会生成一个客户所须要的文档,能够提供给客户下载(若是是Web App的话)。

 

2.创建领域模型

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

    若是一开始没有设计好领域模型,那么编码中容易引发混乱,因此,应该将这个过程重视。

 

 

     步骤

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

clip_image002        (点击查看大图)

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

      其余的如法炮制。

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

clip_image003        (点击查看大图)

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

clip_image004

 

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(3);

    foreach (var item in AccDetailStat.AccidentDetailItems)
    {
        //行操做:克隆一行
        var row = table.Elements<TableRow>().Last().Clone() as TableRow;

        for (int ii = 0; ii < 6; 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 0:
                    t.Text = item.Order.ToString();
                    break;
                case 1:
                    t.Text = item.VehicleNumber;
                    break;
                case 2:
                    t.Text = item.AccidentDate.ToShortDateString();
                    break;
                case 3:
                    t.Text = item.AccidentType;
                    break;
                case 4:
                    t.Text = item.Driver;
                    break;
                case 5:
                    t.Text = item.ConcludeStatus;
                    break;
            }
        }

        //
        var lastRow = table.Elements<TableRow>().Last();
        table.InsertAfter<TableRow>(row, lastRow);
    }//foreach

    //删除模板行
    table.Elements<TableRow>().ElementAt(1).Remove();
}

    代码解说:

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

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

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

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

    多两句口水:模板,模板,就是为咱们提供一个模板,将全部的格式都装在一块儿,咱们只须要查找到这个模板,而后将这个模板给替换,插入到行的尾部就能够了。避免了直接与XML打交道,这是很是幸福的事情。

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

 

 

4.设计

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

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

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

 

 

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

    我至少须要以下的方法:

    ReplaceFacadeInfo()

    ReplaceModule1()

    ReplaceModule2()

   ReplaceModule3()

clip_image005(点击查看大图)

 

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

 

 

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(1),
        PolicyStart = DateTime.Now,
        PolicyStartYear = 2014,
        PolicyStartMonth = 7,
        PolicyStartDay = 2,
        PolicyEndYear = 2015,
        PolicyEndMonth = 7,
        PolicyEndDay = 2,
        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

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

clip_image007(请点击查看大图)

 

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

    觉得经过对文档的反射,生成相应的代码,而后查找到其中的元素的地方,将其替换,而后生成便可。

    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.总结

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

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

相关文章
相关标签/搜索