Table of Contents

SpreadsheetDocument

SpreadsheetDocument 代表一份試算表文件,負責管理工作表定義並協調最終渲染輸出。

建立文件

若是非 DI 專案,建議使用 SpreadsheetManager 作為啟動入口:

透過 SpreadsheetManager.CreateDocument() 建立文件:

SpreadsheetManager.SetRenderer(static () => new ExcelRenderer());

SpreadsheetDocument document = SpreadsheetManager.CreateDocument();

若你的專案已使用 DI,也可直接傳入 renderer 實例建立:

SpreadsheetDocument document = new SpreadsheetDocument(new ExcelRenderer());

例如在 DI 組合根解析 renderer 後建立:

ISpreadsheetRenderer renderer = serviceProvider.GetRequiredService<ISpreadsheetRenderer>();
SpreadsheetDocument document = new SpreadsheetDocument(renderer);

文件層級設定

屬性 說明
DefaultFont 套用至整份文件的預設字型;null 代表使用 renderer 預設值
DefaultSheetName 自動產生工作表名稱時使用的前綴(預設值:「工作表」)
Styles 文件層級 named styles,供 JSON template 以 style name 引用
ContentType 匯出格式的 MIME 類型,來自 renderer
FileNameExtension 匯出格式的副檔名(含前置點),來自 renderer
SpreadsheetDocument document = SpreadsheetManager.CreateDocument();
document.DefaultFont = new CellFont("Calibri", 11);
document.DefaultSheetName = "Sheet";

文件層級 named styles 可透過 UseStyles(...) 設定:

SpreadsheetStyleRegistry styles = new();
styles.Set("Header", new CellStyle(
    HasBorder: true,
    Font: new CellFont("Calibri", 12, Style: FontStyles.Bold)
));

document.UseStyles(styles);

工作表管理

建立工作表

// 建立指定名稱的工作表
SheetDefinition sheet = document.CreateSheet("銷售報表");

// 建立指定名稱與預設列高的工作表
SheetDefinition sheet = document.CreateSheet("銷售報表", defaultRowHeight: 20);

// 自動命名(依序產生「工作表1」、「工作表2」等)
SheetDefinition sheet = document.CreateSheet();

若傳入重複名稱,會自動加上序號後綴(例如 銷售報表(1))。

存取工作表

// 取得最後一個工作表(若無則自動建立)
SheetDefinition last = document.LastSheet;

// 依零基索引取得
SheetDefinition first = document.GetSheet(0);

// 安全取得
if (document.TryGetSheet(0, out SheetDefinition sheet)) {
    // ...
}

版面診斷

GetLayoutDiagnostics() 可在匯出前檢查工作表版面。此方法只回傳診斷結果,不會輸出檔案,也不會改變文件內容。

IReadOnlyList<LayoutDiagnostic> diagnostics = document.GetLayoutDiagnostics();

foreach (LayoutDiagnostic diagnostic in diagnostics) {
    Console.WriteLine($"{diagnostic.Code}: {diagnostic.Message}");
}

若希望在發現診斷結果時直接中止流程,可使用 ValidateLayout()

try {
    document.ValidateLayout();
    document.ExportFile($"output{document.FileNameExtension}");
} catch (SpreadsheetLayoutException ex) {
    foreach (LayoutDiagnostic diagnostic in ex.Diagnostics) {
        Console.WriteLine($"{diagnostic.Code}: {diagnostic.Message}");
    }
}

目前診斷範圍包含:

  • 儲存格 range 座標或大小是否有效。
  • 儲存格 range 是否重疊。
  • 同一儲存格是否同時定義 value 與 formula。
  • 欄寬與列高是否使用有效值。

Export() 不會自動呼叫 ValidateLayout(),因此既有匯出流程維持原本行為。需要 render 前驗證時,由呼叫端明確呼叫診斷 API。

匯出

匯出至位元組陣列

適用於網頁回應或記憶體內處理:

