[轉貼] 【雛型】Docx套版列印功能試作

2012030608:59

來源:

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上實現,有幾種解法,個人評估分析如下:

  1. 利用Office Automation(就如同VBA操作Word/Excel Object Model的做法) 
    在Server端(ASP.NET)使用Office Automation技巧有些額外的風險,我自己有過不好的經驗,依MS KB的說法也不建議。
  2. 產出HTML後標註ContentType,讓Word/Excel開啟 
    先前曾貼文介紹過這種做法,但逼真度與真實doc/xls有段差距,例如: doc沒法精確分頁、xls不支援多Worksheet... 等等。
  3. 使用3rd Party元件直接產生Office文件 
    剛才提到的MS KB裡就有些建議的元件廠商,另外還有一家元件廠商ASPOSE的名字也常被提到。只是,元件的價格通常不便宜。
  4. 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,我要輕輕為你唱首歌)

因為我想做的套版只是更換特定字串,用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,即可順利編譯、執行。