先說說應用情境:我有個外部傳入的結構化資料需要套表產生 Word 表格,使用者預先做好範本 Word 檔,調好表格大小、文字對齊、字型顏色樣式... 等等:

理想目標是傳入包含編號、分類、廠牌型號、數量等屬性的物件陣列轉成 Word 表格。

using System.Collections.Generic;
using System.IO;

namespace EmbDataToDocx
{
    class Program
    {
        static void Main(string[] args)
        {
            var data = new List<EquipItem>()
            {
                new EquipItem()
                {
                    Id = "NB001",
                    Catg = EquipCatgs.筆電,
                    BrandModel = "Lenovo Thinkpad T470p",
                    Qty = 1
                },
                new EquipItem()
                {
                    Id = "MC001",
                    Catg = EquipCatgs.滑鼠,
                    BrandModel = "Logitech Anywhere2S",
                    Qty = 1
                },
                new EquipItem()
                {
                    Id = "PT001",
                    Catg = EquipCatgs.印表機,
                    BrandModel = "FX DocuPrint M225dw",
                    Qty = 1
                }
            };
            var docx = DemoDocxListGenerator.GenerateList(data);
            File.WriteAllBytes("D:\\Test.docx", docx);
        }
    }
}

結果文件範例如下:

這個 Word 檔可當成電子化作業流程的附件,必要時輸出紙本也很美觀大方,且由於範本全由使用者自訂,可符合規章制度的特殊要求。

而在這個應用裡,我希望 Word 檔具備多用途,表格部分供肉眼閱讀,匯入系統時則可從中擷取資料加以利用。之前已介紹過使用 .NET 程式擷取 Word 表格內容的技巧,用 OpenXML SDK 解析表格不是難事。但在這個案例中,Word 表格由資料物件轉換而來,再解析 Word 表格還原回來源物件等於繞了一圈,像是資料交換先印成紙本再 OCR 一般好笑。因此,我有個點子 - 設法在 Word 檔內嵌原始資料 JSON,擷取資料時直接取出 JSON 還原即可。

研究了一下,Word 文件提共所謂的自訂屬性(Csutom Property),MSDoc 有篇詳細介紹 -Set a custom property in a word processing document (Open XML SDK),自訂屬性支援日期、整數、浮點數、文字、布林等型別,複雜資料轉成 XML 或 JSON 即可當成文字儲存,故可滿足各式資料需求。

官方文件介紹得很詳細,我如法炮製了一個 AddDataJsonToDocx 方法,為 docx 加入一個名為 DataJson 的客製屬性存入資料的 JSON 內容:

public static byte[] AddDataJsonToDocx(byte[] docx, object data)
{
    using (var ms = new MemoryStream())
    {
        ms.Write(docx, 0, docx.Length);
        using (var wd = WordprocessingDocument.Open(ms, true))
        {
            var custProps = wd.CustomFilePropertiesPart;
            if (custProps == null)
            {
                custProps = wd.AddCustomFilePropertiesPart();
                custProps.Properties = new DocumentFormat.OpenXml.CustomProperties.Properties();
            }
            custProps.Properties.Append
                (new DocumentFormat.OpenXml.CustomProperties.CustomDocumentProperty()
                {
                    VTLPWSTR = new DocumentFormat.OpenXml.VariantTypes.VTLPWSTR(
                    JsonConvert.SerializeObject(data)),
                    Name = "DataJson",
                    //自訂屬性的專屬FormatId
                    FormatId = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}",
                    PropertyId = custProps.Properties.Count() + 2
                });
            wd.Save();
            return ms.ToArray();
        }
    }
}

呼叫程式稍加修改:

var docx = DemoDocxListGenerator.GenerateList(data);
var docxUpd = DemoDocxListGenerator.AddDataJsonToDocx(docx, data);
File.WriteAllBytes("D:\\Test.docx", docxUpd);

自訂屬性從 Word 也能查詢,方法是從「檔案」/「資訊」/「摘要資訊」開啟「進階摘要資訊」:

下方將會出現自訂屬性:

不過我的 DataJson 不是給人看的,是給程式讀的。下面展示如何用程式讀取自訂屬性,這裡用 PowerShell 示範:(PowerShell 直接內嵌 C# 程式碼的說明可參考前文)

Param ([Parameter(Mandatory=$true)][string]$docxPath)
$ErrorActionPreference = "STOP"
Add-Type -Path "$PSScriptRoot\DocumentFormat.OpenXml.dll"
Add-Type -TypeDefinition @"
using System.IO;
using System.Linq;
using System;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocumentFormat.OpenXml.CustomProperties;
public class DocxCustPropReader
{
    public static string ReadDataJson(string srcPath)
    {
        using (var ms = new MemoryStream(File.ReadAllBytes(srcPath)))
        {
            using (var doc = WordprocessingDocument.Open(ms, false))
            {
                if (doc.CustomFilePropertiesPart == null)
                    return `"No custom property found.`";
                var custProp = doc.CustomFilePropertiesPart.Properties
                    .Select(o => new CustomDocumentProperty(o.OuterXml))
                    .FirstOrDefault(o => o.Name == `"DataJson`");
                if (custProp != null) return custProp.VTLPWSTR.Text;
                return `"Custom property DataJson not found!`";
            }
        }
    }
}
"@ -Language CSharp -ReferencedAssemblies ("$PSScriptRoot\DocumentFormat.OpenXml.dll","$PSScriptRoot\WindowsBase.dll")
[DocxCustPropReader]::ReadDataJson($docxPath)

大成功!

留下小問題:夾帶原始資料固可省去解析表格內容的困擾,但一旦表格內容被使用者更改,便有表格內容(使用者認知)與資料 JSON (程式解讀)不一致的風險。若文件被定義為唯讀,最簡單的解法是為文件加上防修改保護,這部分留到下次再聊。

Example of setting custom porperty of docx to store extra data.


Comments

# by 黃R

第一個超連結有誤,404error@@

# by Jeffrey

to 黃R,修正了,謝謝提醒。

Post a comment