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(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()
這樣組織代碼的意圖很明顯,垂直結構地組織,缺點很明顯,將所有的功能都放在了一個類。
4.1模式分析
這時,我瀏覽(當然,是在對模式有一定熟悉程度的基礎上,這里並不是炫耀,也沒有必要炫耀,只是描述事實而已)了一下設計模式,當遇到Builder和Chain 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
這個工具,就是一個巨坑,怎么都填不滿,不小心使用了一下,心痛啊。
第一次遇到的時候,大喜。
以為通過對文檔的反射,生成相應的代碼,然后查找到其中的元素的地方,將其替換,然后生成即可。
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.總結
過程艱辛,但是堅持不懈!
還要注重與實際進行聯系,積累是一點一滴的,思考也是不斷完善成型的。~~