简介:围绕 C# WinForms 表格录入场景,这份示例工程面向希望限制单元格取值、提升输入一致性的初中级开发者,重点演示在 DataGridView 中配置下拉列,并处理选项、数据源与选择变化等问题。压缩包共 22 个文件,约 58KB,包含 6 个 cs 源码,以及解决方案、项目、窗体资源与配置等文件,另有 exe、pdb 等编译输出和说明文本,源码与运行产物较为完整,便于对照理解列类型、设计器代码与资源组织。该资源已有 2395 人学习下载。读者可参考其中的 DataGridViewComboBoxColumn 用法,了解下拉项填充、列样式与宽度、数据源绑定、选择变更事件、只读控制和自定义显示等要点,也可将其作为表格录入界面原型的实现样例,辅助梳理下拉列与数据绑定的配合方式,适合课程设计、内部管理工具等表格编辑需求参考。
1. 从“状态列只能手打”说起:DataGridView 下拉列到底卡在哪
前阵子接手一个内部订单录入工具,界面上的“订单状态”是一个纯文本框,用户可以随便输入“已发”“已发货”“发了”,后端统计的时候满地都是脏数据。最直接的解法就是在 DataGridView 里把这一列换成下拉列表。我在 csharp 的 WinForms 里做了很多年,本以为十几行代码就能收工,结果真正跑起来之后,问题一个接一个:值明明选进去了,一提交又跳回原文;数据刷新一次,下拉项集体消失;点开某一行直接弹出“DataGridViewComboBoxCell 的值无效”的 DataError。
这个项目解决的就是这一件事:在有界面的表格控件里嵌一个可绑定、可联动、能把值正确写回数据源的下拉列。它适合正在做 WinForms 桌面端、后台管理工具、出入库单据录入的开发者,尤其是被 DataError 弹窗折磨过一轮的人。下面按“能跑起来 → 知道为什么能跑 → 出问题怎么查 → 怎么复用”的顺序走一遍。
2. 最小可跑 Demo:半小时从空窗体到能选值的下拉列
在写业务代码之前,先做一个只有两列、能正常选择、能正确提交的空壳。这个空壳很重要,因为后面 80% 的报错都能在这个最小例子里复现出来,先把环境确定下来,排查时就不至于把问题归到玄学上。
2.1 环境、项目类型与目标框架
WinForms 属于 Windows 桌面应用,新建工程的时候项目模板选Windows Forms App,不要选“控制台应用”再手动改,那样会少一堆默认的应用程序配置文件。命令行起手也行:
# .NET 6/7/8 的 WinForms 模板,要求本机装有 Windows 桌面开发工作负载 dotnet new winforms -n ComboGridDemo cd ComboGridDemo dotnet run目标框架上我一般写net8.0-windows,老项目就net48。两者对 DataGridView 的 API 差异可以忽略,差异主要在DataTable的序列化和 Span 那类新语法上。有血泪经验的一条:如果对方机器上是 Win7 或者一堆老旧的域内终端,别直接上 .NET 8 的 self-contained 发布,先确认运行时能装,否则你调试机跑得飞快,到现场双击没反应。
工程建完之后,Form 上不要急着拖控件。设计器生成的MainForm.Designer.cs一旦混进去大量手动代码,后面改列的顺序会非常痛苦。我的做法是保留一个空 Form,所有控件在构造函数里用代码建,这样整份 UI 结构都在一个文件里,谁改了一眼就能看到。
2.2 用代码建一个 DataGridViewComboBoxColumn
下拉列不是给普通列换一个编辑控件,它是独立的列类型DataGridViewComboBoxColumn。下面这段是能直接粘贴进 Form 构造函数的最小骨架:
public partial class MainForm : Form { private DataGridView grid; private DataGridViewComboBoxColumn colStatus; public MainForm() { InitializeComponent(); BuildGrid(); FillStatusItems(); } private void BuildGrid() { grid = new DataGridView { Dock = DockStyle.Fill, AllowUserToAddRows = false, AllowUserToDeleteRows = false, RowHeadersVisible = false, AutoGenerateColumns = false, // 关键:禁止按数据源自动铺列 EditMode = DataGridViewEditMode.EditOnEnter }; // 普通文本列,用来对照,确认绑定没串位 var colOrderNo = new DataGridViewTextBoxColumn { Name = "colOrderNo", HeaderText = "订单号", DataPropertyName = "OrderNo", Width = 160 }; colStatus = new DataGridViewComboBoxColumn { Name = "colStatus", HeaderText = "订单状态", DataPropertyName = "StatusId", // 对应数据源里的字段名 Width = 140, FlatStyle = FlatStyle.Flat, DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton, ValueType = typeof(int) // 必须显式声明,后面细说 }; grid.Columns.Add(colOrderNo); grid.Columns.Add(colStatus); Controls.Add(grid); } }AutoGenerateColumns = false这一行是整段的重点。默认它的值是 true,一旦你设了grid.DataSource,控件会按数据源的列名自动再建一批文本列,结果就是表格里出现两套“订单状态”,一套是你精心配置的下拉列,另一套是傻乎乎的文本框,而且用户往往点中的是那套文本框。这个坑在接手项目里出现的频率高得离谱。
DisplayStyle我选DropDownButton,意思是单元格平时看着像普通文本框,只有拿到焦点时才出现下拉箭头。另外两个值是ComboBox(一直显示成下拉框)和Nothing(只显示文本)。表格行高有限,常驻下拉框视觉上会比较拥挤,所以一般用DropDownButton。
2.3 两种数据来源:Items 硬编码和 DataSource 绑定
下拉项有两条路。数据量只有三五个固定值、而且基本不会变的时候,直接往Items里塞对象最省事:
public class StatusItem { public int Id { get; } public string Name { get; } public StatusItem(int id, string name) { Id = id; Name = name; } public override string ToString() => Name; // 调试时打印出来有可读含义 } private void FillStatusItems() { colStatus.Items.Clear(); colStatus.Items.Add(new StatusItem(1, "待付款")); colStatus.Items.Add(new StatusItem(2, "已发货")); colStatus.Items.Add(new StatusItem(3, "已签收")); colStatus.DisplayMember = "Name"; // 下拉列表里显示的文字 colStatus.ValueMember = "Id"; // 真正写进单元格的值 }要注意DisplayMember和ValueMember在Items模式下同样生效,不是只有绑定 DataSource 时才需要。而且这里的ValueMember = "Id"跟上面ValueType = typeof(int)必须一致:前者决定“取出来的值是什么”,后者决定“单元格认为这个值该是什么类型”。两者错位,就是后面 DataError 的主要来源。
Items模式撑不住的时候就得换DataSource:状态值超过二十个、要按租户过滤、需要从后台接口或者配置表读取,这些场景下硬编码会失控。切到 DataSource 的写法在下一章展开,这里先记住一条:Items和DataSource尽量不要混用,混着填会出现“列表里有,但选完值对不上”的问题,非常难查。
到这里 Demo 已经能跑了。直接dotnet run或者 F5,双击下拉列,应该能看到三个状态可选。如果这一步就已经弹 DataError,先别往下读,回到ValueType和ValueMember这两行逐字比对,通常就是类型没对齐。
3. 绑定链路拆解:DisplayMember、ValueMember 与 ValueType 谁管什么
能跑起来之后,真正决定这套代码能不能上生产的,是数据源、下拉列、单元格这三层之间怎么串。三个属性各管一段,搞清楚了,后面很多“莫名奇妙”的报错就不用一步步试了。
3.1 三个属性各自负责哪一段
用一个表格说清楚,比在代码里来回翻要直观:
| 属性 | 作用位置 | 常见取值 | 设错的后果 |
|---|---|---|---|
DataSource | 提供下拉候选集合 | DataTable / BindingSource / List<T> | 设了但没设 DisplayMember,下拉里显示成类型名 |
DisplayMember | 决定候选行显示哪一列 | 字符串属性名,如"Name" | 下拉项一片空白或显示System.Data.DataRowView |
ValueMember | 决定选中后取哪一列的值 | 字符串属性名,如"Id" | 单元格里存的是整行对象,回写数据库时报类型错 |
ValueType | 单元格值的 CLR 类型 | typeof(int)/typeof(string) | 值与候选集比较时类型不一致,触发 DataError |
最容易被忽略的是ValueType。它不参与“显示什么”,只参与“比较什么”。假设ValueMember是int类型的Id,但ValueType没设,单元格默认按object或者从数据源推断的类型走。当主表里这一列是DBNull或者字符串"1",控件拿它去和候选集里整数1比对,判定为“值不在列表中”,于是抛 DataError。血泪经验是:ValueType一定要和主数据源里对应字段的类型严格一致,int就是int,可空类型就老老实实用object兜着再做转换。
DisplayMember、ValueMember只对“有结构的对象”有意义。如果Items里全是字符串,这两个属性不设也能正常用;一旦换成DataRowView或者自定义类,不设就一定显示错。
3.2 用 BindingSource + DataTable 把链路接完整
真实项目里我一般用BindingSource包一层,而不是把DataTable直接丢给列。原因很实际:后面要刷新、要按行过滤、要重置绑定,BindingSource提供了现成的入口,直接操作 DataTable 得自己处理事件。
private BindingSource statusSource; private DataTable orderTable; private DataGridViewComboBoxColumn colProvince; private DataGridViewComboBoxColumn colCity; private void BindWithDataSource() { // 1) 字典表:Id 用来存,Name 用来显示 var statusTable = new DataTable("Status"); statusTable.Columns.Add("Id", typeof(int)); statusTable.Columns.Add("Name", typeof(string)); statusTable.Rows.Add(1, "待付款"); statusTable.Rows.Add(2, "已发货"); statusTable.Rows.Add(3, "已签收"); // 2) 包一层 BindingSource,后续刷新都走它 statusSource = new BindingSource { DataSource = statusTable }; colStatus.DataSource = statusSource; colStatus.DisplayMember = "Name"; colStatus.ValueMember = "Id"; colStatus.ValueType = typeof(int); // 3) 主表:StatusId 是外键,类型必须和 ValueMember 一致 orderTable = new DataTable("Orders"); orderTable.Columns.Add("OrderNo", typeof(string)); orderTable.Columns.Add("StatusId", typeof(int)); orderTable.Rows.Add("SO-001", 1); orderTable.Rows.Add("SO-002", 2); orderTable.Rows.Add("SO-003", DBNull.Value); // 故意留一条空值做验证 grid.DataSource = orderTable; }写完先别急着看效果,重点检查三件事。第一,orderTable里StatusId声明成typeof(int),但第三行塞了DBNull.Value——这在DataTable里是允许的,控件会把它当作空单元格处理,不会报错,这正好用来验证你的代码有没有对空值做兜底。第二,colStatus.DataPropertyName必须写成"StatusId",写成"Status"的话这列永远是空的,因为主表里根本没这个字段。第三,顺序上我习惯先设DataSource,再设DisplayMember和ValueMember,最后补ValueType。虽然反过来一般也能生效,但在列已经挂到Columns集合上、且已经产生了单元格实例的情况下,属性设置顺序确实会影响同步时机,统一按一个顺序写能省很多调试时间。
那段带DBNull.Value的数据很值得保留。跑起来之后,第三行的下拉框应该是空白,点开能选,选完立刻变成正常状态——如果它在加载的时候就弹 DataError,说明你的空值处理路径有问题,这时候去查是ValueType设成了不可空类型,还是数据源本身把 DBNull 转成了 0。
3.3 列挂上表格之后,数据源变更的刷新时机
这是最容易翻车的一段。真实业务里状态字典不是死的,管理员随时可能加一个“已作废”。如果此时直接对statusTable做Clear()再重新Rows.Add(),界面上的下拉项会瞬间归零,而且之前已经选好的单元格开始集体报“值无效”。
原因在于DataTable.Clear()会触发一次结构级重置,BindingSource把这次重置当作“所有行都换了”,DataGridViewComboBoxColumn里已经缓存了旧DataRowView的单元格就全部失配。解决办法有两个方向,我一般优先用第一个:
// 方案一:原地增量更新,不触发整体重置 private void UpsertStatus(int id, string name) { var rows = statusTable.Select($"Id = {id}"); if (rows.Length > 0) rows[0]["Name"] = name; // 改显示名,已选中的行自动跟随 else statusTable.Rows.Add(id, name); // 新增,不影响已有选中项 } // 方案二:整体替换,替换后必须重建绑定并重新赋单元格值 private void ReloadStatus(DataTable newTable) { statusSource.DataSource = newTable; // 换源 statusSource.ResetBindings(false); // 通知所有绑定控件刷新 }方案一的关键是“只改行、不换表”。DataTable的行改动会走ListChanged事件,BindingSource把它翻译成“某项变了”,下拉列的候选集会跟着更新,但单元格的值引用不会断,用户已经选好的数据也不会丢。这在带编辑状态的表格里几乎是唯一稳妥的做法。
方案二适合字典整体重载,比如切换租户、切换账套。但注意ResetBindings(false)后面还得检查一遍单元格值:新表里可能没有旧值对应的 Id,这时候那些单元格会变成“值无效”。我的处理方式是在重载后遍历一次数据行,把找不到对应项的值统一置空,并记一条日志,免得用户提交时才发现数据丢了。
4. 交互与回写:EditingControlShowing 和 CommitEdit 的配合
下拉框在单元格里其实是两个东西:一个是常驻的DataGridViewComboBoxCell,负责显示和值;另一个是进入编辑状态后才临时挂上去的DataGridViewComboBoxEditingControl,它继承自标准ComboBox,才是用户真正点开、上下键选的那个弹出层。很多“我明明改了值却没生效”的问题,根子都是把这两个对象当成一个了。
4.1 通过 EditingControlShowing 拿到真正的 ComboBox
想在用户选择的那一瞬间做点事——比如联动下一列、给出提示、做二次校验——必须拿到编辑控件本身。入口是EditingControlShowing事件:
private void Grid_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e) { // 只有当前编辑的是下拉列才处理,否则每次进编辑都会走一遍 if (grid.CurrentCell == null || grid.CurrentCell.OwningColumn.Name != "colStatus") return; if (e.Control is ComboBox combo) { combo.SelectedIndexChanged -= Combo_SelectedIndexChanged; // 先摘再挂 combo.SelectedIndexChanged += Combo_SelectedIndexChanged; } } private void Combo_SelectedIndexChanged(object sender, EventArgs e) { if (sender is not ComboBox combo || combo.SelectedItem == null) return; // 编辑控件里的选中项是原始对象,取 Id 需要强转回自定义类型 if (combo.SelectedItem is StatusItem item) { // 这里做联动,例如根据状态决定是否允许填写发货时间 bool needShipTime = item.Id == 2; grid.CurrentRow?.Cells["colShipTime"].ReadOnly = !needShipTime; } }combo.SelectedIndexChanged -= ...然后+=这两行不能省。EditingControlShowing在每次进入编辑状态时都会触发,而那个编辑控件实例是复用的,你如果只写+=,编辑十次就订阅了十个处理器,SelectedIndexChanged会被触发十次,联动逻辑跟着执行十次,界面上看不出来,但数据库里可能已经写入了十次日志。这种重复订阅特别隐蔽,而且不加日志根本发现不了。
另外,ComboBox是编辑控件的基类,e.Control里传进来的可能是DataGridViewComboBoxEditingControl,用is ComboBox做一次模式匹配就够了。不要在EditingControlShowing里直接给SelectedItem赋初值——那时候控件刚挂上,SelectedItem还是空的,你一赋值反而把单元格原本的值覆盖掉。
4.2 CurrentCellDirtyStateChanged 加 CommitEdit,让值立刻生效
默认情况下,DataGridView 需要等到焦点离开单元格,才把编辑控件的值提交给单元格。这就导致一个很常见的现象:用户在下拉里选了“已发货”,界面上看着变了,但你挂在CellValueChanged里的联动代码一直不执行,非得再点一下别的单元格才触发。
解决办法是让“脏”状态立刻提交:
private void Grid_CurrentCellDirtyStateChanged(object sender, EventArgs e) { if (grid.IsCurrentCellDirty) { // 只提交值,不结束编辑状态,用户还能继续操作 grid.CommitEdit(DataGridViewDataErrorContexts.Commit); } }CommitEdit的参数决定了它提交什么。传Commit是把编辑值写回单元格,传CommitAndEndEdit(实际是另一套重载)会顺带结束编辑。这里用前者,用户选完之后下拉框仍然开着,可以继续改主意。加上这段之后,CellValueChanged就会在用户点选的瞬间触发,联动、校验、脏标记都能及时响应。
注意事件顺序:CurrentCellDirtyStateChanged先触发,CommitEdit内部再次进入提交流程,然后才轮到CellValueChanged。如果你在CellValueChanged里又去调CommitEdit,会形成一层递归,虽然多数情况下控件自己会拦住,但逻辑上很难读。我一般约定:提交只在这一处做,其他事件里只读不提交。
4.3 级联下拉:按行重新绑定 DataGridViewComboBoxCell
省市区、商品分类这类两级联动,是下拉列的进阶用法,也是最容易踩坑的地方。它的难点在于:上级一变,下级的候选集就得换,但换的是这一行的下级单元格,不是整列。
private void Grid_CellValueChanged(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0) return; if (grid.Columns[e.ColumnIndex].Name != "colProvince") return; var row = grid.Rows[e.RowIndex]; if (row.Cells["colCity"] is not DataGridViewComboBoxCell cityCell) return; int provinceId = 0; if (row.Cells["colProvince"].Value != null && row.Cells["colProvince"].Value != DBNull.Value) provinceId = Convert.ToInt32(row.Cells["colProvince"].Value); // 顺序很重要:先清值,再换源,最后补显示/值字段 cityCell.Value = null; cityCell.DataSource = provinceId == 0 ? null : GetCities(provinceId); cityCell.DisplayMember = "Name"; cityCell.ValueMember = "Id"; } // 模拟取城市,真实项目里换成按省过滤的 DataTable 或 List private DataTable GetCities(int provinceId) { var dt = new DataTable(); dt.Columns.Add("Id", typeof(int)); dt.Columns.Add("Name", typeof(string)); // 实际按 provinceId 查字典 dt.Rows.Add(provinceId * 100 + 1, "示例城市A"); dt.Rows.Add(provinceId * 100 + 2, "示例城市B"); return dt; }三行顺序是拿血泪换来的。如果先换DataSource再清Value,中间会有一瞬间“旧值不在新源里”的状态,控件立刻抛 DataError;如果只换源不设DisplayMember,下拉项显示成System.Data.DataRowView。另外cityCell.DataSource设成null是刻意为之:null表示这个单元格没有候选集,下拉点开是空的,比挂一个空DataTable更干净。
这里还有一个架构上的取舍值得说清楚。上面这种写法,每一行的城市单元格都会有自己的DataTable实例。行数少的时候没问题,一旦上千行,就是上千个小表挂在单元格上,滚动的时候明显发卡,内存也往上走。常见的优化方向是:不再给单元格单独设DataSource,而是整列共用一个大数据源,在EditingControlShowing里根据当前行的省份去过滤编辑控件的Items,用完再还原。代价是逻辑复杂一点,收益是滚动流畅。行数超过几百行,我一般直接走后者。
5. 避坑与排查:值无效、显示空白、刷新后下拉项消失
这一章按“现象 → 原因 → 解决”整理,都是我自己或者同事真实撞过的。排查的时候建议按顺序过,因为很多后面的现象是前面没配好导致的连锁反应。
5.1 值无效与 DataError 弹窗
现象一:加载表单直接弹“DataGridViewComboBoxCell 的值无效”,点确定之后单元格变成空白。
原因基本只有两个:单元格的值不在候选集里,或者值的类型和ValueType对不上。前者常见于主表里存的是历史状态码,字典表里早就删掉了;后者常见于主表字段是string而ValueMember是int,控件拿"1"去比对整数1,怎么比都不相等。
解决分两步。第一步把ValueType和主表字段类型对齐,这一点不能妥协。第二步对历史数据做容错,在绑定之前扫一遍主表,把不在字典里的值统一替换成DBNull.Value或者默认状态,并记日志。不要指望在DataError事件里把错误吞掉就完事:
private void Grid_DataError(object sender, DataGridViewDataErrorEventArgs e) { // 吞掉异常,单元格保持原值,避免弹窗打断用户 e.ThrowException = false; e.Cancel = false; // 但一定要留痕,否则数据错了没人知道 var col = grid.Columns[e.ColumnIndex].Name; System.Diagnostics.Debug.WriteLine($"列 {col} 第 {e.RowIndex} 行数据格式不匹配"); }DataError里把ThrowException设成 false,只是让弹窗消失,值该错还是错。生产代码里我会把这个事件当成监控入口,出现一次记一次,而不是当成异常处理器。
现象二:下拉里选完之后,单元格显示的还是旧值,甚至选完直接弹回原状态。
原因是没有提交编辑,或者提交了但值被判定为无效又回滚了。前者加CurrentCellDirtyStateChanged+CommitEdit就能解决;后者要回头看ValueMember和ValueType是否一致。还有一种情况是主表用了DataView做过滤,单元格的DataBoundItem和DataRowView关联断了,写回时找不到目标行,表现也是“选完就跳回去”。
5.2 显示空白、刷新丢项与性能问题
现象三:下拉列表点开全是空白行,或者显示System.Data.DataRowView。
原因是DisplayMember没设,或者设的值和对象的属性名对不上。DataTable做数据源时,下拉项是DataRowView,它的属性名就是列名,大小写敏感;自定义类则必须是公开属性,字段不行。我见过有同事写DisplayMember = "name"而列名是"Name",找了两个小时。
现象四:字典刷新一次之后,所有下拉的候选项消失,点开是空的。
原因是刷新方式不对,前面 3.3 提过。直接statusTable.Clear()或者重新statusSource.DataSource = 新表而没有重置绑定,就会这样。解决办法是原地改行,或者在换源后调ResetBindings(false),并且重新给已有的单元格补值。判断方法很简单:刷新之后点开任意一个下拉,如果里面一条都没有,问题在数据源;如果里面有项但当前单元格是空白的,问题在单元格的值。
现象五:表格超过几百行之后滚动明显卡顿,切换行有延迟。
原因多半是级联下拉给每个单元格都挂了独立的数据源,或者CellFormatting里做了耗时计算。前者按 4.3 末尾说的改成共享数据源 + 编辑时过滤;后者要检查CellFormatting里有没有频繁查库或者反序列化 JSON,那些操作必须挪到缓存层。还有一个容易被忽略的点:Rows.Add逐行追加时,控件会每加一行就重排一次,改用grid.Rows.AddRange()或者先SuspendLayout()能明显改善。
现象六:AutoGenerateColumns忘了关,表格里出现两套列。
现象是同一个字段出现两列,一列是下拉,一列是文本框,用户往往点中文本框那列,输入自由文本。原因是设了DataSource却没关自动生成,控件按数据源又建了一套。解决就是把AutoGenerateColumns = false写死在构造函数里,并且所有列都显式Columns.Add。
6. 二次开发:把下拉列封装成可复用的列工厂
一个项目里出现七八个下拉列是很正常的事,如果每一列都复制一遍DataSource / DisplayMember / ValueMember / ValueType / DisplayStyle这五行,出错概率会随着列数线性上升。我的做法是抽一个扩展方法,把这套固定顺序封进去:
public static class ComboColumnExtensions { /// <summary> /// 把列绑定到一个字典数据源,并锁定显示字段、值字段和值类型 /// </summary> public static DataGridViewComboBoxColumn BindLookup( this DataGridViewComboBoxColumn column, object dataSource, // DataTable / List<T> / BindingSource 都行 string displayMember, string valueMember, Type valueType) { column.DataSource = dataSource; column.DisplayMember = displayMember; column.ValueMember = valueMember; column.ValueType = valueType; column.DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton; column.FlatStyle = FlatStyle.Flat; column.DisplayMember = displayMember; // 双保险,防止数据源延迟绑定 return column; } }调用处就变成一行:
colStatus.BindLookup(statusSource, "Name", "Id", typeof(int));封装之后,验证方式也要跟着固定下来,否则只是把散落的错误集中到了一处。我一般按这个清单过一遍:先用一条不在字典里的历史值测加载,看是否被容错处理;再用一条DBNull测空值路径;然后把字典里某一项改名,确认已选中的行自动跟随;最后加五百行数据滚一遍,看有没有卡顿。四项都过,这一列才算能用。
再往上一层,可以把“列定义”做成配置。真实项目里字典表名、显示字段、值字段基本都是稳定的,用一张配置表驱动列的生成,新加一个下拉列只需要加一行配置,不用改窗体代码:
| 配置项 | 示例值 | 说明 |
|---|---|---|
| 列名 | colStatus | DataGridView 内部标识,事件里靠它判断 |
| 表头 | 订单状态 | 显示给用户的文字 |
| 数据字段 | StatusId | 对应主表字段,必填 |
| 字典来源 | Status | 字典表名或缓存 key |
| 显示字段 | Name | 下拉里看到的列 |
| 值字段 | Id | 写回数据源的列 |
| 值类型 | int | 必须和主表字段一致 |
这套东西搭起来之后,新增一个下拉列从“复制粘贴加改错”变成“配置一行”,维护成本降一大截。我吃过最亏的一次,是一个表单里有十一个下拉列,其中三个的值类型写成了string,上线之后每天都有几条数据因为类型不匹配被静默丢弃,日志里只有零星几行 Debug 输出,查了三天才定位到。
从那以后我每次交出带下拉列的界面,都会强制走一遍“历史值 + 空值 + 改名 + 大数据量”这四项验证,不管多简单的表单都不跳过。希望帮到你。
本文还有配套的精品资源,点击获取