资讯动态

XAF GridView定制化实战:列显隐、按钮状态与样式汇总

发布时间:2026/10/24 1:34:13 来源:尧图企业网站定制
1. 从“够用”到“好用”XAF GridView定制化到底在解决什么问题XAFeXpressApp Framework是大家熟知的快速业务应用开发框架靠着内置的模块化结构和自动生成的UI能在很短时间内搭起一套标准的企业管理后台。不过接触过一段时间后你会发现框架默认生成的表格界面往往只是“能用”距离“好用”还有明显差距。我当时接手一个进销存模拟项目X时前端同事提出了一堆列表页需求某些列要根据权限隐藏、金额数字要千分位红色高亮、操作列要根据单据状态动态禁点、列表底部要汇总金额和数量。如果基于传统WebForm或者纯手工ASP.NET来做这些需求并不算难但在XAF里“改表格”这件事需要先弄清它的Model层和ViewController机制否则会陷入到处写事件、却总被框架重建界面覆盖的窘境。这篇内容适合两类读者一是正在用XAF搭建业务系统、觉得默认列表不好用的开发者二是准备从其他.NET框架迁移到XAF、想搞清楚GridView定制化边界的人。我会用自己实际项目中踩过的坑、验证过的方法把XAF中GridView定制化的入口、常见场景、代码写法和注意事项完整梳理一遍。先说清楚一个概念XAF里的GridView不是孤立的控件。它在运行时由XAF根据Application Model中的视图节点动态生成每个列表视图ListView对应一个视图节点节点的属性控制着GridView的大部分行为。这意味着定制GridView其实有两条路一条是纯声明式改Model——不动代码只改配置另一条是代码介入——通过ViewController拦截控件生成过程对GridView实例直接操作。绝大多数需求是这两条路的组合。理解这一点后面所有定制方案才不会跑偏。2. 写代码之前先搞清GridView定制化的三个主要入口2.1 Model Editor不改代码就能完成的“轻定制”XAF最吸引人的特性之一就是Application Model。在Visual Studio里双击启动项目下的Model.xafml文件会打开Model Editor。展开Views节点找到对应的ListView视图你会看到Columns、View、GridView等子节点。很多定制需求其实在这个编辑器里就能完成不需要写一行C#代码。举个例子默认生成的列表里某列标题需要改成业务上的叫法比如把“ProductName”改成“商品名称”同时设置列宽120像素。操作路径是Views → ListView节点 → Columns → ProductName → 在属性面板改Caption和Width。如果希望该列不显示直接把Index属性改成-1即可。这种“轻定制”最适合结构调整类需求比如调整列顺序、修改标题、固定列宽、启用列的过滤和排序等。不过Model Editor有一个让人容易混淆的点它修改的是存储在xafml文件中的模型差异Model Difference。当多个项目模块叠加时不同模块对同一视图节点可能有各自的定义最终生效的规则是“后加载的模块优先级更高”。因此如果你在A模块中设置了某列隐藏而B模块后来又把它设为可见最终结果以B模块为准。这个机制在团队协作时容易产生“为什么我改了没生效”的困惑排查时要先检查模块依赖顺序而不是怀疑代码没编译。2.2 ViewController运行时介入的主战场当需求超出Model可以表达的范围比如“根据当前登录用户的角色动态修改某列的可编辑性”、“根据行数据状态改变单元格颜色”就需要用ViewController在运行时拿到GridView实例。XAF中每个列表视图都有一个对应的ViewController生命周期在视图创建时触发OnActivated在视图控件加载完成后触发ViewControlsCreated。定制GridView的最佳时机通常是ViewControlsCreated因为此时控件已经生成你拿到的GridView是真实控件实例可以直接调属性、挂事件。OnActivated阶段虽然也能拿到View但View.Control可能还是空引用此时操作控件实例会抛异常。我习惯在ViewController中先判断视图类型再操作控件带个简单的防御性判断using DevExpress.ExpressApp; using DevExpress.ExpressApp.Editors; using DevExpress.ExpressApp.Win.Editors; using DevExpress.XtraGrid.Views.Grid; public class GridViewCustomizeController : ViewController { protected override void OnActivated() { base.OnActivated(); if (View is ListView listView) { // 视图激活时先订阅控件创建完成事件 listView.ControlsCreated ListView_ControlsCreated; } } private void ListView_ControlsCreated(object sender, System.EventArgs e) { if (View is ListView listView listView.Editor is GridListEditor gridEditor) { GridView gridView gridEditor.GridView; // 在这里对gridView进行定制 CustomizeGridView(gridView); } } private void CustomizeGridView(GridView gridView) { // 具体定制代码 } }这段代码有几个关键点值得展开说。第一listView.ControlsCreated事件可能触发多次比如视图每次重新加载时所以CustomizeGridView方法内部要做幂等设计避免重复添加事件导致行为叠加。第二通过listView.Editor判断类型为GridListEditor是因为XAF理论上可以切换不同列表编辑器直接强转可能因编辑器类型不符而抛异常。第三拿到GridView后要尽快完成定制并解除事件订阅避免内存泄漏——虽然XAF的ViewController本身有生命周期管理但事件订阅总是越干净越好。2.3 GridView自身的行与单元格事件第三种入口是GridView自带的事件比如RowCellStyle、CustomDrawCell、RowCellClick、ShowingEditor等。这些事件在特定时机被触发适合实现显示层的细粒度定制。以RowCellStyle为例它的触发频率很高每个单元格绘制时都会触发所以事件处理代码里要避免复杂计算否则列表滚动时会明显卡顿。private void GridView_RowCellStyle(object sender, RowCellStyleEventArgs e) { if (e.Column.FieldName Amount e.CellValue is decimal amount amount 10000) { e.Appearance.ForeColor System.Drawing.Color.Red; e.Appearance.Font new System.Drawing.Font(e.Appearance.Font, System.Drawing.FontStyle.Bold); } }这里的逻辑很好理解当列名是Amount且单元格值大于10000时字体变红加粗。实际项目中类似的定制还有库存低于安全库存时整行标黄、订单状态已取消时整行灰色显示等。行级样式用e.RowHandle配合GridView.GetRowCellValue去读取状态字段判断后修改e.Appearance即可。注意RowCellStyle里不要做数据库查询或跨线程操作UI线程里做重活会拖垮渲染性能这是性能优化中的一条铁律。3. 核心定制场景实战列、按钮、汇总与权限联动3.1 列显隐、顺序和格式的Modelless方案在XAF里列的增删改最推荐的方式就是Model Editor但有一种情况必须代码介入列是动态生成的。比如某跨平台系统项目中不同的仓库类型对应不同的附加属性列这些列由程序运行时根据元数据动态创建xafml文件里根本没有对应节点。此时你需要通过代码向GridView添加列。GridColumn dynamicColumn new GridColumn { Caption 动态列, FieldName DynamicField, Visible true, Width 100, OptionsColumn { AllowEdit false } }; gridView.Columns.Add(dynamicColumn);这里有个容易踩的坑如果你把FieldName指定为不存在的属性名GridView本身不会报错但单元格绑定不到任何数据运行时显示空值。因此在动态加列之前务必确认数据源类型确实包含该属性或者使用自定义UnboundColumn未绑定列模式通过CustomUnboundColumnData事件手动提供数据。未绑定列的设置方法GridColumn unboundCol gridView.Columns.AddField(TotalRowAmount); unboundCol.UnboundType DevExpress.Data.UnboundColumnType.Decimal; unboundCol.Caption 行小计; gridView.CustomUnboundColumnData (sender, e) { if (e.Column.FieldName TotalRowAmount) { decimal quantity Convert.ToDecimal(gridView.GetRowCellValue(e.ListSourceRowIndex, Quantity)); decimal price Convert.ToDecimal(gridView.GetRowCellValue(e.ListSourceRowIndex, Price)); e.Value quantity * price; } };未绑定列灵活且不影响底层数据源非常适合计算列和汇总列的展示需求。需要提醒的是CustomUnboundColumnData中根据行号取其他列值可能存在行索引与数据源索引不一致的情况尤其是开启分组或排序后。稳妥做法是先用gridView.GetDataSourceRowIndex把GridView行号转成数据源行号再取值。3.2 操作按钮的动态控制禁用、隐藏与权限列表每行的操作按钮比如“审核”、“作废”、“打印”是业务系统中的重头戏。XAF中这类按钮通常来自两个地方一是ViewController中定义的Actions二是模块内置的Delete、New等系统Action。按钮的动态控制有一个通用原则不要在按钮的Click事件里做“当前状态不允许操作就return”而是应该在按钮执行前就把它置灰或隐藏让用户直观看到哪些操作不可用。我常写的模式是这样的在ViewController中订阅View的CurrentObjectChanged事件每当选中行变化时重新计算按钮可用状态。private void View_CurrentObjectChanged(object sender, System.EventArgs e) { UpdateActionState(); } private void UpdateActionState() { if (View.CurrentObject is SalesOrder order) { bool isApproved order.Status OrderStatus.Approved; approveAction.Active.SetItemValue(StatusGuard, !isApproved); cancelAction.Active.SetItemValue(StatusGuard, order.Status ! OrderStatus.Cancelled); } }Action的Active是一个复合条件集合使用SetItemValue而不是直接赋值是为了避免不同模块之间互相覆盖状态设置。还有另一种场景按钮存在但希望根据权限隐藏这时可以在Model里给Action设置ConfirmationMessage或TargetObjectsCriteria但更细粒度的权限还是要结合代码判断——XAF的Action.Active机制本质上是一个多来源开关任何来源把Active设为false按钮整体就不可用了。实际项目中“当前单据已审核不可重复审核”、“当前库存不足不能出库”这类交互都是通过Action状态管理实现的。3.3 行样式、单元格格式化与分组汇总接着回到GridView显示层的高级定制。除了前面提到的行颜色格式化单元格显示也是常见需求。比如金额字段要求千分位显示并在列标题上体现单位。最直接的方式是通过Model设置的DisplayFormat属性但在某些场景下需要代码覆盖格式gridView.Columns[Amount].DisplayFormat.FormatType DevExpress.Utils.FormatType.Numeric; gridView.Columns[Amount].DisplayFormat.FormatString #,##0.00;如果要根据行状态显示不同文本比如状态字段显示为“已完成”而非“Completed”可以用CustomColumnDisplayText事件gridView.CustomColumnDisplayText (sender, e) { if (e.Column.FieldName StatusCode) { int statusCode Convert.ToInt32(e.Value); e.DisplayText statusCode switch { 0 草稿, 1 待审核, 2 已审核, _ 未知 }; } };分组汇总的定制更具实战价值。业务员最常提的需求是列表底部能看到“数量合计”和“金额合计”。在XAF中可以通过gridView.GroupSummary或gridView.ColumnSummary实现。我一般在ViewControlsCreated里动态添加gridView.OptionsView.ShowFooter true; GridColumn amountColumn gridView.Columns[Amount]; if (amountColumn ! null) { GridSummaryItem summaryItem new GridSummaryItem { SummaryType DevExpress.Data.SummaryItemType.Sum, FieldName Amount, DisplayFormat 合计: {0:#,##0.00} }; amountColumn.SummaryItem summaryItem; }设置ShowFooter为true后底部会出现汇总栏。有一个细节如果数据源是XAF的XPCollection或BindingList汇总计算是控件端完成的不需要额外查询数据库性能尚可。但如果是大数据量场景例如超过10万行建议只在当前页范围内汇总并限制不可分组过多否则绘制底层会重复计算所有行数据界面交互会明显变慢。4. 实操过程一个带权限和状态控制的定制完整示例下面用一个比较综合的例子把前面讲的内容串起来。假设你在做一个采购订单列表需求是普通用户看不到“成本价”列订单状态为“已关闭”的行整行变灰“审核”按钮只在“草稿”状态时可见列表底部展示金额合计。整个实现分几步走。第一步创建ViewController挂载事件public class PurchaseOrderListViewController : ViewController { public PurchaseOrderListViewController() { TargetViewType ViewType.ListView; TargetObjectType typeof(PurchaseOrder); } protected override void OnActivated() { base.OnActivated(); View.ControlsCreated View_ControlsCreated; View.CurrentObjectChanged View_CurrentObjectChanged; } protected override void OnDeactivated() { base.OnDeactivated(); View.ControlsCreated - View_ControlsCreated; View.CurrentObjectChanged - View_CurrentObjectChanged; } // 后续方法省去思路见下文 }第二步在ViewControlsCreated中根据当前用户角色隐藏成本价列private void View_ControlsCreated(object sender, System.EventArgs e) { if (View is ListView listView listView.Editor is GridListEditor gridEditor) { GridView gridView gridEditor.GridView; SecuritySystem.CurrentUser maybe SecuritySystem.CurrentUser as Employee; bool canViewCost maybe?.Role?.Name Manager || maybe?.Role?.Name Admin; GridColumn costColumn gridView.Columns[CostPrice]; if (costColumn ! null) { costColumn.Visible canViewCost; } // 行色与汇总 gridView.RowCellStyle GridView_RowCellStyle; gridView.OptionsView.ShowFooter true; GridColumn amountColumn gridView.Columns[TotalAmount]; amountColumn.SummaryItem new GridSummaryItem() { SummaryType DevExpress.Data.SummaryItemType.Sum, FieldName TotalAmount, DisplayFormat 合计: {0:#,##0.00} }; } }第三步实现行变灰和审核按钮动态控制private void GridView_RowCellStyle(object sender, RowCellStyleEventArgs e) { var statusValue gridView.GetRowCellValue(e.RowHandle, Status); if (statusValue ! null (int)statusValue (int)OrderStatus.Closed) { e.Appearance.ForeColor System.Drawing.Color.Gray; e.Appearance.BackColor System.Drawing.Color.FromArgb(240, 240, 240); } } private void View_CurrentObjectChanged(object sender, System.EventArgs e) { if (View.CurrentObject is PurchaseOrder order) { bool isDraft order.Status OrderStatus.Draft; approveAction.Active.SetItemValue(OnlyDraft, isDraft); } }这几段代码合在一起就是一个完整的定制流程。实际操作中我还建议加上异常保护控件事件里不要抛异常到UI层尽量捕获并记录日志。因为GridView事件触发频繁一旦偶发异常被抛出用户看到的是界面卡死或无响应排查成本很高。这个例子之所以把“权限控制”和“视图行为”放一起是因为真实项目中两者经常交缠。权限既能通过Model层面的ListView修改权限配置比如设置允许View、允许Edit等也能在代码层做列显隐控制二者最终影响的是不同层级的业务规则。我的经验是涉及明确角色维度的列隔离放代码里更稳妥涉及菜单入口、功能权限、数据范围过滤的优先用XAF权限模块配置不要让业务规则散落在多个ViewController之间。5. 常见问题与排查技巧实录5.1 改了Model.xafml不生效这是刚上手时的第一坑。原因通常是浏览器缓存或IIS应用程序池未重启。XAF的Model Difference在开发机上是启动时从xafml文件加载的但如果同时有多个模块项目且你在某个模块的Model.xafml里做了修改发布后却由另一个模块的xafml覆盖了同名节点。排查方法很简单打开Model Editor看左侧节点的“紫色图标”是否还在紫色表示该节点的修改来自Model Difference如果节点变成默认图标说明你的修改没保存到这个模型层。其次检查模块的Module.Designer.cs里是否有重复的xafml嵌入资源。老项目里我见过把Model.xafml同时设置成了CopyToOutputDirectory和EmbeddedResource导致运行时读取到的是旧文件副本。解决方式是统一按XAF模板默认方式处理Model.xafml保持嵌入式资源唯一来源。5.2 运行时GridView又是空的列没显示这种情况多半是数据源本身没数据或者列被Model层设置了Visiblefalse。先确认不是后者再去数据源层面排查。另外还有一个冷门原因如果列绑定的属性没有设置公共的getter/setterXAF的反射机制可能拿不到该属性列虽然存在但值是空的。比如某些partial class中定义了internal属性XAF默认绑定不到。遇到这个问题时把属性改成public即可或者使用IModelColumn的PropertyEditorType显式指定。5.3 RowCellStyle不触发或样式一闪而过RowCellStyle是高频绘制事件理论上都会触发。不触发最可能的原因是GridView开启了“视图缓存”或“Server Mode”且数据源不是标准BindingList。XAF的GridListEditor默认使用XPServerCollectionSource在Server Mode下部分单元格事件的行为和普通绑定模式不同。如果你发现RowCellStyle无响应可以临时把View.Editor的Server Mode关掉测试——如果关闭后样式生效那问题就清楚了。Server Mode下建议改用CustomRowCellStyleForVirtualMode等事件或者干脆不要依赖行样式做关键业务提示改用字段值本身的文本前缀。注意RowCellStyle高频触发内部尽量避免创建GDI对象如Font、Brush而不释放。要做性能优化可对样式值预缓存比如把Font对象定义为静态只读字段而不是每次事件里new一个。5.4 按钮Action时灵时不灵Action的Active状态是由多个Key共同决定的。如果你在代码里用Active[abc] true这种方式直接赋值再叠加XAF框架内部对权限的判断就可能出现“权限上允许但代码上禁用”或“代码上允许但权限上禁用”的冲突。稳妥做法是统一用SetItemValue并且在不同ViewController中避免对同一个Key重复设置。遇到Action状态不符合预期时用断点查看Action.Active的所有条件和值通常能一眼看出是哪个来源把它置false的。如果当前Action是在PopupWindow或DetailView的Controller中还需要确认TargetViewType是否匹配否则OnActivated不执行按钮状态自然也不会更新。5.5 定制代码影响了系统内置视图有些定制是针对全部ListView生效的比如我在某个ViewController里挂了一个全局的RowCellStyle结果所有列表的行样式都被改动影响到了XAF内置的权限管理列表。解决思路是收窄作用范围TargetObjectType指定具体业务类型或者TargetViewId指定具体视图ID。如果必须全局生效就要在代码里做明确过滤避免对其他模块产生副作用。这类“全局定制事故”在团队协作时尤其普遍所以我在规划阶段就归纳了一个规则凡是显示层的定制代码一律集中在专用的View controllers目录中按模块或功能分组每个Controller的Target属性务必写全。6. 定制之外的思考GridView定制化的边界与架构规划谈到最后我想分享一个架构层面的经验。XAF的GridView定制化虽然灵活但并不意味着所有东西都该在控件层做。行数据计算、状态变更逻辑属于业务逻辑层应放在Domain对象或业务服务里列显隐、按钮状态、颜色显示属于表现层适合放ViewController而哪些科目对哪些角色可见属于权限范围优先用XAF的权限配置。三个层次职责清晰才能避免出现“为了改个颜色顺手把数据改了”的危险操作。我见过一些项目把业务状态判断写进了RowCellStyle单元格一刷新就执行一大串数据库查询。这种设计虽然一时爽却让性能和可维护性大打折扣。后来维护者根本不敢动这些代码因为不清楚这些查询是否有副作用。我的建议是GridView定制化里只做两件事——从当前行取已加载的值做判断、对界面外观做修改。任何需要额外查询数据、需要调用业务服务的逻辑都应该提前在数据加载阶段完成UI层只消费结果。从另一个角度看GridView定制化的需求也反映了XAF视图模型的一个优势你可以在不改变业务类结构的前提下为不同业务流程定制完全不同的列表体验。同一种业务对象采购员进来看到的是带审核按钮的列表财务进来看到的是带金额汇总和凭证按钮的列表管理员看到的是整行可编辑的全字段列表。视图与业务对象解耦配合ViewController的“按需加载”使得XAF系统可以像搭积木一样组合出不同的业务界面。7. 收尾一点个人的实战心得启动GridView定制之前先花半小时把Model Editor里该节点的所有属性浏览一遍能省掉后面很多无谓的代码。很多表面上看“必须写代码”的需求其实一个属性就搞定了。我最初在模拟项目X里做金额列格式化就是想当然写了事件代码结果运行后发现只需在Model里设置DisplayFormat即可那段额外代码不仅多余还拖慢了单元格绘制速度。另外不要在页面加载的构造函数里去访问GridView实例。XAF中控件的实际创建时机比ViewController构造函数晚得多在构造阶段拿到的只能是null这也是新手容易踩的地雷。把定制逻辑统一放在ControlsCreated事件回调里这是控件的“安全操作窗口”。最后再分享一个调试小技巧如果你不确定当前GridView是否已经加载可以在ListView控件上挂一个VisibleChanged事件在里面输出日志这样能直观看到控件的生命周期轨迹写定制代码时心里更有底。GridView定制化这件事说难不算难说简单也不简单关键是把XAF的模型层、控制器层和控件事件三层机制理解透再配合对业务场景的准确判断就能把界面做得既好看又可靠。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