如何将LINQ查询到的结果由匿名类型var转换成DataTable对象
.NET中的LINQ对于操作集合对象提供了很多的便利,使得我们可以在C#中使用类似于SQL语句的方式对集合中的对象进行查找、分组、统计等。使用LINQ写出来的代码简单明了,省去了我们原本需要使用大量for循环或者foreach循环才能实现的效果。众所周知,通过LINQ查询所返回的结果一般来说是一个以var所标识的匿名类型,该类型继承自IEnumerable接口,我们可以直接将它绑定到任何一个数据绑定控件,如DropDownList,ListBox,DataGridView等。但这里有一个问题,对于DataGridView(WinForm版)来说,如果使用LINQ返回的匿名对象进行数据绑定的话,会失去DataGridView中单击列标题进行数据排序的功能,这是因为DataGridView不能从一个匿名对象中获取到进行数据排序的具体规则。要解决这个问题,你可以给这个匿名对象编写具体的排序算法,不过最简单的做法还是将这个匿名对象转换成我们所熟悉的集合对象,如DataTable,然后再绑定到DataGridView中。
查找msdn,你可以找到将LINQ to DataSet的结果转换成DataTable的方法。下面的代码片段来源于msdn上的介绍,http://msdn.microsoft.com/zh-cn/library/bb396189(v=vs.90).aspx
// to the System.Windows.Forms.BindingSource object.
dataGridView.DataSource = bindingSource;
// Fill the DataSet.
DataSet ds = new DataSet();
ds.Locale = CultureInfo.InvariantCulture;
FillDataSet(ds);
DataTable orders = ds.Tables["SalesOrderHeader"];
// Query the SalesOrderHeader table for orders placed
// after August 8, 2001.
IEnumerable<DataRow> query =
from order in orders.AsEnumerable()
where order.Field<DateTime>("OrderDate") > new DateTime(2001, 8, 1)
select order;
// Create a table from the query.
DataTable boundTable = query.CopyToDataTable<DataRow>();
// Bind the table to a System.Windows.Forms.BindingSource object,
// which acts as a proxy for a System.Windows.Forms.DataGridView object.
bindingSource.DataSource = boundTable;
不过这个方法不是我们所希望的!原因是其中的泛型类型必须是DataRow而不能是自定义类型。怎么办呢?我们可不可以将这个方法修改一下让它能支持任意类型?
还记得从.NET 3.0开始就提供的一个功能吗?C#扩展方法。它允许我们向现有类型中“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。看看msdn上的介绍,先来熟悉一下什么是C#扩展方法吧!http://msdn.microsoft.com/zh-cn/library/bb383977.aspx
C#扩展方法是给现有类型“添加”一个方法,现有类型可以是基本数据类型(如int,string等),也可以是自定义类型。定义规则是扩展方法必须定义在一个任意命名的静态类中,该方法必须是静态方法,可以任意命名,方法的参数列表必须以this关键字开始,第二个即为要扩展的数据类型,第三个是一个变量名,同时参数列表中允许定义多个其它参数以实现方法的重载。来看一个例子。
{
public static class MyExtensions
{
public static int WordCount(this String str)
{
return str.Split(new char[] { ' ', '.', '?' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
静态类MyExtensions被定义在命名空间ExtensionMethods中,静态方法WordCount的参数列表中规定了该方法是对String类型的方法进行了扩展。在实际应用中,你需要在代码中添加对ExtensionMethods命名空间的引用,然后通过String.WordCount()的方式来调用这个扩展方法。是不是很神奇啊?再看一个例子。
{
public static class Test {
public static XElement ToXml(this DirectoryInfo dir)
{
// TO Do Something
}
}
}
上面的代码片段对DirectoryInfo类的方法进行了扩展,将上述代码补充完整,便可以直接通过下面的方式调用新扩展的方法。
dir.ToXml();
C#扩展方法允许对自定义的类型进行扩展,同时允许带参数,支持重载。看下面的例子。
{
public class Student
{
public string Description()
{
return "Student.............";
}
public string Description(string name)
{
return "the student’s name is " + name;
}
}
public static class Extensions
{
public static string TestMethod(this Student s)
{
return s.Description();
}
public static string TestMethod(this Student s, string name)
{
return s.Description(name);
}
}
}
于是,自定义的Student类具有了包含一个重载的TestMethod方法,该方法允许接收一个string类型的参数或者没有参数。
好了!回到我们的主题上来。既然C#扩展方法允许我们对类型添加方法,那么我们完全可以对已有的IEnumerable接口扩展一个CopyToDataTable方法,使其可以将LINQ返回的var匿名类型转换成DataTable。来看下具体的实现。using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Reflection;
namespace WindowsFormsApplication2
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
// create sequence
Item[] items = new Item[] { new Book{Id = 1, Price = 13.50, Genre = "Comedy", Author = "Jim Bob"},
new Book{Id = 2, Price = 8.50, Genre = "Drama", Author = "John Fox"},
new Movie{Id = 1, Price = 22.99, Genre = "Comedy", Director = "Phil Funk"},
new Movie{Id = 1, Price = 13.40, Genre = "Action", Director = "Eddie Jones"}};
var query1 = from i in items
where i.Price > 9.99
orderby i.Price
select i;
// load into new DataTable
DataTable table1 = query1.CopyToDataTable();
this.dataGridView1.DataSource = table1;
}
}
public class Item
{
public int Id { get; set; }
public double Price { get; set; }
public string Genre { get; set; }
}
public class Book : Item
{
public string Author { get; set; }
}
public class Movie : Item
{
public string Director { get; set; }
}
public static class DataSetLinqOperators
{
public static DataTable CopyToDataTable<T>(this IEnumerable<T> source)
{
return new ObjectShredder<T>().Shred(source, null, null);
}
public static DataTable CopyToDataTable<T>(this IEnumerable<T> source,
DataTable table, LoadOption? options)
{
return new ObjectShredder<T>().Shred(source, table, options);
}
}
public class ObjectShredder<T>
{
private FieldInfo[] _fi;
private PropertyInfo[] _pi;
private Dictionary<string, int> _ordinalMap;
private Type _type;
public ObjectShredder()
{
_type = typeof(T);
_fi = _type.GetFields();
_pi = _type.GetProperties();
_ordinalMap = new Dictionary<string, int>();
}
public DataTable Shred(IEnumerable<T> source, DataTable table, LoadOption? options)
{
if (typeof(T).IsPrimitive)
{
return ShredPrimitive(source, table, options);
}
if (table == null)
{
table = new DataTable(typeof(T).Name);
}
// now see if need to extend datatable base on the type T + build ordinal map
table = ExtendTable(table, typeof(T));
table.BeginLoadData();
using (IEnumerator<T> e = source.GetEnumerator())
{
while (e.MoveNext())
{
if (options != null)
{
table.LoadDataRow(ShredObject(table, e.Current), (LoadOption)options);
}
else
{
table.LoadDataRow(ShredObject(table, e.Current), true);
}
}
}
table.EndLoadData();
return table;
}
public DataTable ShredPrimitive(IEnumerable<T> source, DataTable table, LoadOption? options)
{
if (table == null)
{
table = new DataTable(typeof(T).Name);
}
if (!table.Columns.Contains("Value"))
{
table.Columns.Add("Value", typeof(T));
}
table.BeginLoadData();
using (IEnumerator<T> e = source.GetEnumerator())
{
Object[] values = new object[table.Columns.Count];
while (e.MoveNext())
{
values[table.Columns["Value"].Ordinal] = e.Current;
if (options != null)
{
table.LoadDataRow(values, (LoadOption)options);
}
else
{
table.LoadDataRow(values, true);
}
}
}
table.EndLoadData();
return table;
}
public DataTable ExtendTable(DataTable table, Type type)
{
// value is type derived from T, may need to extend table.
foreach (FieldInfo f in type.GetFields())
{
if (!_ordinalMap.ContainsKey(f.Name))
{
DataColumn dc = table.Columns.Contains(f.Name) ? table.Columns[f.Name]
: table.Columns.Add(f.Name, f.FieldType);
_ordinalMap.Add(f.Name, dc.Ordinal);
}
}
foreach (PropertyInfo p in type.GetProperties())
{
if (!_ordinalMap.ContainsKey(p.Name))
{
DataColumn dc = table.Columns.Contains(p.Name) ? table.Columns[p.Name]
: table.Columns.Add(p.Name, p.PropertyType);
_ordinalMap.Add(p.Name, dc.Ordinal);
}
}
return table;
}
public object[] ShredObject(DataTable table, T instance)
{
FieldInfo[] fi = _fi;
PropertyInfo[] pi = _pi;
if (instance.GetType() != typeof(T))
{
ExtendTable(table, instance.GetType());
fi = instance.GetType().GetFields();
pi = instance.GetType().GetProperties();
}
Object[] values = new object[table.Columns.Count];
foreach (FieldInfo f in fi)
{
values[_ordinalMap[f.Name]] = f.GetValue(instance);
}
foreach (PropertyInfo p in pi)
{
values[_ordinalMap[p.Name]] = p.GetValue(instance, null);
}
return values;
}
}
}
Item,Book,Movie都是自定义类型,扩展方法对IEnumerable泛型接口添加了能支持任意类型并返回DataTable的方法CopyToDataTable,于是,我们可以直接对LINQ返回的var匿名类型使用CopyDoDataTable方法并将返回值赋值给DataTable对象。然后将DataTable直接绑定给DataGridView从而获取点击列标题进行数据排序的功能。还有稍微复杂一点的应用,给一个代码片段的截图。