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.總結

    過程艱辛,但是堅持不懈!

    還要注重與實際進行聯系,積累是一點一滴的,思考也是不斷完善成型的。~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM