☰
C# ListView绑定数据库:从ADO.NET到界面刷新的完整指南
2026/10/9 6:56:51 网站建设 项目流程

简介:这是一份面向C#窗体程序初学者的编程实例,演示如何用列表视图控件展示数据库中的记录,适用于数据管理界面、信息查询窗体等场景,核心解决数据库内容到界面列表呈现的常见问题,也可作为学习数据绑定技术的入门范例。压缩包共包含四十个文件,总体积七十八KB,以C#代码文件为主体,附有解决方案文件、界面资源文件、生成好的可执行程序及备份文件,结构规整且分类明确,用开发环境打开解决方案即可运行调试。目前已有八百二十五人学习,具备较好的参考价值。代码演示了从数据库连接、执行查询语句、利用数据适配器填充数据集,到将数据绑定到列表控件、设置列和子项的完整流程,同时含有数据库文件和备份目录,方便对照练习;读者可借此掌握列表控件数据绑定、列配置及事件处理等实用技能,适合初级开发者动手实践。

1. ListView 显示数据库数据:这份 C# 源码包能帮你省掉哪些事

ListView 控件在 C# WinForms 开发里是数据展示的标配,但能把数据库表干净利落地映射到列表视图、再处理好刷新与事件响应的完整源码并不多。这份 Case05_4 工程就是干这个的:用 ADO.NET 查出数据、填充 DataTable、逐行写入 ListView 的 Items 与 SubItems,整套流程打包在一个解决方案里。适合刚入门 C# 数据库开发的人跑通流程,也适合做 c#上位机、数据开发、数据管理工具的人直接抄数据访问层。

值得关注的是这个包的工程组织:DataClass 数据访问、MyMeans.cs 工具方法、Form1 界面逻辑分层清晰。拆完你能搞明白连接字符串放哪、SqlDataAdapter 与 DataReader 怎么选、Columns 如何与字段对齐、数据更新后怎么刷新不闪。下面从工程结构讲起,最后落到参数和坑上。

2. 拆解 Case05_4 工程:从 sln 布局到数据访问层的代码组织

2.1 解决方案文件布局:sln、csproj 与 Backup 目录各管什么

拿到压缩包先别急着双击 sln,先把目录结构看一遍。解压后你会看到几个固定角色:Case05_4.sln 是解决方案文件,Visual Studio 靠它决定加载哪些项目、用哪套生成配置;Case05_4 是主项目文件夹,里面是 Form1.cs、Program.cs、MyMeans.cs、DataClass 以及 Properties 资源目录;bin/Debug 下是编译好的 exe,不想重新编译的话可以直接跑起来看效果;Backup 是作者留的备份目录。我习惯先用文本编辑器打开 sln 看一眼项目引用路径——如果提交时带了绝对路径,别人换目录打开就会报"未能找到项目"。这个包里的路径是相对写法,换机器直接打开问题不大。

Case05_4.csproj 是项目文件,声明了编译目标、引用程序集和包含的源文件。做数据开发时我会重点看<Reference>节点:这个项目走 ADO.NET,引用 System.Data、System.Xml 是意料之中的;如果引了 System.Data.SqlClient 的 NuGet 包,说明目标是 SQL Server,如果换成了 MySql.Data,那连的就是 MySQL。Form1.Designer.cs 是窗体设计器自动生成的代码,listView1 的声明、属性初始化都在这里,不建议手改——你在设计器里拖一个控件,所有改动都会自动落到这个文件里。Program.cs 是入口,Main 方法里 EnableVisualStyles 之后 Run(new Form1()),一般不用动。

Backup 目录值得多说两句。里面放的是同一套代码的旧版本快照,很多人拿到就删,我建议留着。当主工程被改得跑不起来、或者你想看作者早期的写法时,这份旧代码就是后悔药。我拆这类示例包的标准流程是:先拿 Backup 和主工程的 Form1.cs 做一次比对,diff 一下就能看出作者改了什么逻辑,通常这才是整个包里最有信息量的部分。

提示:如果 sln 打开报错,优先检查 Backup 目录里那份 csproj 的项目类型 GUID 与主工程是否一致,不一致会导致加载失败。

2.2 DataClass 与 MyMeans.cs:数据访问层到底该放哪些方法

这个工程里 DataClass 文件夹加 MyMeans.cs,是典型的"数据访问类 + 工具类"拆法。示例项目的常见做法是:DataClass 里放一个公共数据类,提供 GetDataTable、ExecuteNonQuery、GetConnection 这类方法;MyMeans.cs 放与数据库无关的小工具,比如字符串判空、ListView 清空、提示框封装。这么拆的逻辑很简单:界面层只关心"给我一张 DataTable",不关心"你连的是哪台库、用哪个驱动",数据访问细节被挡在界面之外。等你要换数据库或者改连接方式时,只动 DataClass 一个文件,Form 层一行都不改。

连接字符串的存放位置是个容易被忽略的点。示例工程里通常直接写在方法开头:

public static string ConnString = "Server=.;Database=TestDB;User ID=sa;Password=123456;";

参数说明:Server=. 表示本机默认实例,也可以写 localhost 或机器名;Database 指定初始库;User ID 和 Password 是 SQL Server 身份验证的账号密码。如果数据库是 Windows 身份验证,把连接串换成Server=.;Database=TestDB;Integrated Security=True;即可。这个写法的优点是双击就能跑,缺点是换环境必须改代码重新编译,漏改一处就报"用户 sa 登录失败"。生产中我一般把连接串挪到 App.config 的 connectionStrings 节点里,用 ConfigurationManager 读,当然后续还可以上加密,但示例包这样直接写不算错,毕竟它的目标是让你先跑通。

再理清 ADO.NET 三个核心对象的职责:SqlConnection 管建连和断连,SqlCommand 管 SQL 语句和参数,SqlDataAdapter 管把查询结果搬进内存 DataTable。三个对象串起来的典型代码如下:

using (SqlConnection conn = new SqlConnection(ConnString)) { conn.Open(); string sql = "SELECT Id, Name, Age, Department FROM Student"; SqlCommand cmd = new SqlCommand(sql, conn); SqlDataAdapter adapter = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); adapter.Fill(dt); return dt; }

逻辑说明:using 保证连接用完后自动释放,Open 之后创建命令和数据适配器,Fill 是核心动作——它内部自己完成数据读取和类型映射,不需要你手动遍历 DataReader 逐行拼。返回的 DataTable 就是给 ListView 绑定的数据源。

参数说明:SqlDataAdapter 构造时直接传入 SqlCommand,Fill 方法会自动打开连接、执行查询、填充表、关闭连接。这里我额外调了 conn.Open(),虽然 Fill 遇到关闭的连接会自己打开,但显式 Open 能提前暴露连接字符串配置错误,否则错误被延迟到 Fill 那一步才抛出,排查时多绕一圈。返回的 DataTable 里 Id 列虽然不显示,但后续要用主键做增删改查,所以 SELECT 里一定要带。

2.3 ListView 没有 DataSource 属性:两种填充方式的正确姿势

这里必须先说一个血泪经验:网上大量教程写listView.DataSource = dataTable;,但 ListView 控件根本没有 DataSource 和 DataMember 属性,那是 DataGridView 的,编译直接报"ListView 不包含 DataSource 的定义"。搜索 C# ListView 绑定数据库,十条结果里至少有两条给你这个错误代码,新手照着敲不通,还以为是环境没配好。这个 Case05_4 工程用的是标准做法——逐行构造 ListViewItem:

private void BindData(DataTable dt) { listView1.Items.Clear(); foreach (DataRow row in dt.Rows) { ListViewItem item = new ListViewItem(row["Name"].ToString()); item.SubItems.Add(row["Age"].ToString()); item.SubItems.Add(row["Department"].ToString()); item.Tag = row["Id"].ToString(); listView1.Items.Add(item); } }

逻辑说明:ListViewItem 的构造参数 Text 对应第一列,SubItems.Add 依次对应后续列,顺序必须和 Columns 的添加顺序一致,否则数据会整行错位。Tag 里塞主键 Id 是个好习惯——用户选中某行时,你可以用 item.Tag 拿到主键去做后续的数据库操作,而不是靠选中行的显示文本反查,既慢又不可靠。

参数说明:row["Name"] 按列名取值,比 row[0] 按索引取值可读性好得多,表结构调整了列顺序代码也不会挂。数据库字段是 NULL 时,ToString() 返回空字符串,界面静默显示空白,不会报错。DateTime 字段建议提前格式化:row["CreateTime"] == DBNull.Value ? "" : Convert.ToDateTime(row["CreateTime"]).ToString("yyyy-MM-dd"),否则默认带时分秒,列宽撑不住就是一串省略号,观感非常差。

2.4 View 属性与列初始化:为什么必须是 Details 模式

ListView 有 LargeIcon、SmallIcon、List、Details 四种主要视图模式,显示数据库表结构只能用 Details——只有它支持多列和列头。工程里 Form1.Designer.cs 中会看到类似这样的初始化代码:

this.listView1.View = View.Details; this.listView1.FullRowSelect = true; this.listView1.GridLines = true; this.listView1.Columns.Add("姓名", 120); this.listView1.Columns.Add("年龄", 60); this.listView1.Columns.Add("部门", 100);

逻辑说明:View.Details 开启详细信息视图;FullRowSelect 让整行高亮而不是只高亮第一个子项;GridLines 显示网格线,数据密集时视觉上更好定位。Columns.Add 第一个参数是列头文本,第二个是列宽像素值。

参数说明:列宽 120/60/100 是经验值,中文两字大概需要 40 像素左右,建议按最长内容估算后加 20% 余量。如果列数多、总宽超过 ListView 宽度,把其中一列宽度设为 -2 表示自动按内容调整,设为 -1 表示按列头文本调整。工程里如果只设了列头没设宽度,列宽默认 60,长字段会显示不全,后面跟一串省略号,这也是新手最常见的观感问题之一。

3. 从数据库到 ListView 的完整实现:连接、查询、列映射与刷新

3.1 先定查询语句:SELECT 字段顺序就是列表列顺序

整个链路的起点不是 ListView,是那条 SQL。很多人上来就SELECT *,省事但坑多:表结构一改,ListView 列就错位;多查了不需要的大字段,内存和渲染都吃亏。我一般遵循两个原则:只查要显示的字段,外加主键;把要显示的字段排在前面。这样 BindData 里 new ListViewItem 的顺序天然和 SELECT 顺序一致,代码可读性也好。

string sql = "SELECT Id, Name, Age, Department FROM Student WHERE Age >= 18 ORDER BY Age DESC";

ORDER BY 在数据库里做排序通常比内存排序高效,尤其是数据量上来之后。查询里能用 WHERE 过滤掉的,不要在内存里再筛一遍——数据库擅长过滤,C# 代码里循环判断只会拖慢界面。如果你的查询条件来自用户输入,比如按年龄段筛选,那 WHERE 子句里的值必须走参数化,后面会细说。

3.2 填充 DataTable:为什么这个场景不用 DataReader

上一章提到三个对象,这里展开说选型。DataReader 是流式读取,一次只读一行,内存占用小,但它是只进只读的,读的过程中连接必须保持打开。ListView 绑定场景下,数据要么全量进内存要么分页,流式读并没有优势,反而因为连接占用时间长,容易把连接池撑满。SqlDataAdapter.Fill 是一次性把结果集搬进内存,Fill 返回后连接就可以释放,界面怎么刷新都不再依赖数据库连接,体验完全不同。

public DataTable GetDataTable(string sql) { using (SqlConnection conn = new SqlConnection(ConnString)) using (SqlCommand cmd = new SqlCommand(sql, conn)) using (SqlDataAdapter adapter = new SqlDataAdapter(cmd)) { DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } }

逻辑说明:三个 using 嵌套,连接、命令、适配器全部自动释放,Fill 之后 DataTable 是独立内存对象,连接关闭不影响它。这个方法放在 DataClass 里,Form 层只需要DataTable dt = dataClass.GetDataTable(sql);一行就能拿到数据。

参数说明:如果查询带参数,千万别用字符串拼接。正确姿势是:

cmd.CommandText = "SELECT Id, Name, Age FROM Student WHERE Age > @minAge"; cmd.Parameters.Add("@minAge", SqlDbType.Int).Value = 20;

参数化既防 SQL 注入,又避免数字在拼接后变成字符串导致索引失效。AddWithValue 在 SQL Server 下存在隐式类型转换的坑,生产环境我更倾向用上面这种明确指定 SqlDbType 的写法,虽然多敲几个字,但能避开 NVARCHAR 隐式转换导致的全表扫描。

3.3 列映射与项填充:把 DataTable 的行列搬成 ListView 的 Items 与 SubItems

绑定这一步要严格按"Columns 顺序 = ListViewItem.Text + SubItems 顺序 = DataTable 列顺序"来对齐。我见过太多翻车案例:列头叫"姓名",第一列 SubItem 填的却是学号,因为作者写死 row[1] 没写 row["Name"]。这种错位编译不报错,运行也不报错,只有肉眼对着屏幕才能发现,属于最阴间的 bug 之一。

private void LoadData() { try { string sql = "SELECT Id, Name, Age, Department FROM Student"; DataTable dt = dataClass.GetDataTable(sql); listView1.BeginUpdate(); BindData(dt); labelStatus.Text = "共 " + dt.Rows.Count + " 条记录"; } catch (Exception ex) { MessageBox.Show(ex.Message); } finally { listView1.EndUpdate(); } }

逻辑说明:BeginUpdate 和 EndUpdate 成对出现,绑定期间暂停界面重绘,几千行数据一次性刷进去不闪不卡。状态栏显示记录数是数据开发里很加分的细节,用户能直观知道数据量级。try-catch 包住整个流程,连接失败、SQL 语法错误都会弹窗提示而不是静默崩溃。

参数说明:BeginUpdate 之后必须保证 EndUpdate 被调用,所以 EndUpdate 放在 finally 里是标准姿势。如果忘了 EndUpdate,ListView 会一直处于不重绘状态,界面看起来像卡死,实际是绘制被挂起。另外注意 BindData 里用 row["Name"] 取列,前提是 DataTable 里真的有这一列,所以 SELECT 的字段名和代码里的列名要保持拼写一致,大小写不敏感但拼错就报错。

3.4 刷新策略:全量重绑与局部更新的取舍

数据变更后最简单粗暴的做法是重新执行一遍 LoadData——查库、Fill、Clear、Add,全程可能几百毫秒。数据量小完全没问题,但到了上万行,每次刷新都全量重绑就会明显卡顿。这个工程的定位是基础示例,全量重绑不算错,但你要知道还有别的路。

