公司專案的需求,常有將資料套進範例檔或是做報表的需求,若是每次套版都要重新寫迴圈、Bind資料,會吐,真的會吐TT。所以這篇除了aspose基本的使用,主軸會放在自訂擴充方法的部分。
首先,先來說明aspose。Aspose是一個開發文檔控制相關套件的公司,需要購買憑證才能有完整功能,套件提供了許多操控文檔結構、資料及樣式的實作。以下我會說明 .doc 及 .xlsx 在aspose中如何實作 bind data:
Aspose.Words
文檔控制在實作上,主要是找到對應欄位或位置,並對該位置作改動,而文檔會有不同的結構,如cells、tables、prograph等,不同的結構在Asp.Net可以當作物件,同時aspose提共了方法來操作這些結構。
doc文檔中,我們可以透過 Mergefield 來找到目標位置在文檔中的位置。如圖中所示,可以使用 word 中的插入功能變數來新增一個MergeField
並撰寫程式如下,將範例檔欄位名稱與資料作對應
Document doc = new Document("template.docx"); // 取得範例檔
// new 一個 DataTable 物件來接收資料
var table = new DataTable();
table.Columns.Add("UserName", string); // 給欄位名稱及型別
var row = table.NewRow();
row[0] = "Mike" // 給值
table.Rows.Add(row); // 加回 DataTable
// 進行郵件合併
doc.MailMerge.Execute(table);
// 存檔
doc.Save("merged.docx");
結果如下,可以看到UserName已經被取代為"Mike"
我們也可以透過bookmark的方式來控制文檔,但這部分並非此篇重點,就不細談。
Aspose.Cells
xlsx 文檔中,則是透過 "&=" 字符來標示欄位,後面接 table Name 及 colum Name
並撰寫程式如下,將範例檔欄位名稱與資料作對應
var workBook = new Workbook("test.xlsx"); // 取得範例檔
var designer = new WorkbookDesigner(); // designer 物件 用以處理文檔
designer.Workbook = workBook;
var test = new DataTable();
/*
datatable 部分
*/
// 給定 tablename
test.TableName = "data";
var dataSet = new DataSet();
dataSet.Tables.Add(test); // 將 datatable塞到dataset
designer.SetDataSource(dataSet); // 透過 designer 來確認資料來源
designer.Process(true); // binding
值得注意的是,xlsx 文檔內 所有物件的上層都會有 table,因此必須給範例檔中相同的 table name 才對應的到。
Bind 完會自動依資料筆數取代字符。資料及結果如下:
自訂擴充方法
在實作時,我們通常會有不同區塊的欄位需要填寫,有時某些資料會是列表,需要動態去作欄位的新增,所以共用方法必須考慮可以去解析.Net資料的結構,特別是像葡萄串一樣的資料。
DataTable
我預設將接近來的資料分為單筆及多筆,用不同方式去處理。
單筆部分
只取非泛型、非列表的欄位,並根據這些欄位及資料塞入datatable中
/// <summary>
/// 單一field bind 後回傳table
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <returns></returns>
private static DataTable GetBindDataSingleField<T>(T data)
{
var table = new DataTable();
// 取得資料的 名稱、屬性 列表 // 列表內資料非泛型、非列表
var columnFieldList = data.GetType().GetProperties().Where(p => !(p.PropertyType.IsGenericType && p is IList)).Select(x => new { x.Name, x.PropertyType }).ToList();
foreach (var column in columnFieldList)
{
// 塞到欄位中 類型為基礎類型或屬性的類型
table.Columns.Add(column.Name, Nullable.GetUnderlyingType(column.PropertyType) ?? column.PropertyType);
}
var row = table.NewRow();
// 取得資料的 名稱、值 列表
var dataMapping = data.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(data, null)).ToList();
// 取得資料的 值 列表
var columnNameList = columnFieldList.Select(m => m.Name).ToList();
foreach (var item in dataMapping)
{
// 名稱若有對到
if (columnNameList.Any(x => x == item.Key))
{
// 名稱對應的row 塞值 // 找不到報null錯
row[item.Key] = item.Value ?? DBNull.Value;
}
}
table.Rows.Add(row);
return table;
}
非單筆部分
逐筆處理資料為泛型、列表的資料,一樣根據這些欄位及資料塞入datatable中
/// <summary>
/// 列表field bind 後回傳table
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="fillNewRowWhenEmpty"></param>
/// <returns></returns>
private static List<DataTable> GetBindDataListField<T>(T data, bool fillNewRowWhenEmpty)
{
var tables = new List<DataTable>();
// 取得資料的 名稱、屬性 列表 // 列表內資料為泛型、列表
var listTypeFields = data.GetType().GetProperties().Where(p => p.PropertyType.IsGenericType && p.GetValue(data, null) is IList).ToList();
foreach (var item in listTypeFields)
{
var table = new DataTable
{
TableName = item.Name
};
// 取得屬性的類型泛型 非單一時報錯
var type = item.PropertyType.GetGenericArguments().Single();
// 取得資料的 名稱、屬性 列表
var columnFieldList = type.GetProperties().Select(x => new { x.Name, x.PropertyType }).ToList();
// List<T> 中取得 T 的 屬性名稱、類型
foreach (var column in columnFieldList)
{
table.Columns.Add(column.Name, Nullable.GetUnderlyingType(column.PropertyType) ?? column.PropertyType);
}
// 取得列表值
var rowDataSource = (IList)item.GetValue(data, null);
foreach (var rowDataItemObj in rowDataSource)
{
var rowDataItem = Convert.ChangeType(rowDataItemObj, type);
// 取得資料的 名稱、值 列表
var dataMapping = rowDataItem.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(rowDataItem, null)).ToList();
var row = table.NewRow();
foreach (var fieldData in dataMapping)
{
// 名稱若有對到
if (columnFieldList.Any(x => x.Name == fieldData.Key))
{
// 名稱對應的row 塞值 // 找不到報null錯
row[fieldData.Key] = fieldData.Value ?? DBNull.Value;
}
}
table.Rows.Add(row);
}
if (table.Rows.Count == 0 && fillNewRowWhenEmpty)
{
table.Rows.Add(table.NewRow());
}
tables.Add(table);
}
return tables;
}
完成以上兩個function,我們便有了可以將資料轉成 DataTable 的方法。接下來,由於 docx 與 xlsx 實作 binding 方式的不同,這邊分開來寫:
Document
/// <summary>
/// bind doc中同名欄位
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="doc"></param>
/// <param name="data"></param>
/// <param name="fillNewRowWhenEmpty"></param>
/// <returns></returns>
public static Document BindData<T>(this Document doc, T data, bool fillNewRowWhenEmpty = false)
{
#region 郵件合併 doc內只能用 功能變數 -> MergyField
// Data 類別第一層
// 取得單一field bind後table
var tableSingleField = GetBindDataSingleField(data);
// doc跟bind後table合併
doc.MailMerge.Execute(tableSingleField);
// Data 類別第二層(List內的List)
// 取得列表 field bind後table
var tables = GetBindDataListField(data, fillNewRowWhenEmpty);
// doc跟bind後table合併
foreach (var item in tables)
{
doc.MailMerge.ExecuteWithRegions(item);
}
doc.Save("Test.docx", Aspose.Words.SaveFormat.Docx);
#endregion
#region ReportingEngine 可用 <<[]>> 較靈活 較簡單
// 將模板文檔與生成report
var engine = new ReportingEngine();
engine.BuildReport(doc, data);
#endregion
return doc;
}
Workbook
/// <summary>
/// 把資料合併到 workBook 中同名的㯗位
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="doc"></param>
/// <param name="data"></param>
/// <returns></returns>
public static Workbook BindData<T>(this Workbook workBook, T data, bool fillNewRowWhenEmpty = true)
{
var designer = new WorkbookDesigner();
designer.Workbook = workBook;
var dataSet = new DataSet();
// Data 類別第一層
// 取得單一field bind後table
var test = GetBindDataSingleField(data);
// 多設table名與第一層資料對應
test.TableName = "data";
dataSet.Tables.Add(test);
designer.SetDataSource(dataSet);
// Data 類別第二層(List內的List)
// 取得列表 field bind後table
var listTables = GetBindDataListField(data, fillNewRowWhenEmpty);
dataSet.Tables.AddRange(listTables.ToArray());
// set回 workbook
designer.SetDataSource(dataSet);
// binding // 保留計算並生成excel
designer.Process(true);
return workBook;
}
使用自訂擴充作 Data Binding
完成以上,我們便可在web專案只考慮資料及範例檔,就能完成套印
/// <summary>
/// 取得doc檔匯出串流
/// </summary>
/// <returns></returns>
public ActionResult ExportDocxFile()
{
// 假資料
var data = _FakeMultiLayerList.FakeListForBind(); // 假資料
var doc = new Document("test.docx"); // 開啟範例文檔
doc.BindData(data); // 使用 自訂bind擴充
doc.Save("bindedDoc.docx", Aspose.Words.SaveFormat.Docx);
return File(doc.GetFileStream(Aspose.Words.SaveFormat.Docx), "application/docx");
}
/// <summary>
/// 取得xlsx檔匯出串流
/// </summary>
/// <returns></returns>
public ActionResult ExportXlsxFile()
{
var data = _FakeMultiLayerList.FakeListForBind(); // 假資料
var workBook = new Workbook("test.xlsx"); // 開啟範例文檔
workBook.BindData(data); // 使用 自訂bind擴充
workBook.Save("bindedDoc.xlsx", Aspose.Cells.SaveFormat.Xlsx);
return File(workBook.GetFileStream(Aspose.Cells.SaveFormat.Xlsx), "application/xlsx");
}
結語
這邊的擴充,是考慮了我在工作上常用到的檔案套印,資料通常都是較扁平的,並且多數沒有作樣式的調整(樣式的部分通常為客製的,很難寫成通用方法),若先把擴充寫好,便只需考慮範例文檔的調整,省了很多事。