使用DataGridView如果图简单省事儿,直接将其DataSource属性绑定到DataTable的DefaultView是最方便快捷的方法。绑定后,它默认就支持排序,筛选等高级功能,另外当DataTable值发生变化是,DataGridView也会跟着刷新。这一切都要归功于DataTable其内部实现的强大功能。另外,DataGridView绑定BindingList实体也是比较推荐的做法,它的有点在于能够进行更多的精细控制,并且在有些情况下效率会更高,缺点就是,内置的BindingList对象并没有实现诸如排序,筛选等功能,这些需要自己实现。本文就结合实例介绍这两种做法。
使用Bogus生成模拟数据
为了演示后续的绑定,使用Bogus生成一些模拟的订单数据。
public class OrderGenerator
{
private static List<Order> allOrders = new List<Order>(10);
private List<string> orderStatus = new List<string>() { "待付款", "已付款", "已发货", "已送达", "已签收" };
private static Faker<Order> orderGenerator;
public static EventHandler<Order> NewOrder;
static OrderGenerator()
{
orderGenerator = new Faker<Order>("zh_CN")
.RuleFor(x => x.ID, x => x.Random.Number(0, 1000000).ToString())
.RuleFor(x => x.OrderID, x => x.Random.Guid().ToString())
.RuleFor(x => x.Name, x => x.Person.LastName + x.Person.FirstName)
.RuleFor(x => x.Address, x => x.Person.Address.State + x.Person.Address.City + x.Person.Address.Street)
.RuleFor(x => x.Phone, x => x.Person.Phone)
.RuleFor(x => x.Count, x => x.Random.Number(1, 20))
.RuleFor(x => x.Price, x => x.Random.Double(10, 2000))
.RuleFor(x => x.Status, x => x.PickRandom<OrderStatus>())
.RuleFor(x => x.CreateTime, x => x.Date.Between(DateTime.Today.AddDays(-1000), DateTime.Today).AddDays(1))
.RuleFor(x => x.PayTime, (x, r) => x.Date.SoonOffset(1, r.CreateTime).DateTime)
.RuleFor(x => x.Remark, x => x.Random.String2(10));
}
public static void Generate(int count)
{
Task.Factory.StartNew(() =>
{
allOrders = orderGenerator.Generate(count).ToList();
foreach (var order in allOrders)
{
if (NewOrder != null)
{
NewOrder.Invoke(null, order);
Thread.Sleep(100);
}
}
});
}
public List<Order> GetOrders()
{
return allOrders;
}
}
public class Order
{
public string ID { get; set; }
public string OrderID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string Phone { get; set; }
public int Count { get; set; }
public double Price { get; set; }
public OrderStatus Status { get; set; }
public DateTime CreateTime { get; set; }
public DateTime PayTime { get; set; }
public string Remark { get; set; }
}
public enum OrderStatus
{
/// <summary>
/// 待付款
/// </summary>
ToPay = 1,
/// <summary>
/// 已付款
/// </summary>
Payed,
/// <summary>
/// 已发货
/// </summary>
Delivery,
/// <summary>
/// 已送达
/// </summary>
Arrived,
/// <summary>
/// 已签收
/// </summary>
Finish
}
当用户调用Generate方法,传入需要生成的记录条数n后,就会通过bogus生成n条模拟数据,然后触发n次回调返回生成的模拟数据。当然也可以将n条数据放在一个List里面一次性触发事件返回。
DataGridView绑定DataTable
DataTable是一个比较复杂的数据类型,它的初始设计,可能是用来在内存中表示一个数据库。配合Visual Studio强大的辅助功能,使得连上SQLServer之后,点点鼠标拖拖控件就能完成与数据库的交互。DataSet就像一个内存数据库,它可以包含多个DataTable,一个DataTable就是一个表,每个DataTable里的DataRow就是一行。DataTable可以指定主键PrimaryKey,甚至还可以指定多个DataTable之间的约束关系。这些功能使得可以很方便的使用ADO.NET与数据库进行交互。
了解了DataTable以及DataSet是用来作为内存数据库存储数据就能相通为什么DataTable中的每一行是以红黑树的方式形式存储的,这非常类似于数据库中主键的B或B+树。
为了演示DataGridView绑定DataTable,这里新建一个Form窗体,然后往窗体上拖一个DataGridView控件,并设置一下控件的一些属性:
AllowUserToAddRows=False
AllowUserToDeleteRows=False
AllowUserToOrderColumns=True
AllowUserToResizeRows=False
Anchor=Top,Bottom,Left,Right
AutoSizeColumnsMode=Fill
EditMode=EditProgrammatically
RowHeadersVisible=False
这里的DataGridView表要显示前一节生成的那些模拟数据,所以DataGrid通常可以设置成只读的一些属性,这些属性不影响DataGridView的绑定。接下来后续的绑定代码以及逻辑。
首先定义一个DataTable,包括设置每一行以及对应的类型,然后还可以设置PrimaryKey主键,通过主键可以快速的在DataTable中寻找需要的数据,这里将创建DataTable放到一个方法里,方法中的ordreTable是一个全局的私有字段。
private void CreateDataTable()
{
orderTable = new DataTable("Order");
DataColumn col = orderTable.Columns.Add("ID", typeof(string));
DataColumn col2 = orderTable.Columns.Add("单号", typeof(string));
orderTable.Columns.Add("姓名", typeof(string));
orderTable.Columns.Add("地址", typeof(string));
orderTable.Columns.Add("电话", typeof(string));
orderTable.Columns.Add("数量", typeof(int));
orderTable.Columns.Add("总价", typeof(double));
orderTable.Columns.Add("状态", typeof(string));
orderTable.Columns.Add("下单时间", typeof(DateTime));
orderTable.Columns.Add("付款时间", typeof(DateTime));
orderTable.Columns.Add("备注", typeof(string));
orderTable.PrimaryKey = new DataColumn[] { col, col2 };
}
在FormLoad的事件中,调用上述方法、注册模拟订单数据生成的事件,以及最后设置DataGridView的DataSource为orderTable的DefaultView,以及设置表格中时间显示的格式。
private void Form1_Load(object sender, EventArgs e)
{
CreateDataTable();
OrderGenerator.NewOrder += NewOrder;
orderGridView.DataSource = orderTable.DefaultView;
orderGridView.Columns["ID"].Visible = false;
orderGridView.Columns["下单时间"].DefaultCellStyle.Format = "yyyy-MM-dd HH:mm:ss";
orderGridView.Columns["付款时间"].DefaultCellStyle.Format = "yyyy-MM-dd HH:mm:ss";
}
至此主体逻辑已经写完,接下来在模拟订单的回调事件中,处理当新的订单数据过来时的逻辑。因为已经将DataGridView绑定到了DataTable上来了,所以只要对DataTable进行修改,自然就会反映到DataGridView上。
private void NewOrder(object sender, Order order)
{
if (this.InvokeRequired)
{
this.BeginInvoke((MethodInvoker)delegate
{
AddOrUpdateOrderTable(order);
});
}
else
{
AddOrUpdateOrderTable(order);
}
}
private void AddOrUpdateOrderTable(Order order)
{
object[] key = new object[] { order.ID, order.OrderID };
DataRow row = orderTable.Rows.Find(key);
if (row != null)
{
row["姓名"] = order.Name;
row["地址"] = order.Address;
row["电话"] = order.Phone;
row["数量"] = order.Count;
row["总价"] = order.Price;
row["状态"] = order.Status.ToString();
row["下单时间"] = order.CreateTime;
row["付款时间"] = order.PayTime;
row["备注"] = order.Remark;
}
else
{
orderTable.Rows.Add(order.ID, order.OrderID, order.Name, order.Address, order.Phone, order.Count,
order.Price, order.Status.ToString(), order.CreateTime, order.PayTime, order.Remark);
}
}
上面展示了回调事件的处理逻辑,有两个地方需要注意:
- 因为回调事件是在另外一个通过Task.Factory.StartNew的线程中执行的,不是UI线程,不能直接更新UI界面,需要通过BeginInvoke的方式将操作Post到UI线程中。
- 因为设置了DataTable的PrimaryKey为两个字段,所以当新订单数据过来时,需要根据这两个字段生成一个key,然后调用DataTable的Find方法来用key查找。如果找到数据,则更新,否则就添加。
界面上添加一个按钮,点击后请求模拟数据:
private void btnStartSimulate_Click(object sender, EventArgs e)
{
OrderGenerator.Generate(10);
}
至此所有的逻辑操作都完成了,这个版本的DataGridView绑定DataTable默认就可以实现诸如排序、筛选等功能。
▲ 显示模拟数据
▲以“下单时间”升序排列
关于筛选,如前文所述,只需要给DefaultView的DataRow的Filter字段进行设置即可,比如要筛选出“下单数量大于10且当前状态为已结束的”所有订单,只需要执行如下语句。
orderTable.DefaultView.RowFilter = "([数量]>'10') and ([状态]='Finish')";
即可筛选出满足条件的记录。要清除筛选条件,只需要将RowFilter设置为空字符串即可。
▲根据筛选条件,显示记录
至于这个拼接字符串的界面,可以参考上一篇文章里的介绍。可以遍历表格中的所有列头,作为字段,然后动态生成控件,最后根据用户的选择来拼接以上的字符串即可。
至此,DataGridView绑定DataTable所实现的排序和筛选功能都完成了,其核心的实现都在DataTable的DefaultView所实现的接口中,只要实现了那些接口,DataGridView就能够调用相关的接口方法来实现筛选和排序。非常简答省事。
DataTable的问题
直接绑定DataTable虽然省事儿,但是在有些情况下我们往往希望能够有效率的同时也能够对对象的展示方式有足够的“掌控”。前面说过,DataTable在设计之初可能就是为了支持内存数据表格的存储以及用来和数据库进行交互的,所以DataTable的设计在有些方面可能不得不支持数据库表格里的那些操作。比如索引,比如约束条件,再比如它的内部数据行的存储方式,在有些时候我们可能会觉得它的实现过于“重”。另外当DataTable某个字段变动时,我们无法控制其是否刷新。
下面举个例子,在DataTable中,只要对某个单元格进行赋值操作,就会触发PropertyChanged事件,它不回去判断给这个单元格赋值的值与之前的值是否相等。
DataTable dt = new DataTable();
dt.Columns.Add("ID", typeof(int));
dt.Columns.Add("Name", typeof(string));
DataRow dr = dt.NewRow();
dr["ID"] = 1;
dr["Name"] = "Test";
dt.Rows.Add(dr);
DataRowView drView = dt.DefaultView[0];
drView.PropertyChanged += (sender, args) =>
{
Console.WriteLine($"property {args.PropertyName} changed");
};
dt.Rows[0][1] = "Test1";
dt.Rows[0][0] = 1;
上面的例子中,首先船舰了一个表格,并且在表格中添加了一行记录,第一列的单元格值是“1”,第二列单元格的值是“Test1”,紧接着我们注册表格行试图的PropertyChanged事件。
接下来,对第一个单元格和第二个单元格进行赋值,这里的值和单元格原有的值是一样的。但是它依然会触发两次的PropertyChanged事件。
property Name changed
property ID changed
要去除这两次事件触发,在给单元格赋值前,我们需要判断单元格原有的值,是否与将要给他赋值的值相等,如果相等则忽略。但因为DataTable是一个“弱”数据类型,它存储的是Object类型,要获取单元格里面存储的值,可能就要进行类型转换,可能就要涉及到装箱拆箱操作。当然也可以继承DataTable创建自己的强类型Table,但底层的DataTable的存储结构限制了只能以Object对象的形式存储。所以拆箱装箱在某些情况下无法避免。
以我工作中遇到的一个例子来说明。我们有个程序需要显示股票行情的买卖前十档的价格和数量数据。最开始的做法就是新建了两个DataTable,然后绑定到了两个DataGridView上。当有新的行情来的时候,就去更新一下DataTable,也没有考虑新来的行情数据相对于之前的数据是否有变更。所以只要是重新赋值,都会触发大概40次的PropertyChanged事件,一个事件的触发可能会导致DataGridView重新绘制一次(“可能”是因为我这里没有看到这部分的源代码,不敢妄下结论)。然而实际上,因为行情数据是一个时间序列数据,大概率下一次的行情数据在上一帧的数据上只会有个别字段值的变动,如果只刷新那些变动了的单元格的值,就能减少大量的事件触发,从而提高效率。然而要做到这一点,就必须在给单元格赋值之前,先获取单元格里的值比较,然后再进行判断,这就避免不了拆箱操作。当然也可以存储前一帧的实体数据对象,然后将新一帧的数据与前一帧的数据逐字段的对比,然后根据字段的位置找到DataTable中对应的单元格,然后进行赋值刷新。这种方式显然不直接和繁琐。
更直接的方式就是直接将DataGridView和BindingList对象进行绑定,这也是.NET Framework 2.0的重大改进。但BindingList里面缺省并没有实现排序和筛选的功能,所以需要手动去实现这些功能,而要手动实现一个BindingList达到DataTable那样的功能非常困难。下面就详细介绍DataGridView和BindingList进行绑定的实现。
DataGridView绑定BindingList
在MSDN上有三篇关于DataGridView自定义绑定的文章,分别是Custom Data Binding, Part 1、Part 2、Part3,这三篇文章分别介绍了DataGridView绑定BindingList的基本方法、实现排序和筛选的功能。
BindingList<T>泛型类的实现接口如下:
public class BindingList<T> : Collection<T>, IBindingList, ICancelAddNew, IRaiseItemChangedEvents
它主要实现了IBindingList接口。IBindingList接口是数据源对象支持编辑功能的最少的需要实现的接口。如果数据源不需要支持编辑功能,比如不需要支持编辑的ListBox控件,则只需要实现IList接口即可。当数据源需要绑定到提供完全编辑支持的控件,比如DataGridView,则要提供诸如排序、搜索、索引、变更提醒等功能,则需要实现IBindingList这个接口,它是IList接口的扩展。
public interface IBindingList : IList
{
bool AllowNew { get; }
object AddNew();
bool AllowEdit { get; }
bool AllowRemove { get; }
bool SupportsChangeNotification { get; }
bool SupportsSearching { get; }
bool SupportsSorting { get; }
bool IsSorted { get; }
PropertyDescriptor SortProperty { get; }
ListSortDirection SortDirection { get; }
event ListChangedEventHandler ListChanged;
void AddIndex(PropertyDescriptor property);
void ApplySort(PropertyDescriptor property, ListSortDirection direction);
int Find(PropertyDescriptor property, object key);
void RemoveIndex(PropertyDescriptor property);
void RemoveSort();
}
要将实体的集合对象绑定到DataGridView,则只需要实例化一个BindingList<T>对象即可。还是以Order这个为例。
private BindingList<Order> orders = new BindingList<Order>();
然后将DataGridView的DataSource设置为上述的BindingList即可。
private void Form2_Load(object sender, EventArgs e)
{
OrderGenerator.NewOrder += NewOrder;
orderGridView.DataSource = orders;
}
然后当新的订单信息事件触发时,直接往orders这个BindingList里面添加或者修改现有的数据。
private void NewOrder(object sender, Order order)
{
if (this.InvokeRequired)
{
this.BeginInvoke((MethodInvoker)delegate
{
AddOrUpdateOrderTable(order);
});
}
else
{
AddOrUpdateOrderTable(order);
}
}
private void AddOrUpdateOrderTable(Order order)
{
object[] key = new object[] { order.ID, order.OrderID };
Order o = orders.FirstOrDefault(x => x.ID == order.ID && x.OrderID == order.OrderID);
if (o != null)
{
o.Name = order.Name;
o.Address = order.Address;
o.Phone = order.Phone;
o.Count = order.Count;
o.Price = order.Price;
o.Status = order.Status;
o.CreateTime = order.CreateTime;
o.PayTime = order.PayTime;
o.Remark = order.Remark;
}
else
{
Order ord = Order.Copy(order);
orders.Add(ord);
}
}
对于DataTable中字段发生变更时是否触发刷新事件的问题,在BindingList中有更好的控制方法,那就是让实体实现INotifyPropertyChanged接口:
public class Order : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] String propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private string orderID;
public string OrderID
{
get { return orderID; }
set
{
if (orderID != value)
{
orderID = value;
OnPropertyChanged();
}
}
}
//剩下属性类似,此处略
}
上面,我们可以在属性的Set方法中对字段的赋值进行控制,只有当新值与就有的值不一致时,才触发PropertyChanged事件,这正是在DataTable中的单元格无法做到。
在BindingList的代码中,他会查找泛型类型T是否实现了INotifyPropertyChanged接口,如果实现了,就进行接管,BindingList<T>中对实体T的INotifyPropertyChanged判断如下:
private void Initialize()
{
// Set the default value of AllowNew based on whether type T has a default constructor
this.allowNew = ItemTypeHasDefaultConstructor;
// Check for INotifyPropertyChanged
if (typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(T)))
{
// Supports INotifyPropertyChanged
this.raiseItemChangedEvents = true;
// Loop thru the items already in the collection and hook their change notification.
foreach (T item in this.Items)
{
HookPropertyChanged(item);
}
}
}
private void HookPropertyChanged(T item)
{
INotifyPropertyChanged inpc = (item as INotifyPropertyChanged);
// Note: inpc may be null if item is null, so always check.
if (null != inpc)
{
if (propertyChangedEventHandler == null)
{
propertyChangedEventHandler = new PropertyChangedEventHandler(Child_PropertyChanged);
}
inpc.PropertyChanged += propertyChangedEventHandler;
}
}
运行起来可以看到界面如下:
▲直接绑定到BindingList<Order>不做任何修改的界面
可以看到,上述的DataGridView显示的列,是根据Order这个实体的所有公共属性名字来生成的,并且当点击列头时是不支持排序的。
BindingList<T>的问题
虽然BindingList<T>通过在实体T实现INotifyPropertyChanged解决了字段更新问题。但BindingList<T>也有自己的缺陷,比如上面的列头显示、排序、筛选等,都需要手动去解决。
列头显示
要解决列头的显示,需要手动的添加列,这里面的”HeaderText“就是显示的列名,”(Name)“是在代码里面访问某个列需要的标识符这里设置为”order_OrderID“,它是在designer中生成的,需要在Form中唯一。假设在一个Form上放了两个DataGridView,那么这两个DataGridView的所有的列的Name必须唯一,否则设计器这里会报错。最后要设置”DataPropertyName“,这就是要绑定实体的对象的属性名称,比如这里我们要将”单号“字段绑定到Order实体的”OrderID“属性上,所以这里设置"DataPropertyName"为”OrderID“.
▲手动添加列头
设置完成之后,在将DataGridView的DataSouce设置为BindingList<Order>之前,还需要禁用自动生成列属性:
private void Form2_Load(object sender, EventArgs e)
{
OrderGenerator.NewOrder += NewOrder;
orderGridView.AutoGenerateColumns = false;
orderGridView.DataSource = orders;
}
再次允许程序,就能看到列头已经改过来了。
▲列头正常显示
排序
现在解决排序和搜索问题。为什么绑定默认的BindingList<T>不支持排序、搜索,它虽然实现了IBindingList接口,但查看源码会发现,它压根就没实现里面的接口方法或属性,只是单纯的返回了false或者null。
bool IBindingList.SupportsSearching
{
get
{
return SupportsSearchingCore;
}
}
protected virtual bool SupportsSearchingCore
{
get
{
return false;
}
}
bool IBindingList.SupportsSorting
{
get
{
return SupportsSortingCore;
}
}
protected virtual bool SupportsSortingCore
{
get
{
return false;
}
}
bool IBindingList.IsSorted
{
get
{
return IsSortedCore;
}
}
protected virtual bool IsSortedCore
{
get
{
return false;
}
}
PropertyDescriptor IBindingList.SortProperty
{
get
{
return SortPropertyCore;
}
}
protected virtual PropertyDescriptor SortPropertyCore
{
get
{
return null;
}
}
所以要解决排序搜索,就必须要实现IBindingList的上述几个对应接口。按照Custom Data Binding Part2文章的介绍,需要新建一个SortableBindingList类,该类继承自BindingList<T>然后重写需要排序的那些个功能。这里实现如下:
public class SortableBindingList<T> : BindingList<T>
{
private bool isSorted;
private PropertyDescriptor sortProperty;
private ListSortDirection sortDirection;
protected override void RemoveSortCore()
{
isSorted = false;
}
protected override bool SupportsSortingCore
{
get { return true; }
}
protected override ListSortDirection SortDirectionCore { get { return sortDirection; } }
protected override PropertyDescriptor SortPropertyCore
{
get { return sortProperty; }
}
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
{
List<T> itemsList = (List<T>)this.Items;
if (property.PropertyType.GetInterface("IComparable") != null)
{
itemsList.Sort(new Comparison<T>(delegate (T x, T y)
{
// Compare x to y if x is not null. If x is, but y isn't, we compare y
// to x and reverse the result. If both are null, they're equal.
if (property.GetValue(x) != null)
return ((IComparable)property.GetValue(x)).CompareTo(property.GetValue(y)) * (direction == ListSortDirection.Descending ? -1 : 1);
else if (property.GetValue(y) != null)
return ((IComparable)property.GetValue(y)).CompareTo(property.GetValue(x)) * (direction == ListSortDirection.Descending ? 1 : -1);
else
return 0;
}));
}
isSorted = true;
sortProperty = property;
sortDirection = direction;
}
}
然后将上述的orders定义从BindingList<Order>改为SortableBindingList<Order>:
private SortableBindingList<Order> orders = new SortableBindingList<Order>();
现在运行,点击表头即可自由排序:
▲ 点击“数量”列头进行排序
过滤
最后一个问题就是过滤或者叫筛选问题,在DataTable中,可以通过设置 orderTable.DefaultView.RowFilter的过滤字符串来达到过滤的目的。但是在BindingList<T>中,跟排序一样,这些查找相关的接口也是没有实现的。也可以参照Custom Data Binding Part3中介绍的方法,实现支持查找和查找的相关属性和方法:
public class SearchableSortableBindingList<T> : BindingList<T>
{
//把前面支持排序的那部分代码复制过来
protected override bool SupportsSearchingCore
{
get { return true; }
}
protected override int FindCore(PropertyDescriptor property, object key)
{
// Specify search columns
if (property == null) return -1;
// Get list to search
List<T> items = this.Items as List<T>;
// Traverse list for value
foreach (T item in items)
{
// Test column search value
string value = (string)property.GetValue(item);
// If value is the search value, return the
// index of the data item
if ((string)key == value) return IndexOf(item);
}
return -1;
}
}
之后,然后要查找的时候,调用BindingSource的Find方法,传入需要查找的列以及需要查找的值。
private void Find(string findColumn, string findStr )
{
// Don't search of a column isn't specified to search in
if (string.IsNullOrEmpty(findColumn)) return;
// Don't search if nothing specified to look for
if (string.IsNullOrEmpty(findStr)) return;
// Get the PropertyDescriptor
PropertyDescriptorCollection properties =
((ITypedList)bindingSource).GetItemProperties(null);
PropertyDescriptor property = properties[findColumn];
// Find a value in a column
int index = bindingSource.Find(property, findStr);
...
}
这太费劲了,而且即使这样,它仍然不够灵活,不能够像DataTable那样支持表达式查找。
幸好,第三方的类BindingListView 完美的解决了上述问题。
优美的BindingListView
在DataTable中就默认实现了的查找,排序等功能在BindingList<T>都没有实现,而这些功能通常都是必须的,可能它在不知道T的类型的情况下无法高效的提供一个实现,所以就没有实现。第三方的BindingListView完美的解决了这个问题,他的相关文档可以查看 https://blw.sourceforge.net/ 。
使用起来非常简单,首先安装这个包。
BindingListView<T>可以看成是对BindingList<T>的一个包装和扩展, 它的用法非常简单:
private BindingList<Order> orders;
private BindingListView<Order> ordersView;
private void Form2_Load(object sender, EventArgs e)
{
orders = new BindingList<Order>();
ordersView = new BindingListView<Order>(orders);
OrderGenerator.NewOrder += NewOrder;
orderGridView.AutoGenerateColumns = false;
orderGridView.DataSource = ordersView;
}
首先新建一个BindingListView<Order>对象,然后用先前定义的BindingList<Order>初始化它。最后把DataGridView的DataSource改为定义的BindingListView即可,其它的地方不用做任何修改。
编译运行,现在的DataGridView在之前绑定的BindingList<T>的基础上已经自动实现了支持排序了。
获取选中的记录
将ViewModel通过BindingList的方式绑定到DataGridView,当用户选中某一行时,可以通过选中行的DataBoundItem直接转换为相应的ViewModel,比如:
Order order = dgv.SelectedRows[i].DataBoundItem as Order;
然后判断order是否为空即可,如果不是,就可以进行下一步操作了。但当将ViewModel通过BindingListView绑定到DataGridView之后,这里行的DataBoundItem的对象类型就变了,其类型不再是Order,而是ObjectView<Order>,可以通过调用dgv.SelectedRows[i].DataBoundItem.GetType()查看其实际类型。
ObjectView<Order> orderView = dgv.SelectedRows[i].DataBoundItem as ObjectView<OrderLogInfoView>;
现在,调用orderView的Object属性,它的类型是Order类型,通过Object字段就能直接能访问Order了。
简单查找
与BindingList<T>提供的单一的Find方法只支持单个列的简单字符串查找不同,BindingListView提供了ApplyFilter方法,这个方法的参数是一个委托,意味着它支持复杂的表达逻辑。
public void ApplyFilter(Predicate<T> includeItem);
public void RemoveFilter();
RemoveFilter表示清除过滤。为了演示,现在假设需要筛选出所有签收过的订单,首先在界面上添加一个CheckBox,它的CheckedChanged事件如下:
private void ckbFinish_CheckedChanged(object sender, EventArgs e)
{
if (ckbFinish.Checked)
{
ordersView.ApplyFilter(x => x.Status == OrderStatus.Finish);
}
else
{
ordersView.RemoveFilter();
}
}
ApplyFilter的参数是一个委托,所以非常灵活,运行程序可以看到结果。
▲所有“已签收”订单,并且按照数量倒序排列。
复杂查找
要实现DataTable那样的能根据表达式字符串进行过滤,就要用到将字符串转换为委托的功能,这个在前面将条件字符串解析为lambda表达式这篇文章中非常详细介绍过了。
最终实现的结果如下:
▲复杂过滤条件的自定义
可以看到,这个条件设置的窗体里面,可以根据实体的属性来对各种筛选条件进行组合,最后拼成条件表达式的字符串,然后将这个字符串转为委托,继而调用ApplyFilter方法:
if (frm.ShowDialog() == DialogResult.OK)
{
filterData = frm.Condition;
string filterString = filterData.ConditionString;
if (!string.IsNullOrEmpty(filterString))
{
var filterFunc = Compiler<Order>.Compile<bool>(filterData.ConditionString);
var predict = new Predicate<Order>(filterFunc);
//过滤数据
ordersView.ApplyFilter(predict);
}
else
{
ordersView.RemoveFilter();
}
}
这里的Compiler泛型类用到了前文介绍的System.Linq.Dynamic.Core ,这里不再赘述。
public class Compiler<TParam>
{
public static Func<TParam, TResult> Compile<TResult>(string body)
{
ParameterExpression prm = Expression.Parameter(typeof(TParam), typeof(TParam).Name);
LambdaExpression exp = DynamicExpressionParser.ParseLambda(new[] { prm }, typeof(TResult), body);
return (Func<TParam, TResult>)exp.Compile();
}
}
另外有个地方需要提一下的就是,在上述自定义框里,实体里面都是字段的名称,是英文字符,要显示英文对应的中文,则可以在类型的属性上加上自定义属性来提供额外的信息,然后利用反射,获取字段的名称,类型,自定义属性的值。我这里定义的自定义属性类型如下:
public class CustomerFilterAttribute : Attribute
{
public string Name { get; set; }
public CustomerFilterAttribute(string name)
{
Name = name;
}
}
然后,在ViewModel的字段上标注那些字段可以用来过滤,并提供对应的用户友好的文字。比如Order类型里面,我们希望根据数量过滤,那么就可以在属性上添加自定义属性:
private int count;
[CustomerFilterAttribute("数量")]
public int Count
{
get { return count; }
set
{
if (count != value)
{
count = value;
OnPropertyChanged();
};
}
}
在根据泛型类型T反射时,提取这部分自定义的属性信息。
private void ChoseForm_Load(object sender, EventArgs e)
{
NameFieldsDic = new Dictionary<string, string>();
NameFieldsTypeDic = new Dictionary<string, Type>();
var properties = DataType.GetProperties();
foreach (var property in properties)
{
var attributes = property.GetCustomAttributes(typeof(CustomerFilterAttribute), true);
foreach (CustomerFilterAttribute att in attributes)
{
NameFieldsDic.Add(att.Name, property.Name);
NameFieldsTypeDic.Add(att.Name, property.PropertyType);
}
}
}
至此大功告成。
总结
本文介绍了使用DataGridView绑定数据的两种方法,一种是直接将DataSource属性绑定到DataTable的DefaultView,绑定后它默认支持排序,筛选等高级功能,另外当DataTable值发生变化时,DataGridView也会自动刷新,它的缺点是DataTable结构过于重,而且对于单元格赋相同的值时,仍然会触发事件导致界面刷新,这在有些场景下会影响刷新效率。另外一种方法是DataGridView绑定BindingList实体,它的优点在于能够进行更多的精细控制,在有些情况下效率会更高,缺点就是内置的BindingList对象并没有实现诸如排序,筛选等功能。针对BindingList的缺点,本文介绍了BindingListView这个第三方的库,它完美解决了BindingList默认不支持排序和筛选,且要实现筛选时筛选功能单一的问题。最后介绍了通过将条件表达式字符串转换为委托并利用BindingListView支持委托查找的功能,从而能够支持复杂的筛选。
参考
- https://github.com/bchavez/Bogus
- https://www.yycoding.xyz/post/2024/1/31/dynamic-convert-a-string-condition-expression-to-lambda-expression
- https://learn.microsoft.com/en-us/previous-versions/dotnet/articles/ms951295(v=msdn.10)
- https://learn.microsoft.com/en-us/previous-versions/dotnet/articles/ms993236(v=msdn.10)
- https://learn.microsoft.com/en-us/previous-versions/dotnet/articles/ms993124(v=msdn.10)
- https://referencesource.microsoft.com/#System/compmod/system/componentmodel/BindingList.cs,bfae1257c0f07cce
- https://github.com/waynebloss/BindingListView
- https://www.yycoding.xyz/post/2024/1/31/dynamic-convert-a-string-condition-expression-to-lambda-expression
- https://www.cnblogs.com/sdflysha/p/20190821-generate-lorem-data.html
- https://www.duidaima.com/Group/Topic/ASP.NET/14979
- https://blw.sourceforge.net/
- https://learn.microsoft.com/en-us/answers/questions/1166104/why-listbox-and-datagridview-does-not-update?cid=kerryherger
- https://www.cnblogs.com/qjjp/p/3286072.html?ivk_sa=1024320u
- https://github.com/sharpdx/SharpDX-Samples/tree/master/Desktop
- https://blog.csdn.net/weixin_34301307/article/details/91966757
- https://www.codeproject.com/articles/20672/real-time-data-grid
- https://github.com/TomaszRewak/DynamicGrid
- DataGridView Printing by Selecting Columns and Rows - CodeProject
- GitHub - YindSoft/fastdatagridview: A fast DataGridView like component for .NET
- Practical Tips For Boosting The Performance Of Windows Forms Apps | Microsoft Learn
- DataGridView.DataSource Property (System.Windows.Forms) | Microsoft Learn
- https://bbs.csdn.net/topics/310025959
- C# DataGridView.DataSource difference between using BindingSource and not - Stack Overflow
- c# - What are the benefits of using a bindingsource with bindinglist<business obj> as datasource? - Stack Overflow