private void btnRefresh_Click(object sender, EventArgs e) { LoadData(); // 全量重绑,简单可靠 listView1.Items[0]?.EnsureVisible(); // 刷新后滚回第一行 }

局部更新的做法是按需操作单行:用户双击某行改完数据后,只更新该行对应的 ListViewItem 的 SubItems,不动其他行。做法是把主键存在 item.Tag 里,更新完数据库后,遍历 listView1.Items 找到 Tag 匹配的那一项,直接改它的 SubItems 文本。这个方案避免了 Clear 再全量 Add 的闪烁,但要注意:全量重绑时如果用 Items.Clear(),之前存的 Tag 也会全丢,所以要么不 Clear,要么重新从 DataTable 里取 Id。

性能优化的底线是:数据量小于一千行时,别为了"优化"引入局部更新的复杂度,全量重绑最不容易出错。等到真出现卡顿,优先排查是不是查询本身慢,而不是绑定代码——很多时候慢在 SQL 上,绑定只是背锅的。

4. ListView 绑定数据库的常见问题与避坑指南

4.1 编译报错:找不到类型或命名空间 SqlConnection

现象:照着教程敲SqlConnection conn = new SqlConnection(...),编译直接报"找不到类型或命名空间 SqlConnection"。

原因:工程没有引用 System.Data.SqlClient,要么缺 using 语句,要么目标框架不对。Sysytem.Data.SqlClient 在 .NET Framework 里是内置的,但 .NET Core 3.0 之后相关 API 挪到了 NuGet 包 Microsoft.Data.SqlClient,直接用旧命名空间可能编不过。

解决:先在代码顶部加using System.Data.SqlClient;,还不行就打开 NuGet 包管理器安装 Microsoft.Data.SqlClient,然后改用Microsoft.Data.SqlClient命名空间。注意这个 Case05_4 工程的 sln 是 .NET Framework 版本的,双击打开时如果提示需要目标框架组件,去项目属性里把目标框架改成本机已安装的对应版本,重新生成一次再跑。

4.2 运行报错:用户 sa 登录失败或无法连接到服务器

现象:程序启动后弹窗"用户 sa 登录失败",或者"在建立与服务器的连接时出错"。

原因:连接字符串里写死的账号密码和本机 SQL Server 配置不匹配。最常见的是 SQL Server 没开混合身份验证模式,或者 sa 账号被禁用,也可能实例名根本不对——本机装的可能是 LocalDB 而不是默认实例。

解决:先用 SQL Server Management Studio 以 Windows 身份登录,确认实例名;然后执行ALTER LOGIN sa WITH PASSWORD='新密码'; ALTER LOGIN sa ENABLE;,再右键服务器属性 → 安全性 → 选"SQL Server 和 Windows 身份验证模式",重启 SQL 服务。如果你连的是 LocalDB,连接串要写成Server=(localdb)\MSSQLLocalDB;Database=TestDB;Integrated Security=True;,和普通实例完全不同。

4.3 界面卡死:加载大数据量时窗体白屏无响应

现象:数据量到几千行,点查询按钮后整个窗体变白,标题栏显示"无响应",过几秒才恢复。

原因:查询和绑定都在 UI 线程同步执行,数据库响应慢或 Fill 数据量大时,UI 线程被阻塞,消息循环停转。这不是 ListView 的问题,是同步调用的固有问题。

解决:数据量小可以直接接受;数据量大就上 async:

private async void btnLoad_Click(object sender, EventArgs e) { DataTable dt = null; dt = await Task.Run(() => dataClass.GetDataTable(sql)); listView1.BeginUpdate(); BindData(dt); listView1.EndUpdate(); }

逻辑说明:Task.Run 把查询丢到后台线程,await 返回之后继续在 UI 线程执行绑定。注意 ListView 是主线程控件,绑定操作必须在 UI 线程,跨线程操作会抛 InvalidOperationException。这也顺带涉及 C# 线程的一个基本认知:后台线程干重活,UI 线程只负责刷新,别让界面线程去等数据库。

4.4 数据错位:列头和内容对不上

现象:列头顺序正常,但每一行的内容看起来整体右移一位或错位,第一列显示的是第二列的字段。

原因:最常见是 ListViewItem 构造参数和 SubItems 的顺序与 Columns 添加顺序不一致。比如 Columns 先加了"姓名、年龄、部门",但代码里new ListViewItem(row["Age"])把年龄放进了第一列,后面 SubItems 依次错位。

解决:绑定前先写一张对照表:第 0 列 = Text = Name,第 1 列 = SubItems[0] = Age,第 2 列 = SubItems[1] = Department,然后严格按这个顺序写代码。调试时可以在 BindData 里临时把每个 item 的 SubItems 文本打印到控制台,肉眼核对一轮。另一个隐蔽原因是 DataTable 的列顺序和 SELECT 不一致,SELECT * 最容易触发,所以我一直强调不要用 SELECT *,显式列出字段名,顺序可控。

4.5 刷新后滚动条复位且界面闪烁

现象:刷新后滚动位置丢失回到第一行,Clear 再 Add 时界面明显闪烁。

原因:Items.Clear() 会重置布局和滚动位置,这是默认行为;闪烁则是因为没有用 BeginUpdate/EndUpdate 包裹重绑过程。

解决:绑定前调 BeginUpdate,绑定后 EndUpdate,闪烁基本消失;滚动位置用listView1.TopItem保存,刷新后listView1.TopItem = savedTopItem;恢复。注意 TopItem 是只读属性,只能在绑定完成且有足够行时赋值,数据量小于一屏时它可能是 null,赋值前要判空。这也是为什么前面刷新示例里加了Items[0]?.EnsureVisible()——至少确保能回到顶部附近,而不是停留在空白区域。

5. 从"能显示"到"好用":排序、虚拟模式与交互细节打磨

5.1 列头点击排序:用 ListViewItemSorter 而不是重新查库

数据到几百行,用户自然会点列头排序。最省事但不是最优的做法是改 SQL 加 ORDER BY 再全量重绑,慢且闪。标准做法是挂 ListViewItemSorter 做内存排序:Compare 方法里根据点击的列索引比较 SubItems 文本,数字列先 int.Parse 再比较,否则 "10" 会排在 "9" 前面。ListView.Sort() 不触发数据库查询,几千行数据毫秒级完成。

private void listView1_ColumnClick(object sender, ColumnClickEventArgs e) { if (listView1.Sorting == SortOrder.Ascending) listView1.Sorting = SortOrder.Descending; else listView1.Sorting = SortOrder.Ascending; listView1.ListViewItemSorter = new ListViewItemComparer(e.Column, listView1.Sorting); listView1.Sort(); }

参数说明:ColumnClickEventArgs.Column 是点击的列索引,与 SubItems 索引一一对应;第一次点击时 Sorting 为 None,会切成 Ascending,再点切回 Descending。ListViewItemComparer 需要自己实现 IComparer,Compare 方法里拿 x 和 y 两个 ListViewItem,各自取 SubItems[col].Text 再比较。

5.2 上万数据量:用 VirtualMode 而不是硬加

超过一万行时,逐行 Add 的瓶颈在创建对象和重绘。虚拟模式开启listView1.VirtualMode = true;并设置 VirtualListSize,ListView 只在需要绘制某一行时触发 RetrieveVirtualItem 事件,你按 itemIndex 从 DataTable 取行构造 ListViewItem 即可。代价是排序、编辑、复选框都要自己实现。数据量小于五千行不建议上虚拟模式,复杂度换不来可见收益。

5.3 双击行拿主键:把 Tag 用起来

交互上最常见的需求是双击一行弹出编辑窗,保存后刷新。前面把主键塞进 item.Tag 就是为这一步做准备:

private void listView1_DoubleClick(object sender, EventArgs e) { if (listView1.SelectedItems.Count == 0) return; string id = listView1.SelectedItems[0].Tag.ToString(); // 用 id 查询明细或打开编辑窗体 }

从那以后,我每次拿这类示例包落地生产,都会强制走一遍这套流程:先看 sln 和 csproj 确认框架与引用,再核对连接字符串,然后把绑定逻辑拆成 GetDataTable 和 BindData 两个独立方法,最后检查每个事件处理里有没有空引用。这套流程帮我挡掉过不少"能编译、能跑、但数据就是不对"的玄学问题。这份源码包把从数据库到界面的朴素路径完整走通了,你在这基础上加排序、虚拟模式、增删改查,每一步都有迹可循。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询