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


    1.概述

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

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

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

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

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

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

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

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

  • 相关阅读:
    C#的几种下载文件方法
    分享下常用的网站
    C#操作XML文件
    MySQL截取字符串函数方法
    NLog使用方法
    弹出div提示框,背景变黑
    有关URL编码问题
    javascript 压缩工具
    [C#][Windows API] 常用Windows原生方法整理(Windows API) (不定期更新: 06/16)【转】
    An Introduction to IDS
  • 原文地址:https://www.cnblogs.com/pengzhen/p/3823980.html
Copyright © 2020-2023  润新知