書接上回,我們今天繼續講解實現物件集合與DataTable的相互轉換。
01、把表格轉換為物件集合
該方法是將表格的列名稱作為類的屬性名,將表格的行資料轉為類的物件。從而實現表格轉換為物件集合。同時我們約定如果類的屬性設定了DescriptionAttribute特性,則特性值和表格列名一一對應,如果沒有設定特性則取屬性名稱和列名一一對應。
同時我們需要約束類只能為結構體或類,而不能是列舉、基礎型別、以及集合型別、委託、介面等。
類的型別校驗成功後,我們還需要校驗表格是否能轉換為物件,即判斷表格列名和類的屬性名稱或者Description特性值是否存在一致,如果沒有一個表格列名和類的屬效能對應上,則報表格列名無法對映至物件屬性,無法完成轉換異常。
當這些校驗成功後,開始迴圈處理表格行記錄,把每一行都轉換為一個物件。
我們可以透過反射動態例項化物件,再透過反射對物件的屬性動態賦值。因為我們的物件即支援類也支援結構體,因此這裡面就會遇到一個技術問題,正常的property.SetValue方法並沒有辦法給結構體動態賦值。
這是因為結構體是值型別,而property.SetValue方法的引數都是object,因此這裡面就涉及到裝箱拆箱,因此SetValue是設定了裝箱以後的物件,而並不能改變原物件。
而解決辦法就是先把結構體賦值給object變數,然後對object變數進行SetValue設定值,最後再把object變數轉為結構體。
下面我們一起看看具體實現程式碼:
//把表格轉換為物件集合
//如果設定DescriptionAttribute,則將特性值作為表格的列名稱
//否則將屬性名作為表格的列名稱
public static IEnumerable<T> ToModels<T>(DataTable dataTable)
{
//T必須是結構體或類,並且不能是集合型別
AssertTypeValid<T>();
if (0 == dataTable.Rows.Count)
{
return [];
}
//獲取T所有可寫入屬性
var properties = typeof(T).GetProperties().Where(u => u.CanWrite);
//校驗表格是否能轉換為物件
var isCanParse = IsCanMapDataTableToModel(dataTable, properties);
if (!isCanParse)
{
throw new NotSupportedException("The column name of the table cannot be mapped to an object property, and the conversion cannot be completed.");
}
var models = new List<T>();
foreach (DataRow dr in dataTable.Rows)
{
//透過反射例項化T
var model = Activator.CreateInstance<T>();
//把行資料對映到物件上
if (typeof(T).IsClass)
{
//處理T為類的情況
MapRowToModel<T>(dr, model, properties);
}
else
{
//處理T為結構體的情況
object boxed = model!;
MapRowToModel<object>(dr, boxed, properties);
model = (T)boxed;
}
models.Add(model);
}
return models;
}
//校驗表格是否能轉換為物件
private static bool IsCanMapDataTableToModel(DataTable dataTable, IEnumerable<PropertyInfo> properties)
{
var isCanParse = false;
foreach (var property in properties)
{
//根據屬性獲取列名
var columnName = GetColumnName(property);
if (!dataTable.Columns.Contains(columnName))
{
continue;
}
isCanParse = true;
}
return isCanParse;
}
//把行資料對映到物件上
private static void MapRowToModel<T>(DataRow dataRow, T model, IEnumerable<PropertyInfo> properties)
{
foreach (var property in properties)
{
//根據屬性獲取列名
var columnName = GetColumnName(property);
if (!dataRow.Table.Columns.Contains(columnName))
{
continue;
}
//獲取單元格值
var value = dataRow[columnName];
if (value != DBNull.Value)
{
//給物件屬性賦值
property.SetValue(model, Convert.ChangeType(value, property.PropertyType));
}
}
}
我們做個簡單的單元測試:
[Fact]
public void ToModels()
{
//驗證正常情況
var table = TableHelper.Create<Student<double>>();
var row1 = table.NewRow();
row1[0] = "Id-11";
row1[1] = "名稱-12";
row1[2] = 33.13;
table.Rows.Add(row1);
var row2 = table.NewRow();
row2[0] = "Id-21";
row2[1] = "名稱-22";
row2[2] = 33.23;
table.Rows.Add(row2);
var students = TableHelper.ToModels<Student<double>>(table);
Assert.Equal(2, students.Count());
Assert.Equal("Id-11", students.ElementAt(0).Id);
Assert.Equal("名稱-12", students.ElementAt(0).Name);
Assert.Equal(33.13, students.ElementAt(0).Age);
Assert.Equal("Id-21", students.ElementAt(1).Id);
Assert.Equal("名稱-22", students.ElementAt(1).Name);
Assert.Equal(33.23, students.ElementAt(1).Age);
}
02、把物件集合轉換為表格
該方法首先會呼叫根據物件建立表格方法得到一個空白表格,然後透過反射獲取物件的所有屬性,然後迴圈處理物件集合,把一個物件的所有屬性值一個一個新增行的所有列中,這樣就完成了一個物件對映成表的一行記錄,直至所有物件轉換完成即可得到一個表格。
程式碼如下:
//把物件集合轉為表格
//如果設定DescriptionAttribute,則將特性值作為表格的列名稱
//否則將屬性名作為表格的列名稱
public static DataTable ToDataTable<T>(IEnumerable<T> models, string? tableName = null)
{
//建立表格
var dataTable = Create<T>(tableName);
if (models == null || !models.Any())
{
return dataTable;
}
//獲取所有屬性
var properties = typeof(T).GetProperties().Where(u => u.CanRead);
foreach (var model in models)
{
//建立行
var dataRow = dataTable.NewRow();
foreach (var property in properties)
{
//根據屬性獲取列名
var columnName = GetColumnName(property);
//填充行資料
dataRow[columnName] = property.GetValue(model);
}
dataTable.Rows.Add(dataRow);
}
return dataTable;
}
進行如下單元測試:
[Fact]
public void ToDataTable()
{
//驗證正常情況
var students = new List<Student<double>>();
var student1 = new Student<double>
{
Id = "Id-11",
Name = "名稱-12",
Age = 33.13
};
students.Add(student1);
var student2 = new Student<double>
{
Id = "Id-21",
Name = "名稱-22",
Age = 33.23
};
students.Add(student2);
var table = TableHelper.ToDataTable<Student<double>>(students, "學生表");
Assert.Equal("學生表", table.TableName);
Assert.Equal(2, table.Rows.Count);
Assert.Equal("Id-11", table.Rows[0][0]);
Assert.Equal("名稱-12", table.Rows[0][1]);
Assert.Equal("33.13", table.Rows[0][2].ToString());
Assert.Equal("Id-21", table.Rows[1][0]);
Assert.Equal("名稱-22", table.Rows[1][1]);
Assert.Equal("33.23", table.Rows[1][2].ToString());
}
03、把一維陣列作為一列轉換為表格
該方法比較簡單就是把一個一維陣列作為一列資料建立一張表格,同時可以選擇是否填寫表名和列名。具體程式碼如下:
//把一維陣列作為一列轉換為表格
public static DataTable ToDataTableWithColumnArray<TColumn>(TColumn[] array, string? tableName = null, string? columnName = null)
{
var dataTable = new DataTable(tableName);
//建立列
dataTable.Columns.Add(columnName, typeof(TColumn));
//新增行資料
foreach (var item in array)
{
var dataRow = dataTable.NewRow();
dataRow[0] = item;
dataTable.Rows.Add(dataRow);
}
return dataTable;
}
單元測試如下:
[Fact]
public void ToDataTableWithColumnArray()
{
//驗證正常情況
var columns = new string[] { "A", "B" };
var table = TableHelper.ToDataTableWithColumnArray<string>(columns, "學生表");
Assert.Equal("學生表", table.TableName);
Assert.Equal("Column1", table.Columns[0].ColumnName);
Assert.Equal(2, table.Rows.Count);
Assert.Equal("A", table.Rows[0][0]);
Assert.Equal("B", table.Rows[1][0]);
table = TableHelper.ToDataTableWithColumnArray<string>(columns, "學生表", "列");
Assert.Equal("列", table.Columns[0].ColumnName);
}
04、把一維陣列作為一行轉換為表格
該方法也比較簡單就是把一個一維陣列作為一行資料建立一張表格,同時可以選擇是否填寫表名。具體程式碼如下:
//把一維陣列作為一行轉換為表格
public static DataTable ToDataTableWithRowArray<TRow>(TRow[] array, string? tableName = null)
{
var dataTable = new DataTable(tableName);
//建立列
for (var i = 0; i < array.Length; i++)
{
dataTable.Columns.Add(null, typeof(TRow));
}
//新增行資料
var dataRow = dataTable.NewRow();
for (var i = 0; i < array.Length; i++)
{
dataRow[i] = array[i];
}
dataTable.Rows.Add(dataRow);
return dataTable;
}
05、行列轉置
該方法是指把DataTable中的行和列互換,就是行的資料變成列,列的資料變成行。如下圖示例:
這個示例轉換,第一個表格中列名並沒有作為資料進行轉換,因此我們會提供一個可選項引數用來指示要不要把類目作為資料進行轉換。
整個方法實現邏輯也很簡單,就是以原表格行數為列數建立一個新表格,然後在迴圈處理原表格列,並把原表格一列資料填充至新表格的一行資料中,直至原表格所有列處理完成則完成行列轉置。具體程式碼如下:
//行列轉置
public static DataTable Transpose(DataTable dataTable, bool isColumnNameAsData = true)
{
var transposed = new DataTable(dataTable.TableName);
//如果列名作為資料,則需要多加一列
if (isColumnNameAsData)
{
transposed.Columns.Add();
}
//轉置後,行數即為新的列數
for (int i = 0; i < dataTable.Rows.Count; i++)
{
transposed.Columns.Add();
}
//以列為單位,一次處理一列資料
for (var column = 0; column < dataTable.Columns.Count; column++)
{
//建立新行
var newRow = transposed.NewRow();
//如果列名作為資料,則先把列名加入第一列
if (isColumnNameAsData)
{
newRow[0] = dataTable.Columns[column].ColumnName;
}
//把一列資料轉為一行資料
for (var row = 0; row < dataTable.Rows.Count; row++)
{
//如果列名作為資料,則行資料從第二列開始填充
var rowIndex = isColumnNameAsData ? row + 1 : row;
newRow[rowIndex] = dataTable.Rows[row][column];
}
transposed.Rows.Add(newRow);
}
return transposed;
}
下面進行簡單的單元測試:
[Fact]
public void Transpose_ColumnNameAsData()
{
DataTable originalTable = new DataTable("測試");
originalTable.Columns.Add("A", typeof(string));
originalTable.Columns.Add("B", typeof(int));
originalTable.Columns.Add("C", typeof(int));
originalTable.Rows.Add("D", 1, 2);
//列名作為資料的情況
var table = TableHelper.Transpose(originalTable, true);
Assert.Equal(originalTable.TableName, table.TableName);
Assert.Equal("Column1", table.Columns[0].ColumnName);
Assert.Equal("Column2", table.Columns[1].ColumnName);
Assert.Equal(3, table.Rows.Count);
Assert.Equal("A", table.Rows[0][0]);
Assert.Equal("D", table.Rows[0][1]);
Assert.Equal("B", table.Rows[1][0]);
Assert.Equal("1", table.Rows[1][1].ToString());
Assert.Equal("C", table.Rows[2][0]);
Assert.Equal("2", table.Rows[2][1].ToString());
}
注:測試方法程式碼以及示例原始碼都已經上傳至程式碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Ideal