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

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

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

相關文章
相關標籤/搜索