來源:
http://blog.darkthread.net/post-2009-07-30-docx-templating.aspx
http://blog.darkthread.net/post-2011-08-14-openxml-template-parser.aspx
在我的程式開發生涯中,套版輸出指定格式的報表/表單一直是揮之不去的煩人差事,沒什麼營養,偏偏在每個案子裡幾乎都像小強一樣冒出來。
面對這類需求,轉成網頁是下策,因為列印時排版格式常會亂到一塌糊塗,鮮少讓人滿意。在經驗裡,Reporting Service是不錯的選擇(而且免費)。
但有些報表如確認書、通知書,在格式上並非Gird格式,跟Reporting Service最擅長的表格呈現有點差距,數量一多,要將User提供的Word檔一一轉成Reporting Service報表便成了苦差事,尤其某些文件被要求必須模仿到跟原始樣版分毫不差,常為了一兩公釐急死大丈夫。(有時,所謂原有樣版不過是某個User信手捻來的作品,並非官頒公訂,也沒人說過排版必須完全相符,但是你也知道的,各地風俗民情不同,尤其是澳洲...)
當以上情境發生,直接把User提供Word樣版裡文字換一換,照樣生出個Word檔來,在我看來是最簡便直覺省工的解法。
依我所知,動態產生Word要在ASP.NET上實現,有幾種解法,個人評估分析如下:
- 利用Office Automation(就如同VBA操作Word/Excel Object Model的做法)
在Server端(ASP.NET)使用Office Automation技巧有些額外的風險,我自己有過不好的經驗,依MS KB的說法也不建議。 - 產出HTML後標註ContentType,讓Word/Excel開啟
先前曾貼文介紹過這種做法,但逼真度與真實doc/xls有段差距,例如: doc沒法精確分頁、xls不支援多Worksheet... 等等。 - 使用3rd Party元件直接產生Office文件
剛才提到的MS KB裡就有些建議的元件廠商,另外還有一家元件廠商ASPOSE的名字也常被提到。只是,元件的價格通常不便宜。 - docx/xlsx OpenXML
Office 2007在檔案格式上做了大改革,揚離了過去的獨有二進位格式,變成公開的標準,而docx/xlsx,其實是ZIP起來的一堆XML及資源檔。這麼一來,修改XML也比以前搞封閉的二進位檔案格式簡單多了,也因為標準公開,可用的技術資源也多。
使用docx/xlsx固然便捷,缺點是產出的docx/xlsx Office 2003/2000無法直接開啟,所幸微軟提供了免費的Word Viewer可以用來檢視,另外也有Microsoft Office Word、Excel 及 PowerPoint 2007 檔案格式相容性套件能將docx/xlsx轉存為doc/xls。在我看來,這點是可被容忍的缺陷。因此,我決定設法試做OpenXML的解決方案。
雖然我們可以自行將docx Rename成.zip,解壓縮後取出XML修改再存回壓縮回去,但MS提供了更好的支援服務: (SDK文件裡的範例寫得簡單明瞭,讓我一下子就上手!! 哦哦~~ Open XML Format SDK,我要輕輕為你唱首歌)
- Open XML Format SDK 1.0
- Open XML Format SDK 2.0 (雖然在物件模型上更為結構化,呼叫起來更方便,但仍在Community Techinal Preview階段)
因為我想做的套版只是更換特定字串,用1.0的寫法也不算太鳥,加上打算用來Production環境,不想承擔CTP的風險。所以我決定現階段先用1.0 SDK正式版來實作,日後有更複雜運用時再改版。
今天只花了幾個小時,還真的把雛型做出來了:
1.先用Word 2007存一個docx檔案,把其中要動態更換的文字用[$keyName$]的格式標起來。(這種[$...$]的格我還為它取了個名字--Parser Tag,從ASP時代,我就一直用它來玩範本套表的把戲)
2.寫一小段程式,把[$keyName$]要更換的文字轉成Dictionary ,連同範本的檔案路徑一起傳給套表輔助元件DocxHelper,傳回的byte[]就是套表好的docx檔案內容。
protected void btnExport_Click(object sender, EventArgs e)
{
string template = Server.MapPath("Notice.docx");
Dictionary<string, string> dct = new Dictionary<string, string>()
{ { "today", DateTime.Today.ToString("yyyy-MM-dd") },
{ "name", txtName.Text },
{ "addr", txtAddr.Text },
{ "amount", txtAmount.Text } };
Response.Clear();
Response.ContentType = "application/octet-stream";
Response.AddHeader("content-disposition", "attachment;filename=Notice.docx");
Response.BinaryWrite(
Darkthread.OpenXml.DocxHelper.MakeDocx(
Server.MapPath("Notice.docx"),
dct)
);
Response.End();
}
3.實際跑起來,按下【匯出DOCX】,TextBox裡的文字就會被填進下載的docx中,很棒吧!!
天哪,我真的中了大樂透了嗎? 不會吧? 莫非有人在耍我? (謎之聲: 不就是你自己嗎? 找時間去看一下精神科好唄?)
我打算在手邊的小案子試行這個新元件,陸續蒐集一些問題跟情境,待元件較為成熟後,到時再推出懶人包跟大家分享。
關於Docx套版列印功能程式
前幾天又有網友問起【雛型】Docx套版列印功能試作的程式範例。
當時文章發佈後,網友ABC, alan也問過何時釋出的問題。當時的考量是,我完成的只是PoC(Proof of Concept),尚非經實務驗證可行的解決方案,況且當下專案正式上線在即(文章中寧可用較簡陋的1.0正式版,也不要用2.0 CTP可見一斑),很快就可以檢驗這個做法的彈性及可靠度,等通過考驗再公佈也不遲。無奈,世事多變化,研究出套版方法沒多久,User調整了作業流程規劃,系統不再需要自行套版產生docx... orz 而後來,在其他專案也沒能再遇到實際上場的機會。就這樣,多年下來,這套做法一直維持在PoC階段,只能終年長坐冷板凳,抑鬱不得志,每日藉酒澆愁...
適逢網友再問起,就決定把當時的PoC程式分享出來,供需要參考的朋友自行取用。但必須聲明,這個版本只是PoC,沒實際上過戰場,也跟懶人包不同,在程式考量周延性或完整度上可能仍有欠缺,大家在應用時可想像成是小麥種子,而不是烤好的麵包。也很歡迎有將它實際應用在專案上的朋友能回饋分享自己裁種、收割、烘焙的心得~
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DocumentFormat.OpenXml.Packaging;
using System.IO;
namespace Darkthread.OpenXml
{
/// <summary>
/// Ver 1.0 By Jeffrey Lee, 2009-07-29
/// </summary>
public class DocxHelper
{
/// <summary>
/// Replace the parser tags in docx document
/// </summary>
/// <param name="oxp">OpenXmlPart object</param>
/// <param name="dct">Dictionary contains parser tags to replace</param>
private static void parse(OpenXmlPart oxp, Dictionary<string, string> dct)
{
string xmlString = null;
using (StreamReader sr = new StreamReader(oxp.GetStream()))
{ xmlString = sr.ReadToEnd(); }
foreach (string key in dct.Keys)
xmlString = xmlString.Replace("[$" + key + "$]", dct[key]);
using (StreamWriter sw = new StreamWriter(oxp.GetStream(FileMode.Create)))
{ sw.Write(xmlString); }
}
/// <summary>
/// Parse template file and replace all parser tags, return the binary content of
/// new docx file.
/// </summary>
/// <param name="templateFile">template file path</param>
/// <param name="dct">a Dictionary containing parser tags and values</param>
/// <returns></returns>
public static byte[] MakeDocx(string templateFile, Dictionary<string, string> dct)
{
string tempFile = Path.GetTempPath() + ".docx";
File.Copy(templateFile, tempFile);
using (WordprocessingDocument wd = WordprocessingDocument.Open(
tempFile, true))
{
//Replace document body
parse(wd.MainDocumentPart, dct);
foreach (HeaderPart hp in wd.MainDocumentPart.HeaderParts)
parse(hp, dct);
foreach (FooterPart fp in wd.MainDocumentPart.FooterParts)
parse(fp, dct);
}
byte[] buff = File.ReadAllBytes(tempFile);
File.Delete(tempFile);
return buff;
}
}
}
PS: 雖然當時程式是配合SDK 1.0寫的,經實測這段程式可配合Open XML Format SDK 2.0運作無誤,專案要參照DocumentFormat.OpenXml以及WindowsBase,即可順利編譯、執行。