public IActionResult Download() {
    SpreadsheetDocument document = SpreadsheetManager.CreateDocument();
    SheetDefinition sheet = document.CreateSheet("報表");
    sheet.AddTemplate(BuildTemplate());

    return File(
        document.Export(),
        document.ContentType,
        $"report{document.FileNameExtension}"
    );
}

匯出至檔案

// 若檔案存在則覆寫(預設)
document.ExportFile($@"C:\Reports\output{document.FileNameExtension}");

// 若檔案存在則拋出 IOException
document.ExportFile(
    $@"C:\Reports\output{document.FileNameExtension}",
    SpreadsheetFileMode.CreateNew
);

SpreadsheetFileMode 選項:

  • Create - 建立新檔案,若存在則覆寫(預設)
  • CreateNew - 建立新檔案,若存在則拋出例外

從 JSON 建立文件

SpreadsheetDocument.FromJson 可從 JSON 字串直接建立完整文件,適合將報表結構外部化配置:

string json = File.ReadAllText("report-template.json");
SpreadsheetDocument document = SpreadsheetDocument.FromJson(json);
document.ExportFile($"output{document.FileNameExtension}");

若要讓 JSON template 引用程式碼中建立的文件層級 named styles,可傳入 SpreadsheetStyleRegistry

SpreadsheetStyleRegistry styles = new();
styles.Set("Header", new CellStyle(
    HasBorder: true,
    Font: new CellFont("Calibri", 12, Style: FontStyles.Bold)
));

SpreadsheetDocument document = SpreadsheetDocument.FromJson(json, styles);

JSON 格式為工作表陣列,目前支援 GridRecordSetDataTableMerged 四種 template 類型:

JSON Schema 可參考 spreadsheet-exporter.schema.json。因為 JSON root 是工作表陣列,不建議把 $schema 寫入同一份 template;建議在編輯器設定中將該 schema 綁定到 template 檔案。

[
  {
    "SheetName": "Report",
    "DefaultRowHeight": 20,
    "ColumnWidths": [
      { "Index": 0, "Width": 18 },
      { "Index": 1, "Width": 24 }
    ],
    "Templates": [
      {
        "Type": "Grid",
        "Rows": [
          {
            "Height": 24,
            "Cells": [
              { "Value": "標題", "ColumnSpan": 2, "Style": { "HorizontalAlignment": "Center" } }
            ]
          }
        ]
      }
    ]
  }
]

工作表層級支援欄位

欄位 型別 說明
SheetName string 工作表名稱;省略時會自動命名
DefaultRowHeight number 工作表預設列高
Password string 工作表保護密碼
FreezePanes object 凍結窗格設定,需同時提供 RowColumn
IsAutoFilterEnabled boolean 啟用自動篩選
ColumnWidths object / array 欄寬設定,可用索引物件或 { Index, Width } 陣列
PageSettings object 列印設定,目前支援 PageOrientationPaperSize
Metadata object 附加自訂資料,供 renderer 或擴充邏輯讀取
Styles object 工作表層級 named styles,供 template 以 style name 引用
Templates array 工作表上的 template 定義

FreezePanes 範例:

{
  "FreezePanes": {
    "Row": 1,
    "Column": 0
  }
}

ColumnWidths 可用兩種格式:

{
  "ColumnWidths": {
    "0": 18,
    "1": 24
  }
}
{
  "ColumnWidths": [
    { "Index": 0, "Width": 18 },
    { "Index": 1, "Width": 24 }
  ]
}

Grid template 支援欄位

欄位 型別 說明
Type string 固定為 Grid
Rows array 列定義
Rows[].Height number 該列列高
Rows[].Cells array 儲存格定義
Rows[].Cells[].Value any 固定值
Rows[].Cells[].Formula string 公式字串
Rows[].Cells[].ColumnSpan number 欄合併數,預設 1
Rows[].Cells[].RowSpan number 列合併數,預設 1
Rows[].Cells[].StyleName string named style 名稱
Rows[].Cells[].Style object 儲存格樣式
Rows[].Cells[].DataValidation object 儲存格資料驗證

