65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 泛型列表扩展,用于将数据输出到格式化字符串、CSV 文件和 Excel 工作表窗口

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2013年11月19日

CPOL

6分钟阅读

viewsIcon

73744

downloadIcon

1941

使用扩展方法将通用列表中的数据导出到格式化字符串、CSV 文件或 Excel 工作表窗口,并可选择数据字段

引言

泛型列表是我最常用的集合数据结构。我经常需要以特定的类型查看泛型列表中的全部或部分数据,例如在控制台、调试窗口或 Excel 工作表窗口中,或者有时将数据发送到 CSV 文件。关于列表数据输出到字符串或 CSV/Excel 文件的方法和代码片段有很多。但是,我找不到一个完全满足我需求的。因此,我根据以下要求编写了自己的方法。

  1. 易于使用,作为 List<T> 的扩展方法形式
  2. 可选择包含或排除数据字段(或对象类型的属性)
  3. 如果输出为单个 string,则提供对象列表项的格式化 string
  4. 能够将 List<T> 中的数据保存到 CSV 文件
  5. 直接打开一个 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 集合中的对象项(相当于表中的行),然后使用反射来选择属性(相当于表中的列)。有时,我们希望排除某些属性,特别是对于特定类型的项目中自动添加的非用户数据属性。其他时候,我们可能只需要部分属性,而不是全部。通过这些扩展方法,我们可以将属性名称指定为逗号分隔的字符串,作为可选的 includeexclude 参数,以满足需求。这些参数不区分大小写,以便于使用,尽管对象中的属性名是区分大小写的。

另外请注意,在代码处理逻辑中,如果 include 参数的值为一个非空 string,它将始终具有优先权。

如前一节所述,建议使用命名参数形式调用这些扩展方法。命名参数使您无需记忆或按顺序排列调用方法的参数列表中的参数,特别是对于可选参数。您可以在下面的部分中看到示例。

不想使用 Interop?

IList<T>.ToExcel() 方法会调用 Microsoft.Office.Interop.Excel 库,将 List 数据转换为 Excel 工作表格式,然后打开 Excel 窗口,而不先创建任何文件。有些开发人员可能不喜欢向项目中添加 Interop 引用。那么替代方案就是使用 IList<T>.ToExcelNoInterop() 方法,通过已创建的 CSV 文件打开 Excel。在使用此选项处理 CSV 文件创建时,有两种选择。

  1. 调用方法而不关心 CSV 文件。该方法默认路径为当前用户的临时目录,文件名通常为 ListDataOutput.csv。您将不会对 CSV 文件采取任何操作,就好像它不存在一样。尽管此文件不会自动删除,但在临时目录中只会保留一个同名文件。每次调用相同的方法时,都会覆盖此临时文件。当然,您也可以从打开的 Excel 窗口中手动将 CSV 文件或工作表文件另存为任何您想要的名称或可用类型,保存到任何其他位置。
     
  2. 调用方法并指定文件路径。在这种情况下,CSV 文件将保存到指定的路径,并且 Excel 窗口将自动打开,其中包含来自 CSV 文件的数据。

双引号包围

与 CSV 相关的两个方法中的代码通过使用双引号包围来处理一些小技巧,以防止这些方法或数据输出出现异常,这些异常可能会中断方法的执行或数据结构。

  1. 当文件路径在文件夹或文件名中包含空格时,.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 + "\"");
  2. 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 数据。在构建示例应用程序后,您可以运行它来查看所有四种输出结果,或者逐个测试这些方法。

  1. 输出到 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

  2. 输出到 CSV 文件
    //Export to CSV file
    DataSource.Products.ToCSV<Product>
        (exclude: "ProductId,OutOfStock", path: @"D:\TestProducts.csv");

    这会将 CSV 文件保存在指定的路径中。 

  3. 从不引用 Interop 创建的 CSV 文件打开 Excel 工作表窗口
    //Open Excel from file created in user temp directory without Interop
    DataSource.Products.ToExcelNoInterop<Product>(exclude: "ProductId,OutOfStock");

    这默认会在用户临时目录中保存 ListDataOutput.csv 文件,然后自动打开 Excel 工作表窗口。 

  4. 引用 Interop 库打开 Excel 工作表窗口
    //Directly open Excel
    DataSource.Products.ToExcel<Product>(exclude: "ProductId");

    在这种情况下,不会创建文件。窗口标题中显示“Sheet1”。退出 Excel 窗口时,会提示您保存文件。

摘要 

这里描述的 C# 泛型列表数据输出扩展方法具有许多功能且易于使用。我希望这些方法能对需要它们的开发人员有所帮助,为他们的日常工作带来便利。

历史

  • 2013 年 11 月 19 日:初始版本
© . All rights reserved.