因爲前面的引文已經對Open Xml SDK作了一個簡要的介紹。html
此次來點實際的——Word模板操做。程序員
從本質上來說,本文的操做都是基於模板替換思想的,即,咱們經過替換Word模板中指定元素,來完成生成文檔的目的。設計模式
不羅嗦了,直接進入主題,如下是步驟:編輯器
1) 要了解模板的業務背景——創建領域模型;ide
2) 針對每一類進行替換——積累每種Element的操做方式;函數
3) 考慮設計——讓你的代碼加強可擴展性;工具
4) 逐步測試——保證可以迭代地前進;單元測試
5) 去除噪音——排除那些不歸路。測試
術語約定:字體
WT——Word Template,指客戶提供給開發人員的文檔模板,開發人員根據此模板構建代碼,在用戶須要的時候生成一個產品文檔。
待替換元素——指WT中須要被替換的字符或表格或圖片等。當待替換元素被所有替換後,將會生成一個客戶所須要的文檔,能夠提供給客戶下載(若是是Web App的話)。
領域模型,直接決定了層(Layering)的設計,以及使用的面向對象的思想。
若是一開始沒有設計好領域模型,那麼編碼中容易引發混亂,因此,應該將這個過程重視。
步驟:
如上圖所示:CustomName表示須要替換的元素,且屬於連續文本,格式一致。
其餘的如法炮製。
有了對WT總體的分析,下一步就要考慮各類實現,這裏的實現主要是對待替換元素進行替換。
首先須要瞭解知道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 |
圖片,有一個特殊的對象表示——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()); } }
代碼解說:如代碼中的註釋所示。
表格、行、單元格:
/// <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打交道,這是很是幸福的事情。
至此,基本的元素查找和替換都掌握了。下面考慮代碼的組織方式。
因爲我不想去查找很複雜的XML,以及爲了修改和擴展都比較方便。
首先,加入我分析了WT以後得出的領域層次是這樣的:
那麼,若是我寫了一個WordTemplateManager的類來完成文檔的生成。
我至少須要以下的方法:
ReplaceFacadeInfo()
ReplaceModule1()
ReplaceModule2()
ReplaceModule3()
這樣組織代碼的意圖很明顯,垂直結構地組織,缺點很明顯,將全部的功能都放在了一個類。
這時,我瀏覽(固然,是在對模式有必定熟悉程度的基礎上,這裏並非炫耀,也沒有必要炫耀,只是描述事實而已)了一下設計模式,當遇到Builder和Chain Of Responsibility 的時候,我心動了。
這兩種模式均可以用來將垂直結構的代碼組織,變爲扁平結構的代碼組織。
Builder的適用場景:將每一個元動做(如製造輪胎,製造方向盤)抽象,獨立成爲一個部件,在須要的時候可以按需組裝。
CASE1:須要一輛汽車;
For(1 to 4)
Call 製造輪胎();
End For
Call 製造方向盤();
CASE2:須要一輛自行車
For(1 to 2)
Call製造輪胎();
End For
而我又以爲抽象「元動做」重用率不高,隨即考慮使用職責鏈,是的,最後就組織成爲一個單鏈表。
請關注代碼中的註釋。
接口
/// <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; }
關於TDD的好處,不是說說就能獲得的,也許真的一開始感受不到TDD的好處,可是嘗試了幾回耗時的開發練習以後,會發現對目標的掌握愈來愈清晰。
老闆今天說了一句話,「大部分外國程序員都以爲他人寫的代碼很垃圾,包括本身回頭看本身寫的也以爲很垃圾」。
我以爲應該對這句話進行補充,不能由於這句話而讓不少人逃避責任。
首先,這句話是現狀;
其次,補充一句「而不進行測試和重構代碼是垃圾中的戰鬥機」。
固然,前文對單元測試的目的作了簡要的分析,雖然有點理論化,全是幾個月生生死死,迷迷糊糊,突然大塊的體會啊!
思路,不免會出錯,但不要次次都錯就行。這裏提供一種參考。
這個工具,就是一個巨坑,怎麼都填不滿,不當心使用了一下,心痛啊。
第一次遇到的時候,大喜。
覺得經過對文檔的反射,生成相應的代碼,而後查找到其中的元素的地方,將其替換,而後生成便可。
1)卻不知,一個8頁的文檔,反向生成了3W+行代碼;
2)全部的代碼在一個文檔裏面;
3)不少重複的代碼,一直堆到底;
4)但試圖重構生成的代碼時,發現格式種類不少,不容易重構,若是重構好了,客戶修改模板以後,推到重來,那時哭都哭不出來了;
5)編輯代碼時,滾動到2W行左右的時候,在VS2013中編輯器卡死;
不太甘心、捨不得之下,果斷放棄。
一開始,瞭解到docx的本質就是一堆XML,想到,用XML的API(如Linq To Xml,XmlDocument)能夠遍歷、替換、保存。而後,就能夠給用戶下載了。
因而,按照這種思路嘗試,固然,之前也見過石旺大神經過這種方式生成周報的餅圖。可是,那時沒有看懂。
最後,我仍是放棄了。
1)docx的XML的文檔結構不是通常的複雜,有不少部件Parts,樣式Styles等;
2)當我去找一個文本時(如:asdfbSsdf),居然找不到。被分隔成幾個部分(asdf,bS,sdf),徹底不知道怎麼替換(後來才明白這是連續文本Run的緣由);
3)何況,我還須要記住諸多的帶<w:*>前綴的XML標記;
最終,在排除前兩個方案的基礎上,我選擇了用SDK打開一個文檔以後,用OpenElement對象去進行替換吧。
實踐證實,這個選擇沒偏離方向。
1)BaseString存儲。
因爲好久之前,我就知道docx中的圖片能夠用Base64String的方式存儲,因此,一直想把一個圖片轉換爲一個Base64String,而後替換到Word XML中。
可是須要直接用XML操做的方式,我已經被那麼多恐怖的XML標籤嚇到。(石旺大神曾經就是這樣作的,)
2)直接用Stream。
若是有一個函數可以提供Stream類型的返回值,那麼,用它吧。
過程艱辛,可是堅持不懈!
還要注重與實際進行聯繫,積累是一點一滴的,思考也是不斷完善成型的。~~