GridValueFormula 只能擇一設定。

RecordSet template 支援欄位

欄位 型別 說明
Type string 固定為 RecordSet
HeaderHeight number 標題列高度
RecordHeight number 資料列高度
Columns array 欄位定義
Records array 資料列;每筆記錄會轉成 Dictionary<string, object?>

Columns[] 支援欄位:

欄位 型別 說明
HeaderText string 欄位標題
HeaderStyleName string 標題 named style 名稱
HeaderStyle object 標題樣式
FieldStyleName string 資料儲存格 named style 名稱
FieldStyle object 資料儲存格樣式
FieldKey string Records[] 取值的欄位名稱
Value any 固定值
Formula string 固定公式
DataValidation object 資料儲存格驗證
Children array 子欄位,建立多層標題

RecordSet 欄位的 FieldKeyValueFormula 只能擇一設定。

FieldKey 支援巢狀物件路徑,例如:

{
  "HeaderText": "City",
  "FieldKey": "Customer.Address.City"
}

DataTable template 支援欄位

欄位 型別 說明
Type string 固定為 DataTable
HeaderHeight number 標題列高度
RecordHeight number 資料列高度
Columns array 欄位定義;省略時會依 Records 的物件鍵值推斷欄位
Records array 資料列;每筆記錄必須是 JSON object

Columns[] 支援欄位:

欄位 型別 說明
ColumnName string 對應 Records[] 的欄位名稱
HeaderText string / null 欄位標題;省略時使用 ColumnName
HeaderStyleName string 標題 named style 名稱
HeaderStyle object 標題樣式
FieldStyleName string 資料儲存格 named style 名稱
FieldStyle object 資料儲存格樣式
Value any 固定值
Formula string / null 固定公式
DataValidation object 資料儲存格驗證

DataTable 欄位的 ValueFormula 只能擇一設定。

Columns 省略時,欄位順序依 Records 內第一次出現的鍵值順序決定。記錄缺少欄位時,該儲存格值為 null

Merged template 支援欄位

欄位 型別 說明
Type string 固定為 Merged
Templates array 子 template 定義,支援所有已註冊的 JSON template 型別

Merged 會依子 template 順序垂直堆疊版面,行為與程式碼中的 MergedTemplate 一致。

DataValidation 支援欄位

GridRows[].Cells[].DataValidationRecordSetColumns[].DataValidationDataTableColumns[].DataValidation 使用相同欄位:

欄位 型別 說明
ValidationType string / number 驗證類型,可用 ListIntegerDecimalDateTimeTextLengthCustom
Operator string / number / null 比較運算子,可用 BetweenNotBetweenEqualNotEqualGreaterThanLessThanGreaterThanOrEqualLessThanOrEqual
Value1 any 第一個比較值
Value2 any 第二個比較值
ListItems array / null 清單驗證的可選值,項目必須是字串
Formula string / null 自訂驗證公式,或數值 / 日期 / 時間驗證的公式來源
IsDropdownShown boolean 清單驗證是否顯示下拉選單
IsBlankAllowed boolean 是否允許空白值
ErrorTitle string / null 驗證錯誤標題
ErrorMessage string / null 驗證錯誤訊息
IsErrorAlertShown boolean 是否顯示錯誤提示
PromptTitle string / null 輸入提示標題
PromptMessage string / null 輸入提示訊息
IsInputPromptShown boolean 選取儲存格時是否顯示輸入提示

未設定 ValidationType 時,使用 DataValidation 的預設驗證類型 List

Style 與 Font 支援欄位

Styles 可在文件與工作表層級宣告 named styles。文件層級 styles 透過 SpreadsheetDocument.FromJson(json, styles)document.UseStyles(styles) 提供;工作表層級 styles 則在 JSON 的 Styles 欄位宣告。

