用于列表的流畅 CSV/XML 导出器






4.96/5 (78投票s)
一个小的实用工具,提供流畅接口类来导出列表。
引言
我不知道你怎么样,但我在工作中经常处理大量的列表,时不时地,我需要将一些 List<T>
导出到 CSV 文件,我过去只是写了一个小的辅助方法,它使用一些反射来获取 T
对象的所有公共属性,然后获取属性的名称并用作标题,然后遍历 List<T>
并通过反射获取属性值。
这没问题,但这并不是一个通用的解决方案,而且实际上只适用于我存储在列表中的特定类型的 T
,并且所有关于选择要使用哪些列以及列标题应该是什么的代码都隐藏在导出方法的调用者之外。
所以我想了想,觉得一定有更好的方法,所以本文介绍了一个通用的解决方案,我可以通过以下方式实现:
- 可以作为任何
IEnumerable<T>
的扩展方法使用 - 让
using
代码指定要导出哪些列 - 使用流畅 API(因为它们现在非常流行)
- 允许使用表达式树自动获取标题名称
- 允许用户提供自定义标题名称
- 允许用户为导出的数据提供自定义格式字符串
- 允许将数据导出到 CSV 文件或允许将数据导出到 CSV 字符串
- 允许导出器处理 NULL 值
- 允许用户指定自定义分隔符(如果未提供,则使用逗号 ",")
可以用它做什么
如果我们假设有一个默认对象用作 T
(可以是任何有属性的类),它看起来像这样
public class Person
{
public int Age { get; set; }
public String Name { get; set; }
public Person(int age, String name)
{
this.Age = age;
this.Name = name;
}
}
并且我们有一个 List<Person>
对象配置如下
List<Person> people = new List<Person>();
people.Add(new Person(1, "sam"));
people.Add(new Person(2, "john"));
people.Add(new Person(3, "paul"));
那么使用我这个小导出器,我们可以执行以下功能,下面的代码片段实际上是如何用于以下场景的工作示例。
导出到 CSV 字符串
我们可以按如下方式将 List<Person>
的数据导出到 CSV 字符串
使用默认标题,并且对导出的数据不使用任何格式
注意:在此示例中,我提供了自定义分隔符 ":" 来使用。
//Get it as a String result, using default Headers, and default Columns,
//and custom seperator
using (StringWriter writer = new StringWriter())
{
people.GetExporter(":")
.AddExportableColumn((x) => x.Age)
.AddExportableColumn((x) => x.Name)
.AsCSVString(writer);
String resultsWithDefaultHeadersAndDefaultColumns = writer.ToString();
}
这将产生以下输出
Age:Name
1:sam
2:john
3:paul
使用默认标题,并且对导出的数据使用格式
//Get it as a String result, using automatic Headers, but formatted Columns,
//and standard "," seperator
using (StringWriter writer = new StringWriter())
{
people.GetExporter()
.AddExportableColumn((x) => x.Age, customFormatString: "The Person Age Is {0}")
.AddExportableColumn((x) => x.Name, customFormatString: "The Persons Name {0}")
.AsCSVString(writer);
String resultsWithDefaultHeadersAndFormattedColumns = writer.ToString();
}
这将产生以下输出
Age,Name
The Person Is 1,The Person Name Is sam
The Person Is 2,The Person Name Is john
The Person Is 3,The Person Name Is paul
使用自定义标题,并且对导出的数据不使用任何格式
//Get it as a String result, using custom Headers, but default Columns
//and standard "," seperator
using (StringWriter writer = new StringWriter())
{
people.GetExporter()
.AddExportableColumn((x) => x.Age, headerString: "AgeColumn")
.AddExportableColumn((x) => x.Name, headerString: "NameColumn")
.AsCSVString(writer);
String resultsWithCustomHeadersAndDefaultColumns = writer.ToString();
}
这将产生以下输出
AgeColumn,NameColumn
1,sam
2,john
3,paul
导出到 CSV 文件
我们还可以选择使用这个小辅助类导出到 CSV 文件,我们可以这样做
使用默认标题,并且对导出的数据不使用任何格式
//Get it as a CSV file, using default Headers, and default Columns,
//and standard "," seperator
using (StreamWriter writer =
new StreamWriter(@"c:\temp\exportedWithDefaultHeadersAndDefaultColumns.csv"))
{
people.GetExporter()
.AddExportableColumn((x) => x.Age)
.AddExportableColumn((x) => x.Name)
.AsCSVString(writer);
}
使用默认标题和数据格式化,这将产生以下效果
//Get it as a CSV file, using automatic Headers, but formatted Columns,
//and standard "," seperator
using (StreamWriter writer =
new StreamWriter(@"c:\temp\exportedWithDefaultHeadersAndFormattedColumns.csv"))
{
people.GetExporter()
.AddExportableColumn((x) => x.Age, customFormatString: "The Person Age Is {0}")
.AddExportableColumn((x) => x.Name, customFormatString: "The Persons Name {0}")
.AsCSVString(writer);
}
使用自定义标题,并且对导出的数据不使用任何格式
//Get it as a CSV file, using custom Headers, but default Columns
//and standard "," seperator
using (StreamWriter writer =
new StreamWriter(@"c:\temp\erxportedWithCustomHeadersAndDefaultColumns.csv"))
{
people.GetExporter()
.AddExportableColumn((x) => x.Age, headerString: "AgeColumn")
.AddExportableColumn((x) => x.Name, headerString: "NameColumn")
.AsCSVString(writer);
}
导出到 XML 文件
由于一位读者的额外努力,他发布到了本文的论坛,我们还可以导出到 XML 文件。我们可以这样做
using (XmlTextWriter sw = new XmlTextWriter("LastResults.xml",
System.Text.Encoding.UTF8))
{
people.GetExporter()
.AddExportableColumn((x) => x.Age, headerString: "AgeColumn")
.AddExportableColumn((x) => x.Name, headerString: "NameColumn")
.ToXML(sw);
}
注意:我们必须将 XSL 文件放在与二进制文件相同的文件夹中。下载的代码包含了该文件。
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="4.0"
encoding="ISO-8859-1" indent="no" omit-xml-declaration="yes"/>
<xsl:template match="dump">
<head>
<meta NAME="ROBOTS" CONTENT="NOINDEX,NOFOLLOW"/>
<meta HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE"/>
<title>
Dump (<xsl:value-of select="count(/dump/item)"/> items)
</title>
<style media="screen" type="text/css">
body {
background-color: #F1F0EB;
font-family: Verdana, Tahoma, Helvetica, sans-serif;
font-size: .8em;
margin: 10, 10, 10, 10;
}
h1, h2, h3 {
font-family: Tahoma, Arial, sans-serif;
font-weight: bolder;
}
h2 {
font-size: 2em;
color: #A0A0A0;
}
table {
border-width: 1px;
border-spacing: 2px;
border-style: solid;
border-color: gray;
border-collapse: separate;
background-color: #F1F0EB;
}
table th {
border-width: 0px;
font-size: .9em;
line-height: 1.2em;
padding: 1px;
border-style: none;
border-color: gray;
background-color: #F1F0EB;
-moz-border-radius: ;
}
table td {
border-width: 0px;
padding: 1px;
font-size: .9em;
line-height: 1.2em;
border-style: none;
border-color: gray;
background-color: white;
-moz-border-radius: ;
}
</style>
</head>
<html>
<body>
<h2>List dump</h2>
<p>
Date: <xsl:value-of select="/dump/@date"/>
</p>
<table border="1">
<thead>
<tr bgcolor="#9acd32">
<xsl:apply-templates select="item[1]/*" mode="th" />
</tr>
</thead>
<tbody>
<xsl:apply-templates select="item" />
</tbody>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="item">
<tr>
<xsl:apply-templates select="*" mode="td" />
</tr>
</xsl:template>
<xsl:template match="item/*" mode="th">
<th>
<xsl:value-of select="local-name()" />
</th>
</xsl:template>
<xsl:template match="item/*" mode="td">
<td>
<xsl:value-of select="." />
</td>
</xsl:template>
</xsl:stylesheet>
在浏览器中查看时,它将如下所示
它是如何工作的
嗯,正如你们中的一些人可能已经猜到的那样,秘密在于使用表达式树/可选参数和命名参数。但在我们深入研究之前,值得注意的是,通过使用这种技术,导出的控制权牢牢掌握在本文包含的导出助手类的使用者手中,正如我所说的,这很重要,因为它完全是通用的,并且允许用户指定他们想要导出哪些列以及导出的顺序。正如上面的例子所示,用户可以选择使用自定义列标题/格式字符串,或者推断它们(通过解析表达式树)。
一位读者建议将其重构为允许导出器成为 IEnumerable<T>
的扩展方法,所以我添加了它。感谢 David Sehnal。
导出还使用了流畅接口,我在我的一篇旧文章中对此进行了一些讨论,您可以在此处阅读:https://codeproject.org.cn/KB/WPF/fluentAPI.aspx。
总之,不多说,这是本文中提供的导出器代码的完整代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;
using System.IO;
using System.Xml;
namespace FluentListExporterColumns
{
public static class IEnumerableExtensions
{
/// <summary>
/// Exporter extension method for all IEnumerableOfT
/// </summary>
public static FluentExporter<T> GetExporter<T>(
this IEnumerable<T> source, String seperator = ",") where T : class
{
return new FluentExporter<T>(source, seperator);
}
}
/// <summary>
/// Represents custom exportable column with a expression for the property name
/// and a custom format string
/// </summary>
public class ExportableColumn<T>
{
public Expression<Func<T, Object>> Func { get; private set; }
public String HeaderString { get; private set; }
public String CustomFormatString { get; private set; }
public ExportableColumn(
Expression<Func<T, Object>> func,
String headerString = "",
String customFormatString = "")
{
this.Func = func;
this.HeaderString = headerString;
this.CustomFormatString = customFormatString;
}
}
/// <summary>
/// Exporter that uses Expression tree parsing to work out what values to export for
/// columns, and will use additional data as specified in the List of ExportableColumn
/// which defines whethere to use custom headers, or formatted output
/// </summary>
/// <typeparam name="T"></typeparam>
public class FluentExporter<T> where T : class
{
private List<ExportableColumn<T>> columns =
new List<ExportableColumn<T>>();
private Dictionary<Expression<Func<T, Object>>, Func<T, Object>>
compiledFuncLookup =
new Dictionary<Expression<Func<T, Object>>, Func<T, Object>>();
private List<String> headers = new List<String>();
private IEnumerable<T> sourceList;
private String seperator;
private bool doneHeaders;
public FluentExporter(IEnumerable<T> sourceList, String seperator = ",")
{
this.sourceList = sourceList;
this.seperator = seperator;
}
public FluentExporter<T> AddExportableColumn(
Expression<Func<T, Object>> func,
String headerString = "",
String customFormatString = "")
{
columns.Add(new ExportableColumn<T>(
func,headerString,customFormatString));
return this;
}
/// <summary>
/// Export all specified columns as a string,
/// using seperator and column data provided
/// where we may use custom or default headers
/// (depending on whether a custom header string was supplied)
/// where we may use custom fomatted column data or default data
/// (depending on whether a custom format string was supplied)
/// </summary>
public void AsCSVString(TextWriter writer)
{
if (columns.Count == 0)
throw new InvalidOperationException(
"You need to specify at least one column to export value");
int i = 0;
foreach (T item in sourceList)
{
List<String> values = new List<String>();
foreach (ExportableColumn<T> exportableColumn in columns)
{
if (!doneHeaders)
{
if (String.IsNullOrEmpty(exportableColumn.HeaderString))
{
headers.Add(GetPropertyName(exportableColumn.Func));
}
else
{
headers.Add(exportableColumn.HeaderString);
}
Func<T, Object> func = exportableColumn.Func.Compile();
compiledFuncLookup.Add(exportableColumn.Func, func);
if (!String.IsNullOrEmpty(exportableColumn.CustomFormatString))
{
var value = func(item);
values.Add(value != null ?
String.Format(exportableColumn.CustomFormatString,
value.ToString()) : "");
}
else
{
var value = func(item);
values.Add(value != null ? value.ToString() : "");
}
}
else
{
if (!String.IsNullOrEmpty(exportableColumn.CustomFormatString))
{
var value = compiledFuncLookup[exportableColumn.Func](item);
values.Add(value != null ?
String.Format(exportableColumn.CustomFormatString,
value.ToString()) : "");
}
else
{
var value = compiledFuncLookup[exportableColumn.Func](item);
values.Add(value != null ? value.ToString() : "");
}
}
}
if (!doneHeaders)
{
writer.WriteLine(headers.Aggregate(
(start, end) => start + seperator + end));
doneHeaders = true;
}
writer.WriteLine(values.Aggregate(
(start, end) => start + seperator + end));
}
}
// <summary>
/// Export all specified columns as a XML string, using column data provided
/// and use custom headers depending on whether a custom header string was supplied.
/// Use custom formatted column data or default data depending
/// on whether a custom format string was supplied.
/// </summary>
public void ToXML(XmlTextWriter writer)
{
if (columns.Count == 0)
throw new InvalidOperationException(
"You need to specify at least one element to export value");
foreach (T item in sourceList)
{
List<String> values = new List<String>();
foreach (ExportableColumn<T> exportableColumn in columns)
{
if (!doneHeaders)
{
if (String.IsNullOrEmpty(exportableColumn.HeaderString))
{
headers.Add(MakeXMLNameLegal(
GetPropertyName(exportableColumn.Func)));
}
else
{
headers.Add(MakeXMLNameLegal(exportableColumn.HeaderString));
}
Func<T, Object> func = exportableColumn.Func.Compile();
compiledFuncLookup.Add(exportableColumn.Func, func);
if (!String.IsNullOrEmpty(exportableColumn.CustomFormatString))
{
var value = func(item);
values.Add(value != null ?
String.Format(exportableColumn.CustomFormatString,
value.ToString()) : "");
}
else
{
var value = func(item);
values.Add(value != null ? value.ToString() : "");
}
}
else
{
if (!String.IsNullOrEmpty(exportableColumn.CustomFormatString))
{
var value = compiledFuncLookup[exportableColumn.Func](item);
values.Add(value != null ?
String.Format(exportableColumn.CustomFormatString,
value.ToString()) : "");
}
else
{
var value = compiledFuncLookup[exportableColumn.Func](item);
values.Add(value != null ? value.ToString() : "");
}
}
}
if (!doneHeaders)
{
writer.Formatting = Formatting.Indented;
writer.WriteStartDocument(true);
writer.WriteProcessingInstruction("xml-stylesheet",
"type='text/xsl' href='dump.xsl'");
writer.WriteComment("List Exporter dump");
// Write main document node and document properties
writer.WriteStartElement("dump");
writer.WriteAttributeString("date", DateTime.Now.ToString());
doneHeaders = true;
}
writer.WriteStartElement("item");
for (int i = 0; i < values.Count; i++)
{
writer.WriteStartElement(headers[i]);
writer.WriteString(values[i]);
writer.WriteEndElement();
}
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.Flush();
}
/// <summary>
/// Export to file, using the AsCSVString() method to supply the exportable data
/// </summary>
public void WhichIsExportedToFileLocation(StreamWriter fileWriter)
{
AsCSVString(fileWriter);
}
/// <summary>
/// Gets a Name from an expression tree that is assumed to be a
/// MemberExpression
/// </summary>
private static string GetPropertyName<T>(
Expression<Func<T, Object>> propertyExpression)
{
var lambda = propertyExpression as LambdaExpression;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
var unaryExpression = lambda.Body as UnaryExpression;
memberExpression = unaryExpression.Operand as MemberExpression;
}
else
{
memberExpression = lambda.Body as MemberExpression;
}
var propertyInfo = memberExpression.Member as PropertyInfo;
return propertyInfo.Name;
}
private string MakeXMLNameLegal(string aString)
{
StringBuilder newName = new StringBuilder();
if (!char.IsLetter(aString[0]))
newName.Append("_");
// Must start with a letter or underscore.
for (int i = 0; i <= aString.Length - 1; i++)
{
if (char.IsLetter(aString[i]) || char.IsNumber(aString[i]))
{
newName.Append(aString[i]);
}
else
{
newName.Append("_");
}
}
return newName.ToString();
}
}
}
理解这部分最重要的就是我们使用了一些巧妙的 .NET 4 可选属性,所以我们可以假设想要默认标题,除非我们另有说明。另外值得注意的是,我们可以通过编译 Expression
来获取一个 Func
委托,然后调用它来获取属性的值。
另一个重要部分是如何从表达式树获取属性名称,这是通过 GetPropertyName()
方法完成的,如果我们使用默认标题,它就是用于获取标题名称的。
我认为代码本身就足够清晰了,但如果你迷失了,请告诉我,如果需要,我可以尝试进一步充实这篇文章。
就是这样
我知道这是一篇很小的文章,但我认为它实际上是一个相当有用的实用工具,它非常灵活,而且相当容易使用。所以,如果你觉得它可能会帮到你,请投一票。
历史
- 28/03/2011 : 初始 API。
- 29/03/2011 : 重构为使用
IEnumerable
的扩展方法。 - 01/04/2011 : 重构为接受
TextWriter
基于类的存储,以节省多次写入数据。 - 05/04/2011 : 重构以包含一位读者在论坛上发布的
AsXML()
支持。谢谢。 Corridorwarrior。 - 06/04/2011 : 修复了第二个字符串示例输出中的拼写错误。