ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DevExpress WinForms GridControl数据绑定全攻略:从绑定机制到性能优化

DevExpress WinForms GridControl数据绑定全攻略:从绑定机制到性能优化 上一篇我们聊了 Data Grid 的基础绑定把 DataTable、List 这些常规套路过了一遍。这回来点硬货专门把绑定这条路上真正容易出问题、也真正体现水平的几个点掰开揉碎讲清楚。DevExpress WinForms 的 GridControl 绑定能力远比表面看到的要深会用的人拿它当展示控件不会用的人拿它当摆设。这篇我们重点处理三件事如何正确绑定业务对象并让界面自动响应变化、复杂路径绑定与表达式列怎么玩、以及大数据量下 Server Mode 和 Instant Feedback Mode 到底该怎么选、怎么配。这篇内容完全围绕 DevExpress WinForms 里 Data Grid 的数据绑定展开适合已经跑通基础绑定、但想进一步解决列表不刷新主从联动卡顿加载十万行数据转圈这些真实问题的朋友。我会把我实际项目里踩过的坑、调过的参数、改过的绑定方式全部摆出来。1. 先分清绑定的核心对象普通对象、可观察集合与BindingList1.1 普通业务对象与 INotifyPropertyChanged很多人最初接触 WinForms 数据绑定都是从绑定 DataTable 开始的。DataTable 本身自带一套变更通知机制Rows 增删改都会自动触发 UI 刷新所以用起来感觉绑定就该是自动的。但切到面向对象的业务模型后问题立刻冒出来——你绑定了一个 List 界面上显示得好好的可一旦在后台线程里把某个 Customer 的名称改了Grid 纹丝不动。原因很简单List 只负责存不负责通知。Grid 显示的数据是从集合读出来的快照集合本身不知道元素内部发生了什么变化。要让 Grid 感知对象属性的变更这个对象必须实现 INotifyPropertyChanged 接口属性 setter 里主动触发 PropertyChanged 事件。这是 .NET 数据绑定的基石也是从能用走向好用的第一道门槛。public class Customer : INotifyPropertyChanged { private string _customerName; public string CustomerName { get _customerName; set { if (_customerName value) return; _customerName value; OnPropertyChanged(nameof(CustomerName)); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }注意几个细节。第一setter 里先做旧值新值比较值没变就不触发事件避免无意义的刷新风暴。第二OnPropertyChanged 用 nameof 而不是硬编码字符串后期重命名属性时不会漏掉。第三事件声明用 PropertyChangedEventHandler 而不是 EventHandler因为 WinForms 的 BindingSource 依赖 PropertyChangedEventArgs.PropertyName 来定位属性。这里有个很多新手不知道的关键点INotifyPropertyChanged 事件是在哪个线程触发的Grid 就在哪个线程刷新。如果你是后台任务修改了属性绑定的集合又在 UI 线程这种情况下 WinForms 的跨线程访问保护可能出来捣乱。实际项目里我一般这样处理后台只负责计算数据、把结果放到临时对象里回到 UI 线程后再改正式对象的属性。或者干脆用 BeginInvoke 把属性赋值切回 UI 线程。这个习惯能帮你躲开一大半数据改了界面不刷新的诡异问题。1.2 ObservableCollection 还是 BindingList对象属性变更解决了集合层面的增删又来了。List 不通知新增和删除Grid 不会自动加行或减行。这时候有两个选择ObservableCollection 和 BindingList 。ObservableCollection 实现了 INotifyCollectionChanged集合元素增删会触发 CollectionChanged 事件Grid 能感知。但它有个短板——默认不实现 INotifyPropertyChanged 的批量通知也不支持排序、过滤的 UI 内建操作。也就是说它适合只增删、不筛选的场景。BindingList 是 WinForms 的老牌选手实现了 IBindingList 接口天生支持排序、查找、变更通知。更关键的是它支持双向绑定的完整链路。比如你把 BindingList 直接赋给 GridControl.DataSource再配合 BindingSource 使用Grid 上的增删改能直接作用到集合本身。BindingListCustomer customers new BindingListCustomer(); customers.Add(new Customer { CustomerName 张三 }); customers.Add(new Customer { CustomerName 李四 }); gridControl.DataSource customers;那么实际项目里怎么选我自己的经验是这样的需求场景推荐集合原因属性变更实时刷新ObservableCollection 元素实现INPC集合通知 属性通知双通道需要Grid内排序/过滤BindingList原生的 IBindingList 支持主从绑定、联动编辑BindingListBindingSource 配合最顺大量数据只读展示List / BindingList无变更通知开销反而小很多人以为 ObservableCollection 比 BindingList 先进其实在 WinForms 生态里恰恰相反。BindingList 是 WinForms 数据绑定的亲儿子DevExpress 的 GridControl 对它的支持也更完整。最典型的坑ObservableCollection 绑到 Grid 上用户想通过 Grid 内置的行新增按钮加记录经常出现行加了但数据源没同步。换成 BindingList 就顺了。1.3 BindingSource 这层传话人为什么不能省GridControl.DataSource 可以直接指向集合但正规项目里我强烈建议中间加一层 BindingSource。BindingSource 的角色类似数据源和界面之间的传话人它自己维护当前记录位置Current、支持 Filter 和 Sort还能统一处理 CurrencyManager 的联动逻辑。BindingSource bindingSource1 new BindingSource(); bindingSource1.DataSource typeof(Customer); // 注意这里传类型 bindingSource1.DataSource customers; // 然后传实例 gridControl.DataSource bindingSource1;先传 typeof 再传实例这是老 WinForms 程序员留下的习惯目的是让 BindingSource 在空数据时也能提供元数据。New 行加进来之前Grid 就知道这个数据源有哪些列可以显示。直接赋实例也行但拿到空集合时列头会一片空白这个问题排查起来很绕。BindingSource 还有一层隐藏价值它把所有下级控件的绑定统一到一个 Current 管理通道上。比如窗体上除了 Grid 还有一个 TextEdit 显示当前客户的联系方式你把 TextEdit 的 DataBindings 也指向同一个 BindingSourceGrid 换行时 TextEdit 自动跟着变这就省掉了手动写 RowChanged 事件的活。2. 复杂路径绑定嵌套属性与表达式字段2.1 绑定嵌套属性业务模型很少是扁平的。客户下面有联系人订单下面有客户名称产品下面有分类名称。Grid 的列要显示这些嵌套属性在 DevExpress GridControl 里有两种做法。第一种是在 GridColumn.FieldName 里直接写点路径。比如 Column 的 FieldName 设为 Contact.PhoneGrid 会自动按照属性路径去取值。这个功能底层用的是 DevExpress 的属性路径解析器不是简单的反射 PropertyInfo.GetValue它能跨多级、还能处理中间对象为空的情况。gridColumn1.FieldName Contact.Phone; gridColumn2.FieldName Order.Customer.CustomerName;允许空引用是嵌套路径最大的坑。如果某个 Customer 的 Contact 是 null取值时不会崩但筛选和排序会静默失败。我遇到过一次诡异现象某列大部分行有值少部分行为空排序时空值行排在最后看起来好像没问题但用户点列头筛选发现空值对应的过滤选项不见了。原因是 DevExpress 的自动筛选行需要通过反射读取所有行的值来建立候选列表空引用导致这一列的值列表不完整。解决这个问题有两个方向。第一个方向是保证业务对象的嵌套属性永远不为空——构造对象时就初始化好子对象这是个好的建模习惯但不总是可行。第二个方向是在 Grid 层面处理用 CustomUnboundData 事件填充这一列的逻辑值或者把嵌套对象打平成顶层属性直接读字段。个人建议能打平就打平尤其是要参与排序筛选的字段简单粗暴往往最稳妥。2.2 用表达式列处理派生字段还有一种典型场景界面上要显示一个原始数据里不存在的字段比如折扣价 原价 * (1 - 折扣率)或者总金额 数量 * 单价。这种字段有几种实现方式但最省事的是 DevExpress 的 Unbound Column 表达式模式。gridColumn1.UnboundType DevExpress.Data.UnboundColumnType.Decimal; gridColumn1.UnboundExpression [Quantity] * [UnitPrice] * (1 - [DiscountRate]);注意几个限制。UnboundExpression 的字段名要用方括号包起来语法接近 SQL 表达式支持算术运算、字符串拼接、甚至 Eval() 做复杂逻辑。但表达式列是只读的。你在 Grid 上改成这个列的值改动不会写回数据源。如果业务上要支持在 Grid 里改折扣价然后反推折扣率这条路就走不通了。表达式列的另一个特点是可以参与排序、筛选、汇总。因为表达式结果在 Grid 内部被当成一个虚拟列来处理属性完整。这个列在 Grid 的列头菜单里也能正常使用自动筛选器。实际项目里我经常给一个 Grid 加若干表达式列做金额汇总再用 Grid 底部的 ShowAutoFilterRow 配合起来做透视效果比引入额外的计算列干净得多。还有一个被低估的功能表达式列的语法里支持条件判断有时候能省掉一整个后台字段。Iif([Status] Closed, 已关闭, 进行中)这种表达式在 Grid 里写出来业务逻辑就散落在界面层了。谨慎使用。适合那种纯展示、不参与复杂计算的状态文本映射不适合作为业务规则的核心。3. 主从绑定与联动3.1 主从结构一个 Grid 里的层级数据DevExpress GridControl 支持两级甚至多级的主从视图。最常见的需求是客户 订单左边父视图列出客户点击某一行下方子视图显示该客户的订单再点一个订单子子视图显示行明细。这种三层结构在 Grid 里叫 View 层级用 LevelName 和 Relation 配置。gridControl.DataSource customers; // 主视图 gridView1.LevelName Customers; // 第二级视图订单 gridView2.LevelName Orders; gridView2.Relations.Add( new DevExpress.XtraGrid.Views.Base.Relation( gridView1, CustomerID, Orders, CustomerID)); // 第三级视图订单明细 gridView3.LevelName OrderDetails;绑定数据源时传一个主集合然后在 Grid 的设计器里定义 Relation 映射。DevExpress 会根据 Relation 自动展开子水平的数据。这个功能的好处是省掉了手工写 Detail 视图的代码模型和 Grid 的层级关系一目了然。这里要特别注意如果主数据集和子数据集是两张独立的 DataTableRelation 的 ParentColumns 和 ChildColumns 必须设置得干干净净。含糊的后果是子视图某些行数据空白、某些行显示重复。我自己调试这种问题通常是把两张表拉到 DataGridView 里手动查一下数据确认关联字段的匹配情况再回 Grid 里看。3.2 跨表联动主从 DataSource 切换的正确姿势另一种主从是独立控件的联动——左边一个查客户列表选择客户后右边 Grid 显示他的订单列表。这种场景核心在于数据源的切换时机。我常用的做法是主 Grid 的 SelectionChanged 事件里重新设置从 Grid 的 DataSource。注意先把从 Grid 的 DataSource 设为 null再设新值这样能避免 DevExpress 内部在下级视图上残留上一批数据的列设置。private void gridView1_SelectionChanged(object sender, DevExpress.Data.SelectionChangedEventArgs e) { if (gridView1.GetFocusedDataRow() null) { gridView2.DataSource null; return; } int customerId Convert.ToInt32(gridView1.GetFocusedRowCellValue(CustomerID)); var orders GetOrdersByCustomer(customerId); gridView2.DataSource null; gridView2.DataSource orders; }还有个容易忽略的细节SelectionChanged 事件在 Grid 初始化时会触发一次此时焦点行可能还没真正落定。要多一个判空逻辑。否则你会在窗体刚加载时看到从 Grid 闪一下空视图然后又正常显示数据。如果从 Grid 要支持排序和过滤数据源类型务必选 BindingList 或 DataTable不要用 List 。List 在切换 DataSource 时排序会丢失之前的排序状态全部重置用户会非常困惑。4. 大数据量展示Server Mode 与 Instant Feedback Mode4.1 两种模式解决什么问题的数据量一上来就轮到 DevExpress 的两大王牌出场了。这两者都是延迟加载思路核心目的是一样的避免把几十万行数据一次性塞进内存还让 UI 保持流畅。但两者的机制和适用场景差别不小选错了后果很直接——要么卡死要么提供不了容器你需要的功能。先说 Server Mode。它是真正的服务端模式。Grid 只加载视口范围内的行滚动时再向数据源请求下一页数据。获取数据的方式是通过实现 ServerModeDataLoader 接口或者设置 DataController 的 ServerMode 并提供一个 IListServer 实现。它的底层每次翻页都走一次查询适合后端是数据库、ORM、Web 服务的场景。Instant Feedback Mode翻译过来叫即时反馈模式外观上表现得像一次性加载了全部数据可以瞬间滚动到任意位置。它基于虚拟模式Virtual Mode实现Grid 直接访问随机行的数据。和 Server Mode 不同它数据源是内存中的集合但通过分块方式提供数据。正因为这样它非常适合数据本来就在本地内存里、只是量大的场景。两者最核心的差异用表格对比下维度Server ModeInstant Feedback Mode数据来源每次滚动按需查询内存集合滚动体验有网络/查询延迟无延迟、秒到位适用场景数据库、Web服务内存中十万级对象实现难度需要实现接口直接绑定排序/筛选服务端处理本地处理4.2 实现要点与参数调优先讲 Server Mode。把 GridView 设置成 Server Mode 的第一件事是在 Load 事件里设置gridControl.DataSource new DevExpress.Data.Linq.LinqServerModeSource(); linqServerModeSource1.QueryableSource dbContext.Customers.AsQueryable();用 LinqServerModeSource 是最省事的做法直接传 IQueryable剩下的事情 DevExpress 帮你做。但这个方案是高度依赖数据库和 ORM 的。如果你的数据源不是 EF Core得手动实现 IListServer。我试过给 MongoDB 写一个过程繁琐但不难核心就是实现 GetAllCount、LoadRows、GetRowKey 这几个方法。这类接口的关键是按需提供数据一次性把数据量算出来再去适配 UI 的滚动窗口。Server Mode 的性能瓶颈通常在数据库查询所以要注意每次翻页请求的数据条数。默认是一次取 100 行。如果页大小太大下拉滚动条时会有延迟感太小则频繁查询数据库压力大。建议结合实际的行高和视口高度来估算一般视口能显示 30 行设置一次预取 100~200 行会比较流畅。Instant Feedback Mode 的实现就简单很多直接用 BindingSource 包一层集合然后gridView1.DataSource new BindingSource { DataSource customers }; gridView1.OptionsBehavior.Editable false; // 虚拟模式不支持直接编辑注意Instant Feedback Mode 是只读的。因为它本质上不持有完整数据副本不支持行编辑。如果业务上既要十万行又要可编辑那得考虑别的方法了。一种变通数据量可控时直接用 BindingList 全量加载省心数据量太大时该项目考量切换 Server Mode。另一个经验大数据量下Grid 的性能还受列数影响很大。每列都要负责取值、格式化、排序。如果只用得到 5 列别把 20 列全拖进去光隐藏列名也比多显示列划算。还有字符串列的自动宽度在大数据量下别开 BestFit那玩意会遍历所有行做宽度计算。十行无所谓十万行就等于自杀。5. 常见问题与排查心得5.1 数据源明明改了Grid 不刷新这种情况出现最多的是绑定了 List 直接调用 list.Add。破局思路换用 BindingList 并且通过 BindingList.Add 添加。如果项目里还有别的地方持有原 List 引用切换数据源后要注意统一入口。// 错误做法 customers.Add(newCustomer); // ListT不通知 Grid // 正确做法 bindingListCustomers.Add(newCustomer); // BindingListT自动通知 Grid还有一种可能是绑定的集合在后台线程里被修改而 UI 线程没有收到同步。上面提到过跨线程修改集合很容易让界面无声无息地不同步。这个问题的排查思路是先在 UI 线程强制刷新一下 Grid比如 GridControl.RefreshDataSource如果能显示数据基本可以断定是线程问题。5.2 属性值改了行内格子没变先确认你的属性 setter 是否触发了 PropertyChanged。我们经常在先写完了 INotifyPropertyChanged但后面重构时把 setter 改成 auto-property事件也就被吞了。再确认事件参数里的 PropertyName 是否与 GridColumn.FieldName 精确一致。大小写、空格这一块 DevExpress 是严格匹配的差一个字母都刷不出来。还有个细节BindingSource 的 ResetItem 方法能强制刷新指定行。遇到改了属性和事件都对不上用这个先救急排查bindingSourceCustomer.ResetItem(index);但我不建议把 ResetItem 当作常规方案它掩盖了原问题。常规还是要靠 INotifyPropertyChanged 走正确通道。5.3 列存在但绑定后不显示数据这多半是 FieldName 写错了。DevExpress 的列绑定是字符串匹配运行时不报错只会看到列里空空的。排查方法在设计器里点开 Columns 的 Properties看 FieldName 下拉列表里有没有你要的字段。下拉里没有说明要么数据源的公共属性不存在要么列类型和数据源类型不匹配。还有一种隐蔽情况你绑定的是匿名类型的集合。匿名类型在 DevExpress 的反射机制下有时识别不全表现出来就是列头能出来但数据全是空。这种情况下建议把匿名对象转成命名类型再绑定。5.4 Grid 卡顿先定位是绑定问题还是渲染问题遇到滚动卡顿第一反应别急着骂 Grid。先做一个实验把 Grid 的 CellValueChanged 事件和 CustomDrawCell 事件全部禁用再滚动看卡不卡。如果不卡了说明瓶颈在事件处理或者自绘代码还卡才往数据源方向排查。绑定方面的优化路径我已经在前面说过了——换成延迟加载模式、减少列数、不要用 BestFit、关闭不必要的 Format Condition。如果这些做完了还卡检查一下 Form 的 DoubleBuffered 属性。DevExpress 控件自带双缓冲但父容器如果频繁触发重绘照样拖累。我还遇到过一个很蠢的坑数据量几千行本来不卡但因为代码里给 Grid 添加了一个全网格范围内的 CustomDrawCell里面每次取字符串再 GDI 绘制结果滚动变成PPT。后来把自绘范围缩小到只有特定列性能立刻回来了。这个经验就是——自绘代码能少写就少写能限定范围别全绘。6. 绑定方案选型的经验总结讲了这么多最后按照我项目里的习惯给大家梳理一个决策路径。新的 WinForms 项目接到手数据量和交互复杂度先问清楚再走绑定选型。数据量低于一万行、只读展示直接用 BindingList 或 List 都行列也别搞太花哨。一万行以上、要流畅滚动优先考虑 Instant Feedback Mode。数据来自数据库、数据量很大、需要实时查询上 Server Mode 或者 LinqServerModeSource。需要用户编辑、新增、删除BindingList BindingSource 是底线虚拟模式靠边站。需要多层级展示GridView 的 Relations 比手动放多个 Grid 联动要稳得多。有个容易被忽略的点是绑定方案的统一性。一个主窗体上若有多处用到 Grid尽量用同一套绑定模式和集合类型。混用 List 、BindingList 、DataTable、匿名类型短期内看不出问题后面加需求时就会变成一场灾难。数据绑定的一致性能省下一大半后期沟通和排障成本。我在真实项目里踩过的最大一个跟头是把 DataTable 和业务对象混绑在同一个 Grid 的列里。不光是新手老手也容易图省事在某个列上直接赋值一个 DataTable 作为列表数据源。结果就是排序、过滤、主从三条链路全部绕晕。记住一个原则一个 Grid 的数据源只能有一个根下面的列全部依赖这个根来取数据。用 Unbound 列做辅助展示没问题但别让它变成主数据源的一部分。关于 INotifyPropertyChanged 的使用再啰嗦一句不是所有属性都需要实现变更通知。只在 UI 会监控、且业务逻辑需要实时反映的属性上加。为所有属性无差别加 INPC代码可读性下降不说每次 setter 都触发事件的开销在极端大量数据下也能感受到。收到通知刷新字段这个路径轻则无害重则引发一连串 UI 更新是很常见的性能隐患之一。最后再分享一个小技巧。调试绑定问题的时候在 Grid 的设计器里把 OptionsBehavior.Editable 临时设成 false然后跑起来再设回 true。这一步的作用是把用户编辑改变数据源状态这个变量从排查范围里剔除让你能专注分析数据源本身的问题。我经常靠这个简单开关快速定位问题是在模型还是在界面很实用。数据绑定这件事说到底就是数据源、中间件、展示层三者之间的契约管理。把契约理清、选对工具、保持一致性DevExpress WinForms 的 Data Grid 就是你手里最顺手的展示控件。希望这篇能帮你少走几条弯路尤其是绑定不刷新和性能卡顿这两个大头碰上了能少熬几夜。
返回列表