template 使用 StyleNameHeaderStyleNameFieldStyleName 引用時,會先找工作表層級 style,再找文件層級 style。找到 named style 後,inline style 欄位只覆寫 JSON 中有明確宣告的屬性。例如 inline Font.Style 只會覆寫字型樣式,不會清除 named style 中的字型名稱、大小或顏色。

{
  "Styles": {
    "Header": {
      "HasBorder": true,
      "Font": { "Style": "Bold" }
    }
  },
  "Templates": [
    {
      "Type": "Grid",
      "Rows": [
        {
          "Cells": [
            {
              "Value": "Title",
              "StyleName": "Header",
              "Style": {
                "HorizontalAlignment": "Center"
              }
            }
          ]
        }
      ]
    }
  ]
}

GridStyleRecordSetHeaderStyleFieldStyle,以及 DataTableHeaderStyleFieldStyle 目前支援以下欄位:

欄位 型別 說明
HorizontalAlignment string / number 水平對齊
VerticalAlignment string / number 垂直對齊
HasBorder boolean 是否套用框線
WrapText boolean 是否自動換行
BackgroundColor string / object 背景色
DataFormat string / null Excel 格式字串
IsLocked boolean 是否鎖定儲存格
Font object 字型設定

Font 支援欄位:

欄位 型別 說明
Name string / null 字型名稱
Size number 字型大小
Color string / object 字型顏色
Style string / number / array 字型樣式,可用 BoldItalicUnderlineStrikeout 或其數值

色彩格式支援:

  • 命名色彩,例如 Red
  • HTML 色碼,例如 #FFAA00
  • ARGB / RGB 物件,例如 { "R": 255, "G": 170, "B": 0 }

列舉值支援名稱或數值,且字串比對不分大小寫。

完整範例

[
  {
    "SheetName": "Orders",
    "DefaultRowHeight": 20,
    "Password": "sheet-pass",
    "FreezePanes": { "Row": 1, "Column": 0 },
    "IsAutoFilterEnabled": true,
    "ColumnWidths": {
      "0": 12,
      "1": 24,
      "2": 18
    },
    "PageSettings": {
      "PageOrientation": "Landscape",
      "PaperSize": "A4"
    },
    "Metadata": {
      "reportType": "monthly",
      "generatedBy": "system"
    },
    "Templates": [
      {
        "Type": "Grid",
        "Rows": [
          {
            "Height": 28,
            "Cells": [
              {
                "Value": "Order Summary",
                "ColumnSpan": 3,
                "Style": {
                  "HorizontalAlignment": "Center",
                  "HasBorder": true,
                  "BackgroundColor": "#1F4E79",
                  "Font": {
                    "Name": "Calibri",
                    "Size": 14,
                    "Color": "White",
                    "Style": ["Bold"]
                  }
                }
              }
            ]
          }
        ]
      },
      {
        "Type": "RecordSet",
        "HeaderHeight": 22,
        "RecordHeight": 20,
        "Columns": [
          { "HeaderText": "Order ID", "FieldKey": "OrderId" },
          {
            "HeaderText": "Customer",
            "FieldKey": "Customer.Name"
          },
          {
            "HeaderText": "Amount",
            "FieldKey": "Amount",
            "FieldStyle": {
              "HorizontalAlignment": "Right",
              "DataFormat": "#,##0.00"
            },
            "DataValidation": {
              "ValidationType": "Decimal",
              "Operator": "GreaterThanOrEqual",
              "Value1": 0,
              "ErrorTitle": "Invalid amount",
              "ErrorMessage": "Amount must be greater than or equal to zero."
            }
          }
        ],
        "Records": [
          {
            "OrderId": 1001,
            "Customer": {
              "Name": "Northwind"
            },
            "Amount": 1250.40
          },
          {
            "OrderId": 1002,
            "Customer": {
              "Name": "Adventure Works"
            },
            "Amount": 980.00
          }
        ]
      }
    ]
  }
]

目前未支援的 JSON 功能

  • 需要以程式碼動態計算的 generator 邏輯。
  • 自訂 template 型別,除非先自行註冊對應的 JSON parser。

