C# 泛型列表扩展,用于将数据输出到格式化字符串、CSV 文件和 Excel 工作表窗口
使用扩展方法将通用列表中的数据导出到格式化字符串、CSV 文件或 Excel 工作表窗口,并可选择数据字段
引言
泛型列表是我最常用的集合数据结构。我经常需要以特定的类型查看泛型列表中的全部或部分数据,例如在控制台、调试窗口或 Excel 工作表窗口中,或者有时将数据发送到 CSV 文件。关于列表数据输出到字符串或 CSV/Excel 文件的方法和代码片段有很多。但是,我找不到一个完全满足我需求的。因此,我根据以下要求编写了自己的方法。
- 易于使用,作为
List<T>
的扩展方法形式 - 可选择包含或排除数据字段(或对象类型的属性)
- 如果输出为单个
string
,则提供对象列表项的格式化string
- 能够将
List<T>
中的数据保存到 CSV 文件 - 直接打开一个 Microsoft Excel 工作表窗口,其中包含
List<T>
中的数据
扩展方法语法
输出到 string
(重载的扩展方法)
public string IList.ToString<T>([string include = ""], [string exclude = ""])
输出到 CSV 文件:
public void IList.ToCSV<T>([string path = ""], [string include = ""], [string exclude = ""])
输出到 Excel(不使用 Interop
库)
public void IList.ToExcelNoInterop<T>
([string path = ""], [string include = ""], [string exclude = ""])
输出到 Excel(不创建文件,但需要使用 Interop
库)
public void IList.ToExcel<T>([string include = ""], [string exclude = ""])
方法的所有参数都是可选的。建议使用命名参数形式,即“参数名: 参数值
”,而无需考虑参数的顺序。例如,您可以像这样调用方法,从具有 Product
类型的列表(DataSource.Products
)中创建 CSV 文件
DataSource.Products.ToCSV<Product>
(exclude: "ProductId,OutOfStock", path:@"D:\TestProducts.csv");
扩展方法详情
下载源代码中的 GenericListOutput.cs 文件包含如下代码。所有必要的注释都已附加,或代码行本身即可解释。您可以将此类添加到任何使用 .NET Framework 4.0/Visual C# 2010 及更高版本的 C# 项目中。然后 List<T>
扩展方法应该就可以使用了。如果缺少任何必需的引用,您可能需要为项目添加一些程序集引用。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Excel = Microsoft.Office.Interop.Excel;
public static class GenericListOutput
{
public static string ToString<T>
(this IList<T> list, string include = "", string exclude = "")
{
//Variables for build string
string propStr = string.Empty;
StringBuilder sb = new StringBuilder();
//Get property collection and set selected property list
PropertyInfo[] props = typeof(T).GetProperties();
List<PropertyInfo> propList = GetSelectedProperties(props, include, exclude);
//Add list name and total count
string typeName = GetSimpleTypeName(list);
sb.AppendLine(string.Format("{0} List - Total Count: {1}",
typeName, list.Count.ToString()));
//Iterate through data list collection
foreach (var item in list)
{
sb.AppendLine("");
//Iterate through property collection
foreach (var prop in propList)
{
//Construct property name and value string
propStr = prop.Name + ": " + prop.GetValue(item, null);
sb.AppendLine(propStr);
}
}
return sb.ToString();
}
public static void ToCSV<T>(this IList<T> list, string path = "",
string include = "", string exclude = "")
{
CreateCsvFile(list, path, include, exclude);
}
public static void ToExcelNoInterop<T>(this IList<T> list,
string path = "", string include = "", string exclude = "")
{
if (path == "")
path = Path.GetTempPath() + @"ListDataOutput.csv";
var rtnPath = CreateCsvFile(list, path, include, exclude);
//Open Excel from the file
Process proc = new Process();
//Quotes wrapped path for any space in folder/file names
proc.StartInfo = new ProcessStartInfo("excel.exe", "\"" + rtnPath + "\"");
proc.Start();
}
private static string CreateCsvFile<T>(IList<T> list, string path,
string include, string exclude)
{
//Variables for build CSV string
StringBuilder sb = new StringBuilder();
List<string> propNames;
List<string> propValues;
bool isNameDone = false;
//Get property collection and set selected property list
PropertyInfo[] props = typeof(T).GetProperties();
List<PropertyInfo> propList = GetSelectedProperties(props, include, exclude);
//Add list name and total count
string typeName = GetSimpleTypeName(list);
sb.AppendLine(string.Format("{0} List - Total Count: {1}",
typeName, list.Count.ToString()));
//Iterate through data list collection
foreach (var item in list)
{
sb.AppendLine("");
propNames = new List<string>();
propValues = new List<string>();
//Iterate through property collection
foreach (var prop in propList)
{
//Construct property name string if not done in sb
if (!isNameDone) propNames.Add(prop.Name);
//Construct property value string with double quotes
//for issue of any comma in string type data
var val = prop.PropertyType == typeof(string) ? "\"{0}\"" : "{0}";
propValues.Add(string.Format(val, prop.GetValue(item, null)));
}
//Add line for Names
string line = string.Empty;
if (!isNameDone)
{
line = string.Join(",", propNames);
sb.AppendLine(line);
isNameDone = true;
}
//Add line for the values
line = string.Join(",", propValues);
sb.Append(line);
}
if (!string.IsNullOrEmpty(sb.ToString()) && path != "")
{
File.WriteAllText(path, sb.ToString());
}
return path;
}
public static void ToExcel<T>
(this IList<T> list, string include = "", string exclude = "")
{
//Get property collection and set selected property list
PropertyInfo[] props = typeof(T).GetProperties();
List<PropertyInfo> propList = GetSelectedProperties(props, include, exclude);
//Get simple type name
string typeName = GetSimpleTypeName(list);
//Convert list to array for selected properties
object[,] listArray = new object[list.Count + 1, propList.Count];
//Add property name to array as the first row
int colIdx = 0;
foreach (var prop in propList)
{
listArray[0, colIdx] = prop.Name;
colIdx++;
}
//Iterate through data list collection for rows
int rowIdx = 1;
foreach (var item in list)
{
colIdx = 0;
//Iterate through property collection for columns
foreach (var prop in propList)
{
//Do property value
listArray[rowIdx, colIdx] = prop.GetValue(item, null);
colIdx++;
}
rowIdx++;
}
//Processing for Excel
object oOpt = System.Reflection.Missing.Value;
Excel.Application oXL = new Excel.Application();
Excel.Workbooks oWBs = oXL.Workbooks;
Excel.Workbook oWB = oWBs.Add(Excel.XlWBATemplate.xlWBATWorksheet);
Excel.Worksheet oSheet = (Excel.Worksheet)oWB.ActiveSheet;
oSheet.Name = typeName;
Excel.Range oRng =
oSheet.get_Range("A1", oOpt).get_Resize(list.Count+1, propList.Count);
oRng.set_Value(oOpt, listArray);
//Open Excel
oXL.Visible = true;
}
private static List<PropertyInfo> GetSelectedProperties
(PropertyInfo[] props, string include, string exclude)
{
List<PropertyInfo> propList = new List<PropertyInfo>();
if (include != "") //Do include first
{
var includeProps = include.ToLower().Split(',').ToList();
foreach (var item in props)
{
var propName = includeProps.Where
(a => a == item.Name.ToLower()).FirstOrDefault();
if (!string.IsNullOrEmpty(propName))
propList.Add(item);
}
}
else if (exclude != "") //Then do exclude
{
var excludeProps = exclude.ToLower().Split(',');
foreach (var item in props)
{
var propName = excludeProps.Where
(a => a == item.Name.ToLower()).FirstOrDefault();
if (string.IsNullOrEmpty(propName))
propList.Add(item);
}
}
else //Default
{
propList.AddRange(props.ToList());
}
return propList;
}
private static string GetSimpleTypeName<T>(IList<T> list)
{
string typeName = list.GetType().ToString();
int pos = typeName.IndexOf("[") + 1;
typeName = typeName.Substring(pos, typeName.LastIndexOf("]") - pos);
typeName = typeName.Substring(typeName.LastIndexOf(".") + 1);
return typeName;
}
}
数据输出的属性选择
提供选择输出数据属性(也称为字段)的选项,这是查看泛型列表中所需数据的强大功能。我们通常使用 LINQ 来选择 List
集合中的对象项(相当于表中的行),然后使用反射来选择属性(相当于表中的列)。有时,我们希望排除某些属性,特别是对于特定类型的项目中自动添加的非用户数据属性。其他时候,我们可能只需要部分属性,而不是全部。通过这些扩展方法,我们可以将属性名称指定为逗号分隔的字符串,作为可选的 include
或 exclude
参数,以满足需求。这些参数不区分大小写,以便于使用,尽管对象中的属性名是区分大小写的。
另外请注意,在代码处理逻辑中,如果 include
参数的值为一个非空 string
,它将始终具有优先权。
如前一节所述,建议使用命名参数形式调用这些扩展方法。命名参数使您无需记忆或按顺序排列调用方法的参数列表中的参数,特别是对于可选参数。您可以在下面的部分中看到示例。
不想使用 Interop?
IList<T>.ToExcel()
方法会调用 Microsoft.Office.Interop.Excel
库,将 List
数据转换为 Excel 工作表格式,然后打开 Excel 窗口,而不先创建任何文件。有些开发人员可能不喜欢向项目中添加 Interop 引用。那么替代方案就是使用 IList<T>.ToExcelNoInterop()
方法,通过已创建的 CSV 文件打开 Excel。在使用此选项处理 CSV 文件创建时,有两种选择。
- 调用方法而不关心 CSV 文件。该方法默认路径为当前用户的临时目录,文件名通常为 ListDataOutput.csv。您将不会对 CSV 文件采取任何操作,就好像它不存在一样。尽管此文件不会自动删除,但在临时目录中只会保留一个同名文件。每次调用相同的方法时,都会覆盖此临时文件。当然,您也可以从打开的 Excel 窗口中手动将 CSV 文件或工作表文件另存为任何您想要的名称或可用类型,保存到任何其他位置。
- 调用方法并指定文件路径。在这种情况下,CSV 文件将保存到指定的路径,并且 Excel 窗口将自动打开,其中包含来自 CSV 文件的数据。
双引号包围
与 CSV 相关的两个方法中的代码通过使用双引号包围来处理一些小技巧,以防止这些方法或数据输出出现异常,这些异常可能会中断方法的执行或数据结构。
- 当文件路径在文件夹或文件名中包含空格时,.NET Framework
System.IO.Path
类中的方法(如GetTempPath()
)可以很好地处理它们。如果我们为ProcessStartInfo
构造函数注入这样的文件路径string
,情况就不那么乐观了。启动 Excel 应用程序进程时,我们需要将路径string
用双引号包围,如下行代码所示//Quotes wrapped path for any space in folder/file names proc.StartInfo = new ProcessStartInfo("excel.exe", "\"" + path + "\"");
- CSV 文件中的任何逗号如果不是用双引号包围,都会破坏 CSV 文件的完整结构。但我们可能只需要为
string
类型的值使用双引号。由于我们已经通过反射获取了属性,因此可以轻松地添加代码来解决这个问题。//Construct value string with double quotes for issue of any comma in string type data var val = prop.PropertyType == typeof(string) ? "\"{0}\"" : "{0}"; propValues.Add(string.Format(val, prop.GetValue(item, null)));
调用扩展方法的示例
下载的源代码包含 DataSource
类,其中 Products
属性指向 List<Product>
集合,该集合填充了 Product
数据。在构建示例应用程序后,您可以运行它来查看所有四种输出结果,或者逐个测试这些方法。
- 输出到
string
以在 Console 窗口中显示//Get string result for display on Console or Debugging window string result = DataSource.Products.ToString<Product> (include:"ProductName,CategoryId,UnitPrice"); Console.Write(result);
下面的屏幕截图显示了
List
数据的格式化string
。 - 输出到 CSV 文件
//Export to CSV file DataSource.Products.ToCSV<Product> (exclude: "ProductId,OutOfStock", path: @"D:\TestProducts.csv");
这会将 CSV 文件保存在指定的路径中。
- 从不引用
Interop
创建的 CSV 文件打开 Excel 工作表窗口//Open Excel from file created in user temp directory without Interop DataSource.Products.ToExcelNoInterop<Product>(exclude: "ProductId,OutOfStock");
这默认会在用户临时目录中保存 ListDataOutput.csv 文件,然后自动打开 Excel 工作表窗口。
- 引用
Interop
库打开 Excel 工作表窗口//Directly open Excel DataSource.Products.ToExcel<Product>(exclude: "ProductId");
在这种情况下,不会创建文件。窗口标题中显示“Sheet1”。退出 Excel 窗口时,会提示您保存文件。
摘要
这里描述的 C# 泛型列表数据输出扩展方法具有许多功能且易于使用。我希望这些方法能对需要它们的开发人员有所帮助,为他们的日常工作带来便利。
历史
- 2013 年 11 月 19 日:初始版本