書(shū)接上回,我們繼續(xù)來(lái)聊聊ExcelHelper的具體實(shí)現(xiàn)。 01、讀取Excel到DataSet單元測(cè)試在上一章我們主要講解了讀取Excel到DataSet的三個(gè)重載方法具體實(shí)現(xiàn),還沒(méi)來(lái)得及做單元測(cè)試,因此我們首先對(duì)這三個(gè)方法做個(gè)單元測(cè)試。具體代碼如下: [Fact] public void Read_FileName_DataSet() { //讀取所有工作簿 var dataSet = ExcelHelper.Read("Read.xlsx"); Assert.Equal(3, dataSet.Tables.Count); var table1 = dataSet.Tables[0]; Assert.Equal("Sheet1", table1.TableName); Assert.Equal("A", table1.Rows[0][0]); Assert.Equal("B", table1.Rows[0][1]); Assert.Equal("1", table1.Rows[0][2]); Assert.Equal("C", table1.Rows[1][0]); Assert.Equal("D", table1.Rows[1][1]); Assert.Equal("2", table1.Rows[1][2]); //讀取所有工作簿,并且首行數(shù)據(jù)作為表頭 dataSet = ExcelHelper.Read("Read.xlsx", true); Assert.Equal(3, dataSet.Tables.Count); table1 = dataSet.Tables[1]; var columus = table1.Columns; Assert.Equal("Sheet2", table1.TableName); Assert.Equal("E", columus[0].ColumnName); Assert.Equal("F", columus[1].ColumnName); Assert.Equal("3", columus[2].ColumnName); Assert.Equal("G", table1.Rows[0][0]); Assert.Equal("H", table1.Rows[0][1]); Assert.Equal("4", table1.Rows[0][2]); //根據(jù)工作簿名稱(chēng)sheetName讀取指定工作簿 dataSet = ExcelHelper.Read("Read.xlsx", true, "Sheet2"); Assert.Single(dataSet.Tables); Assert.Equal("Sheet2", dataSet.Tables[0].TableName); //通過(guò)工作簿名稱(chēng)sheetName讀取不存在的工作簿 dataSet = ExcelHelper.Read("Read.xlsx", true, "Sheet99"); Assert.Empty(dataSet.Tables); //同時(shí)指定sheetName和sheetNumber優(yōu)先使用sheetName dataSet = ExcelHelper.Read("Read.xlsx", true, "Sheet1", 2); Assert.Single(dataSet.Tables); Assert.Equal("Sheet1", dataSet.Tables[0].TableName); //通過(guò)工作簿編號(hào)sheetNumber讀取不存在的工作簿 dataSet = ExcelHelper.Read("Read.xlsx", true, null, 99); Assert.Empty(dataSet.Tables); //通過(guò)工作簿編號(hào)sheetNumber讀取指定工作簿 dataSet = ExcelHelper.Read("Read.xlsx", true, null, 1); Assert.Single(dataSet.Tables); Assert.Equal("Sheet1", dataSet.Tables[0].TableName); } 02、根據(jù)文件路徑讀取Excel到對(duì)象集合在上一章中我們實(shí)現(xiàn)了Excel與DataSet相互轉(zhuǎn)換,而在前面TableHelper實(shí)現(xiàn)章節(jié)中我們已經(jīng)實(shí)現(xiàn)了對(duì)象集合與表格DataTable的相互轉(zhuǎn)換,因此我們只要把這兩者結(jié)合起來(lái)就可以實(shí)現(xiàn)Excel與對(duì)象集合的相互轉(zhuǎn)換。 因?yàn)镋xcel中有多個(gè)工作簿Sheet,而每一個(gè)工作簿Sheet代表一個(gè)表格DataTable,一個(gè)表格DataTable關(guān)聯(lián)一個(gè)對(duì)象集合,因此我們約定本方法必須指定一個(gè)工作簿Sheet用來(lái)轉(zhuǎn)換對(duì)象集合,如果沒(méi)有指定則默認(rèn)讀取第一個(gè)工作簿Sheet。 而該方法通過(guò)文件完全路徑讀取到Excel文件流后,調(diào)用具體實(shí)現(xiàn)文件流處理重載方法,具體代碼如下: //根據(jù)文件路徑讀取Excel到對(duì)象集合 //指定sheetName,sheetNumber則讀取相應(yīng)工作簿Sheet //如果不指定則默認(rèn)讀取第一個(gè)工作簿Sheet public static IEnumerable<T> Read<T>(string path, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null) { using var stream = new FileStream(path, FileMode.Open, FileAccess.Read); return Read<T>(stream, IsXlsxFile(path), isFirstRowAsColumnName, sheetName, sheetNumber); } 03、根據(jù)文件流、文件名讀取Excel到對(duì)象集合在有些場(chǎng)景下,我們直接得到的就是Excel文件流,因此更通用的處理方式就是處理ExceL文件流,因?yàn)闊o(wú)論如何最終我們都是要拿到Excel文件流的。 該方法也是一個(gè)重載方法,為了方便哪些上傳文件后,有文件流,有文件名,但是不想自己處理文件后綴格式的,提供一個(gè)便捷方法,因此該方法會(huì)通過(guò)文件名識(shí)別出文件具體后綴格式,再調(diào)用下一個(gè)重載方法,具體實(shí)現(xiàn)如下: //根據(jù)文件流讀取Excel到對(duì)象集合 //指定sheetName,sheetNumber則讀取相應(yīng)工作簿Sheet //如果不指定則默認(rèn)讀取第一個(gè)工作簿Sheet public static IEnumerable<T> Read<T>(Stream stream, string fileName, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null) { return Read<T>(stream, IsXlsxFile(fileName), isFirstRowAsColumnName, sheetName, sheetNumber); } 04、根據(jù)文件流、文件后綴讀取Excel到對(duì)象集合該方法是上面兩個(gè)方法的最終實(shí)現(xiàn),具體實(shí)現(xiàn)分為兩步: (1)讀取指定工作簿Sheet到DataSet中; (2)把DataSet中第一個(gè)表格DataTable轉(zhuǎn)換為對(duì)象集合; 而這兩步都是調(diào)用之前實(shí)現(xiàn)好的方法,具體代碼如下: //根據(jù)文件流讀取Excel到對(duì)象集合 //指定sheetName,sheetNumber則讀取相應(yīng)工作簿Sheet //如果不指定則默認(rèn)讀取第一個(gè)工作簿Sheet public static IEnumerable<T> Read<T>(Stream stream, bool isXlsx, bool isFirstRowAsColumnName = false, string? sheetName = null, int? sheetNumber = null) { //讀取指定工作簿Sheet至DataSet var dataSet = CreateDataSetWithStreamOfSheet(stream, isXlsx, isFirstRowAsColumnName, sheetName, sheetNumber ?? 1); if (dataSet == null || dataSet.Tables.Count == 0) { return []; } //DataTable轉(zhuǎn)對(duì)象集合 return TableHelper.ToModels<T>(dataSet.Tables[0]); } 下面我們針對(duì)上面三個(gè)方法做個(gè)簡(jiǎn)單的單元測(cè)試,代碼如下: public class Student { public string A { get; set; } [Description("B")] public string Name { get; set; } [Description("1")] public DateTime Age { get; set; } } [Fact] public void Read_FileName_T() { //表格數(shù)據(jù)格式無(wú)法轉(zhuǎn)為對(duì)象數(shù)據(jù)類(lèi)型,則拋異常 Assert.Throws<FormatException>(() => ExcelHelper.Read<Student>("Read.xlsx", true, "Sheet1")); //表格成功轉(zhuǎn)為對(duì)象集合 var models = ExcelHelper.Read<Student>("Read.xlsx", true, "Sheet3"); Assert.Single(models); var model = models.First(); Assert.Equal("C", model.A); Assert.Equal("D", model.Name); Assert.Equal(new DateTime(2024, 11, 29), model.Age); } 05、把表格數(shù)組寫(xiě)入Excel文件流該方法是先把表格數(shù)組生成Excel的IWorkbook,然后再寫(xiě)入內(nèi)存流MemoryStream。 而表格數(shù)組轉(zhuǎn)換為IWorkbook也很簡(jiǎn)單,在IWorkbook中創(chuàng)建工作簿Sheet,然后把每個(gè)表格數(shù)據(jù)填充至相應(yīng)的工作簿Sheet中即可,具體代碼如下: //把表格數(shù)組寫(xiě)入Excel文件流 public static MemoryStream Write(DataTable[] dataTables, bool isXlsx, bool isColumnNameAsData) { //表格數(shù)組寫(xiě)入Excel對(duì)象 using var workbook = CreateWorkbook(dataTables, isXlsx, isColumnNameAsData); var stream = new MemoryStream(); workbook.Write(stream, true); stream.Flush(); return stream; } //表格數(shù)組轉(zhuǎn)為IWorkbook private static IWorkbook CreateWorkbook(DataTable[] dataTables, bool isXlsx, bool isColumnNameAsData) { //根據(jù)Excel文件后綴創(chuàng)建IWorkbook var workbook = CreateWorkbook(isXlsx); foreach (var dt in dataTables) { //根據(jù)表格填充Sheet FillSheetByDataTable(workbook, dt, isColumnNameAsData); } return workbook; } 而根據(jù)表格填充工作簿Sheet實(shí)現(xiàn)也非常簡(jiǎn)單,只需遍歷表格中每個(gè)單元格,把其值填充至對(duì)應(yīng)工作簿Sheet中相同的位置即可,當(dāng)然其中表格列名是否要作為數(shù)據(jù),需要單獨(dú)處理,具體代碼如下: //根據(jù)表格填充工作簿Sheet private static void FillSheetByDataTable(IWorkbook workbook, DataTable dataTable, bool isColumnNameAsData) { var sheet = string.IsNullOrWhiteSpace(dataTable.TableName) ? workbook.CreateSheet() : workbook.CreateSheet(dataTable.TableName); if (isColumnNameAsData) { //把列名加入數(shù)據(jù)第一行 var dataRow = sheet.CreateRow(0); foreach (DataColumn column in dataTable.Columns) { dataRow.CreateCell(column.Ordinal).SetCellValue(column.ColumnName); } } //循環(huán)處理表格的所有行數(shù)據(jù) for (var i = 0; i < dataTable.Rows.Count; i++) { var dataRow = sheet.CreateRow(i + (isColumnNameAsData ? 1 : 0)); for (var j = 0; j < dataTable.Columns.Count; j++) { dataRow.CreateCell(j).SetCellValue(dataTable.Rows[i][j].ToString()); } } } 06、把表格數(shù)組寫(xiě)入Excel文件該方法需要注意的是對(duì)于Excel文件路徑的處理,如果給定的Excel文件路徑不存在,則本方法會(huì)自動(dòng)創(chuàng)建相應(yīng)的文件夾,如果給定的Excel文件路徑中不包括文件名稱(chēng),則本方法會(huì)自動(dòng)根據(jù)當(dāng)前時(shí)間+4位隨機(jī)數(shù)的方式+.xlsx的命名方式自動(dòng)生成文件名。 處理好這些則只需要調(diào)用根據(jù)表格數(shù)組生成Excel對(duì)象方法,最后寫(xiě)入Excel文件中,具體代碼如下: //把表格數(shù)組寫(xiě)入Excel文件 public static void Write(DataTable[] dataTables, string path, bool isColumnNameAsData) { //檢查文件夾是否存在,不存在則創(chuàng)建 var directoryName = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } //檢查是否指定文件名,沒(méi)有則默認(rèn)以“時(shí)間+隨機(jī)數(shù).xlsx”作為文件名 var fileName = Path.GetFileName(path); if (string.IsNullOrEmpty(fileName)) { directoryName = Path.GetFullPath(path); fileName = DateTime.Now.ToString("yyyyMMdd-hhmmss-") + new Random().Next(0000, 9999).ToString("D4") + ".xlsx"; path = Path.Combine(directoryName, fileName); } //表格數(shù)組寫(xiě)入Excel對(duì)象 using var workbook = CreateWorkbook(dataTables, IsXlsxFile(path), isColumnNameAsData); using var fs = new FileStream(path, FileMode.Create, FileAccess.Write); workbook.Write(fs, true); } 下面我們對(duì)上面兩個(gè)寫(xiě)入方法進(jìn)行詳細(xì)的單元測(cè)試,具體如下: [Fact] public void Write_Table() { var table = TableHelper.Create<Student>(); var row1 = table.NewRow(); row1[0] = "Id-11"; row1[1] = "名稱(chēng)-12"; row1[2] = new DateTime(2024, 11, 28); table.Rows.Add(row1); var row2 = table.NewRow(); row2[0] = "Id-21"; row2[1] = "名稱(chēng)-22"; row2[2] = new DateTime(2024, 11, 29); table.Rows.Add(row2); var message = "The column name of the table cannot be mapped to an object property, and the conversion cannot be completed."; //把表格寫(xiě)入Excel,并且列名不作為數(shù)據(jù)行,結(jié)果重新讀取Excel無(wú)法和對(duì)象完成轉(zhuǎn)換 ExcelHelper.Write([table], "Write.xls", false); var exception1 = Assert.Throws<NotSupportedException>(() => ExcelHelper.Read<Student>("Write.xls", true, "Sheet0")); Assert.Equal(message, exception1.Message); //把表格寫(xiě)入Excel,并且列名作為數(shù)據(jù)行,但是重新讀取Excel時(shí)第一行沒(méi)有作為列名,結(jié)果還是無(wú)法和對(duì)象完成轉(zhuǎn)換 ExcelHelper.Write([table], "Write.xls", true); var exception2 = Assert.Throws<NotSupportedException>(() => ExcelHelper.Read<Student>("Write.xls", false, "Sheet0")); Assert.Equal(message, exception2.Message); //重新讀取Excel時(shí)第一行作為列名 var models = ExcelHelper.Read<Student>("Write.xls", true, "Sheet0"); Assert.Equal(2, models.Count()); var model = models.First(); Assert.Equal("Id-11", model.A); Assert.Equal("名稱(chēng)-12", model.Name); Assert.Equal(new DateTime(2024, 11, 28), model.Age); File.Delete("Write.xls"); } 07、把對(duì)象集合寫(xiě)入Excel文件流或Excel文件到這里這兩個(gè)方法就很好實(shí)現(xiàn)了,因?yàn)檫@兩個(gè)方法需要的所有基礎(chǔ)方法都已經(jīng)實(shí)現(xiàn),核心思路就是先把對(duì)象集合轉(zhuǎn)換為表格DataTable,然后再通過(guò)調(diào)用相關(guān)把表格數(shù)組寫(xiě)入Excel的擴(kuò)展方法實(shí)現(xiàn)即可,具體代碼如下: //把對(duì)象集合寫(xiě)入Excel文件流 public static MemoryStream Write<T>(IEnumerable<T> models, bool isXlsx, bool isColumnNameAsData, string? sheetName = null) { //對(duì)象集合轉(zhuǎn)為表格 var table = TableHelper.ToDataTable<T>(models, sheetName); //表格數(shù)組寫(xiě)入Excel文件流 return Write([table], isXlsx, isColumnNameAsData); } //把對(duì)象集合寫(xiě)入Excel文件 public static void Write<T>(IEnumerable<T> models, string path, bool isColumnNameAsData, string? sheetName = null) { //對(duì)象集合轉(zhuǎn)為表格 var table = TableHelper.ToDataTable<T>(models, sheetName); //表格數(shù)組寫(xiě)入Excel文件 Write([table], path, isColumnNameAsData); } 最后我們?cè)龠M(jìn)行一次詳細(xì)的單元測(cè)試,代碼如下: [Fact] public void Write_T() { //驗(yàn)證正常情況 var students = new List<Student>(); var student1 = new Student { A = "Id-11", Name = "名稱(chēng)-12", Age = new DateTime(2024, 11, 28) }; students.Add(student1); var student2 = new Student { A = "Id-21", Name = "名稱(chēng)-22", Age = new DateTime(2024, 11, 29) }; students.Add(student2); var message = "The column name of the table cannot be mapped to an object property, and the conversion cannot be completed."; //把對(duì)象集合寫(xiě)入Excel,并且列名不作為數(shù)據(jù)行,結(jié)果重新讀取Excel無(wú)法和對(duì)象完成轉(zhuǎn)換 ExcelHelper.Write<Student>(students, "Write_T.xls", false); var exception1 = Assert.Throws<NotSupportedException>(() => ExcelHelper.Read<Student>("Write_T.xls", true, "Sheet0")); Assert.Equal(message, exception1.Message); //把對(duì)象集合寫(xiě)入Excel,并且列名作為數(shù)據(jù)行,但是重新讀取Excel時(shí)第一行沒(méi)有作為列名,結(jié)果還是無(wú)法和對(duì)象完成轉(zhuǎn)換 ExcelHelper.Write<Student>(students, "Write_T.xls", true); var exception2 = Assert.Throws<NotSupportedException>(() => ExcelHelper.Read<Student>("Write_T.xls", false, "Sheet0")); Assert.Equal(message, exception2.Message); //重新讀取Excel時(shí)第一行作為列名 var models = ExcelHelper.Read<Student>("Write_T.xls", true, "Sheet0"); Assert.Equal(2, models.Count()); var model = models.First(); Assert.Equal("Id-11", model.A); Assert.Equal("名稱(chēng)-12", model.Name); Assert.Equal(new DateTime(2024, 11, 28), model.Age); File.Delete("Write_T.xls"); } 到這里我們整個(gè)Excel封裝就完成了,相信通過(guò)對(duì)象集合完成Excel導(dǎo)入導(dǎo)出能滿足大多數(shù)業(yè)務(wù)開(kāi)發(fā)需求。當(dāng)然如果有更復(fù)雜的業(yè)務(wù)需求,還需要我們自己去研究相應(yīng)的第三方庫(kù)。 注:測(cè)試方法代碼以及示例源碼都已經(jīng)上傳至代碼庫(kù),有興趣的可以看看。https:///hugogoos/Ideal |
|
來(lái)自: IT規(guī)劃師 > 《002.Document庫(kù)》