JSON 錯誤診斷

SpreadsheetDocument.FromJson(...) 解析失敗時,內建 parser 會在錯誤訊息中附上診斷代碼與 JSON path。JSON root 是工作表陣列,因此第一張工作表的路徑會從 $[0] 開始。

SE-JSON-001: $[0].Templates[0].Rows[0].Cells[0].ColumnSpan must be a 32-bit integer.
SE-JSON-002: $[0].Templates[0].Type is required.
SE-JSON-003: $[0].Templates[0].Rows[0].Cells[0] cannot specify both 'Value' and 'Formula'.

註冊自訂 JSON parser

SpreadsheetDocument.FromJson(...) 會透過 JsonTemplateRegistryType 欄位尋找對應的 parser。

內建已註冊的型別有:

  • DataTable
  • Grid
  • Merged
  • RecordSet

若要支援自訂 template,可實作 ITemplateJsonParser,再於呼叫 FromJson(...) 前註冊:

using System.Text.Json;
using CloudyWing.SpreadsheetExporter;
using CloudyWing.SpreadsheetExporter.Templates;
using CloudyWing.SpreadsheetExporter.Templates.Json;

public class ReportHeaderTemplateJsonParser : ITemplateJsonParser {
    public string TypeName => "ReportHeader";

    public ISheetTemplate Parse(JsonElement element) {
        string title = element.GetProperty("Title").GetString()!;
        int columnSpan = element.GetProperty("ColumnSpan").GetInt32();
        return new ReportHeaderTemplate(title, columnSpan);
    }
}

JsonTemplateRegistry.Register(new ReportHeaderTemplateJsonParser());

SpreadsheetManager.SetRenderer(static () => new ExcelRenderer());
SpreadsheetDocument document = SpreadsheetDocument.FromJson(json);

對應 JSON:

{
  "Type": "ReportHeader",
  "Title": "Q1 Sales Report",
  "ColumnSpan": 4
}

若需要完全重設註冊內容,可先呼叫 JsonTemplateRegistry.Clear(),再視需要呼叫 JsonTemplateRegistry.RegisterBuiltins() 與自訂 parser 註冊。

自訂 renderer 行為

ExcelRenderer 提供 OnSheetCreated 虛擬方法,可在工作表建立後透過 ClosedXML API 進行額外設定:

using ClosedXML.Excel;
using CloudyWing.SpreadsheetExporter;
using CloudyWing.SpreadsheetExporter.Renderer.ClosedXML;

public class CustomExcelRenderer : ExcelRenderer {
    protected override void OnSheetCreated(IXLWorksheet worksheet, SheetLayout context) {
        // 透過 ClosedXML API 進行自訂設定
        worksheet.SheetView.ShowGridLines = false;
    }
}

SpreadsheetManager.SetRenderer(static () => new CustomExcelRenderer());

Renderer 能力宣告

Renderer 可選擇實作 ISpreadsheetRendererWithCapabilities,宣告自身支援的功能。既有只實作 ISpreadsheetRenderer 的 renderer 仍可正常使用。

ISpreadsheetRenderer renderer = new ExcelRenderer();

if (renderer is ISpreadsheetRendererWithCapabilities capableRenderer) {
    RendererCapabilities capabilities = capableRenderer.Capabilities;

    if (capabilities.SupportsMergedCells) {
        // 可使用合併儲存格相關版面。
    }
}

SpreadsheetDocument.GetRendererCapabilityDiagnostics() 可檢查文件是否使用 renderer 未宣告支援的能力。 此方法只在 renderer 實作 ISpreadsheetRendererWithCapabilities 時回傳診斷;既有 renderer 未宣告能力時,診斷結果為空集合。

IReadOnlyList<LayoutDiagnostic> diagnostics = document.GetRendererCapabilityDiagnostics();

foreach (LayoutDiagnostic diagnostic in diagnostics) {
    Console.WriteLine($"{diagnostic.Code}: {diagnostic.Message}");
}

相關主題