简介:面向使用 Visual Studio 2015 和 C# 开发 Windows 数据库应用的初学者与进阶学习者,这套配套项目资料围绕桌面程序与数据库交互展开,覆盖从开发环境配置到完整项目实训的学习路径。压缩包共 3423 个文件、约 95.9MB,以 C# 源码、资源与窗体文件、动态库组件、配置文件、可执行程序及报表文件为主,并配有课件、实训说明和教材案例,便于按模块查阅与运行。已有 1082 人学习。内容涉及数据连接、增删改查、离线数据集管理,也延伸到多表查询、存储过程、触发器、视图、索引和数据库安全等主题;课件可辅助梳理关系模型、SQL 基础、事务处理与性能优化,实训与附录项目则提供可对照的案例结构。适合课程设计、毕业设计、课堂教学与自学演练,也可作为数据库课程配套实验参考。
1. 这份 C# 数据库项目,解决的是「会拖控件、不会连库」的尴尬
刚入行那两年,我见过太多人的 C# 水平卡在同一个位置:拖控件、改属性、双击按钮弹一个 MessageBox 都顺手,一到「把数据存进数据库,再按条件读出来」就整段垮掉。这份 Visual Studio 2015(C#) Windows数据库项目开发.zip 走的不是零散教学片段那条路,它给的是一个能编译、能登录、能增删改查的完整 Windows 桌面数据库项目:窗体、业务层、数据访问层、建库脚本全在同一套解决方案里,改对连接串就能跑。它适合三类人——课程设计要交差的学生、刚从 Web 后端转桌面端的开发者、想拿一份能改的骨架接内部管理系统的自由职业者。
2. 技术栈与目录结构:WinForms 三层架构是怎么摆开的
先说明一点:这是一个传统 WinForms 桌面项目,不是 WPF,也不是 .NET Core 那一套。所以它的技术栈基本上被时间钉死了——Visual Studio 2015 建解决方案,目标框架是 .NET Framework 4.5 或 4.6,界面用 WinForms,数据访问用原生 ADO.NET(System.Data.SqlClient),数据库是本地实例上的关系型库。这套组合在今天看起来有点「老」,但它有一个 Web 项目比不了的好处:所有东西都在一个进程里,断点能从按钮点击一路跟到 SQL 执行,调试成本极低。
我一般把这类项目理解为「一张三层结构图 + 一堆窗体代码」。三层不是学院派的说辞,它是实打实的引用约束:界面层不许出现SqlConnection,数据访问层不许出现MessageBox。这份项目能直接拿来二次开发,恰恰是因为它把这条线划清楚了。
2.1 解决方案里到底有几个项目
把压缩包解开,第一件事不是急着双击.sln,而是先看目录。一个结构正常的项目,解决方案里通常是 6 个左右的工程,而不是一个大杂烩:
MySchoolSystem.sln ├─ MySchoolSystem.UI Windows 窗体,设为启动项目 │ ├─ Forms/ 登录、主界面、各业务窗体 │ ├─ Program.cs 入口,负责全局异常兜底 │ └─ App.config 连接串就放在这里 ├─ MySchoolSystem.BLL 业务规则、参数校验、事务边界 ├─ MySchoolSystem.DAL SQL 语句与参数,只跟数据库打交道 ├─ MySchoolSystem.Model 实体类,不引用任何其他项目 ├─ MySchoolSystem.Common 加密、日志、字符串工具 └─ DB/ └─ SchoolDb.sql 建库建表 + 初始账号脚本看到这个结构,脑子里应该立刻出现两条判断:Model是最底层,谁都可以引用它、它谁也不引用;UI是最上层,只能被启动,不应该被别的项目引用。这两条如果反了,比如DAL反过来引用了UI里的某个类,那这个项目就是「假三层」,改起来会很痛苦。我在动手改之前,习惯先在 VS 里右键每个项目看一眼「引用」节点,确认依赖方向是单向的 UI → BLL → DAL → Model,这条链路一旦确认,后面加功能就不会乱。
还要注意DB目录。它不是工程,只是随包附带的脚本目录,但它是整个项目能否跑起来的关键——库没建好,程序打开就是一句「连接失败」,什么都不用谈。
2.2 三层之间的引用关系与数据流向
数据流向其实是双向的,但两条方向上传的东西不一样,这点很多人绕不过来。
往下走(UI → BLL → DAL):传的是普通参数,比如int classId、string keyword、一个实体对象。往下走的时候不允许传DataTable,也不允许传SqlParameter,因为这些类型带着「数据库的味道」往上冒了。往上走(DAL → BLL → UI):传的是实体集合IList<Student>或者DataTable。我个人的习惯是查询列表统一返回实体集合,只有报表类的多表联查才允许直接把DataTable丢给DataGridView,因为那种场景再定义十个实体类纯属自找麻烦。
下面这张表是我判断一个 C# 数据库项目值不值得接手时最常看的东西,你也可以拿它对照手上的项目:
| 层 | 允许出现 | 绝对不该出现 |
|---|---|---|
| UI | 控件操作、事件处理、简单格式转换 | SQL 字符串、SqlConnection |
| BLL | 校验、事务、多表组合逻辑 | 控件类型、MessageBox |
| DAL | SQL、参数、SqlDataReader | 业务判断、界面提示 |
| Model | 属性、少量计算属性 | 任何数据库或界面引用 |
表格里第三行是重灾区。很多人写 DAL 的时候顺手在里面MessageBox.Show("删除失败"),当时很方便,等你哪天想把项目改成 Web 服务或者加个后台定时任务,这段代码就变成了必须拆掉的硬伤——控制台程序弹不出窗体,直接抛异常。
2.3 数据库脚本、配置文件与资源目录
配置文件App.config是新手最容易忽略、老手最容易踩的一个点。连接串写在这里而不是硬编码在SqlHelper里,好处是部署时改一个文本文件就能换库,坏处是很多人不知道编译之后它叫项目名.exe.config,藏在bin\Debug里,改错了地方。我在排查这类问题时,第一个动作永远是去bin\Debug下确认那个.config的时间和内容,而不是看源码里的App.config。
<?xml version="1.0" encoding="utf-8"?> <configuration> <connectionStrings> <!-- name 必须和代码里 ConnectionStrings["SchoolDb"] 的键完全一致 --> <add name="SchoolDb" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=SchoolDb;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> <appSettings> <!-- 密码加盐用的固定串,换项目时记得一起换掉 --> <add key="PwdSalt" value="s4lt_2024" /> </appSettings> </configuration>这段配置里三个参数值得说清楚:Data Source是实例名,.\SQLEXPRESS表示本机的默认 SQLEXPRESS 实例,写成(LocalDB)\MSSQLLocalDB就是 LocalDB 版本;Initial Catalog是数据库名,必须和脚本里CREATE DATABASE的名字一致,差一个字母就是「无法打开登录所请求的数据库」;Integrated Security=True表示走 Windows 身份验证,不需要账号密码,这也是本地开发最省事的做法,如果换成 SQL 账号认证,就要写User Id=xx;Password=xx。
DB目录下的.sql脚本一般包含建库、建表、主外键、初始管理员账号四部分。跑脚本的顺序很重要:先建库再USE,再建表,最后插初始数据。如果顺序反了,USE一个不存在的库会直接报错。
3. 核心链路拆解:一条 SELECT 从 DataGridView 走到 SQL Server
这一章是整份项目里最值得逐行读的部分。WinForms 数据库项目的代码量看着吓人,但真正重复的套路就那么几套:查列表、按条件搜、增删改、事务提交。把这四套看懂,剩下的窗体都是复制粘贴改名字。
3.1 实体层:先用一个类把表结构钉死
实体类是整个项目里最没有技术含量、却最不该省的一层。它的作用不是「面向对象」,而是让你在编译期就能发现字段名打错。
// Model/Student.cs public class Student { public int Id { get; set; } // 姓名用 string 而不是 char[],长度约束交给数据库的 NVARCHAR(32) public string Name { get; set; } public string Gender { get; set; } // 生日允许为空,所以用可空 DateTime,别用 DateTime.MinValue 当哨兵值 public DateTime? Birthday { get; set; } public int ClassId { get; set; } }Birthday用DateTime?是个细节但很关键。用DateTime.MinValue或者DateTime.Now当「空值」的哨兵,在界面上显示出来就是0001-01-01或者一个莫名其妙的今天日期,用户看不懂,你自己半年后也看不懂。可空类型在 ADO.NET 里对应DBNull.Value,读写的时候多做一次判断,逻辑就干净了。
至于要不要写 DTO、要不要上 ORM,我的看法是:这种规模的课程设计或中小型管理系统,实体类 + 手写 ADO.NET 就够,引一个 ORM 进来反而增加了「框架配置」这类跟业务无关的排查成本。真要上 ORM,等你遇到第三张多对多表的时候再说。
3.2 数据访问层:参数化 SQL 与连接池
先看一个几乎所有这类项目都会有的公共类。它的存在意义是把「打开连接、执行、关闭」这三件事收敛到一处,避免每个方法都写一遍using。
// DAL/SqlHelper.cs public static class SqlHelper { // 连接串只读一次,改库只改 App.config private static readonly string ConnStr = ConfigurationManager.ConnectionStrings["SchoolDb"].ConnectionString; // 给需要事务的调用方使用,事务必须和连接绑在一起 public static SqlConnection OpenConnection() { var conn = new SqlConnection(ConnStr); conn.Open(); return conn; // 由调用方负责 Dispose } public static DataTable Query(string sql, params SqlParameter[] ps) { using (var conn = new SqlConnection(ConnStr)) using (var cmd = new SqlCommand(sql, conn)) using (var da = new SqlDataAdapter(cmd)) { if (ps != null && ps.Length > 0) cmd.Parameters.AddRange(ps); var dt = new DataTable(); da.Fill(dt); // 断开式查询,适配器内部自动 Open/Close return dt; } } public static int Execute(string sql, params SqlParameter[] ps) { using (var conn = new SqlConnection(ConnStr)) using (var cmd = new SqlCommand(sql, conn)) { if (ps != null && ps.Length > 0) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); // 返回受影响行数,用来判断成功与否 } } }Query里用SqlDataAdapter.Fill而不是SqlDataReader,是因为填充DataTable后连接就被释放了,属于「断开式」访问,DataGridView绑定之后不需要保持连接,这在桌面程序里更稳。Execute返回的是受影响行数,调用方拿到0就说明没命中任何记录,是很实用的判断依据。
参数化是这里的硬规矩。Execute("DELETE FROM Student WHERE Id=" + id)这种拼接写法,一旦id来自文本框,注入就成立;用SqlParameter之后,数据库会把整个值当成数据而不是 SQL 片段,安全性和「玄学」的引号转义问题一起解决。
真正做查询的是 DAL 里的具体方法:
// DAL/StudentDal.cs public IList<Student> GetByClass(int classId) { const string sql = @"SELECT Id, Name, Gender, Birthday, ClassId FROM dbo.Student WHERE ClassId = @classId ORDER BY Id"; var dt = SqlHelper.Query(sql, new SqlParameter("@classId", classId)); var list = new List<Student>(dt.Rows.Count); foreach (DataRow row in dt.Rows) { list.Add(new Student { Id = Convert.ToInt32(row["Id"]), Name = row["Name"].ToString(), Gender = row["Gender"] == DBNull.Value ? "" : row["Gender"].ToString(), Birthday = row["Birthday"] == DBNull.Value ? (DateTime?)null : Convert.ToDateTime(row["Birthday"]), ClassId = Convert.ToInt32(row["ClassId"]) }); } return list; }这段映射代码是重复劳动,但它是显式的:每个字段从哪来、空值怎么处理、类型怎么转,一眼可见。DBNull.Value判断必须写,数据库里允许为空的列读出来就是DBNull,直接ToString()会得到空字符串看着没事,但Convert.ToDateTime(DBNull.Value)会当场抛InvalidCastException,而这种异常往往只在你点开某一条「刚好没填生日」的记录时才出现。
3.3 业务层:校验、事务和异常
业务层不是把 DAL 的方法原样转发一遍,那叫「透传层」,没有存在价值。它真正干活的地方有两处:进来先校验,写多张表时开事务。
// BLL/StudentBll.cs public bool AddStudent(Student s, string operatorName) { if (string.IsNullOrWhiteSpace(s.Name)) throw new ArgumentException("姓名不能为空"); if (s.ClassId <= 0) throw new ArgumentException("必须选择一个班级"); using (var conn = SqlHelper.OpenConnection()) using (var tx = conn.BeginTransaction()) { try { const string insertSql = "INSERT INTO dbo.Student(Name, Gender, Birthday, ClassId) " + "VALUES(@name, @gender, @birthday, @classId)"; using (var cmd = new SqlCommand(insertSql, conn, tx)) { cmd.Parameters.Add("@name", SqlDbType.NVarChar, 32).Value = s.Name.Trim(); cmd.Parameters.Add("@gender", SqlDbType.NVarChar, 4).Value = (object)s.Gender ?? DBNull.Value; cmd.Parameters.Add("@birthday", SqlDbType.Date).Value = (object)s.Birthday ?? DBNull.Value; cmd.Parameters.Add("@classId", SqlDbType.Int).Value = s.ClassId; cmd.ExecuteNonQuery(); } // 同一事务里写操作日志:要么都成功,要么都不留痕 const string logSql = "INSERT INTO dbo.OperateLog(Action, OperatorName, CreateTime) " + "VALUES(@action, @op, GETDATE())"; using (var cmd = new SqlCommand(logSql, conn, tx)) { cmd.Parameters.Add("@action", SqlDbType.NVarChar, 100).Value = "新增学生:" + s.Name; cmd.Parameters.Add("@op", SqlDbType.NVarChar, 32).Value = operatorName; cmd.ExecuteNonQuery(); } tx.Commit(); return true; } catch { tx.Rollback(); // 日志写失败,学生记录也不会留下 throw; // 继续往上抛,交给 UI 统一提示 } } }几个参数和写法解释一下。SqlDbType.NVarChar, 32里的 32 是长度上限,和建表语句里NVARCHAR(32)对齐,不对齐的话 SQL Server 可能做隐式转换,索引直接失效。(object)s.Gender ?? DBNull.Value是把 C# 的null转成数据库认的DBNull.Value,直接传null会报「参数化查询未提供值」。tx.Commit()之后return true,注意别把Commit写在using块外面,连接已经释放了会抛「事务已完成」。
这里我一般不会在 BLL 里catch完就吞掉异常然后return false,那样调用方拿不到任何原因。抛出去,让 UI 层统一提示,日志里留完整堆栈,排查的时候才有据可依。
3.4 界面层:DataGridView 绑定的三个细节
界面层最容易写成一坨,但真正要小心的只有绑定这一块。
// UI/Forms/FrmStudent.cs private readonly StudentBll _bll = new StudentBll(); private void FrmStudent_Load(object sender, EventArgs e) { // 下拉框绑定必须先设 DisplayMember 和 ValueMember,再设 DataSource cboClass.DisplayMember = "ClassName"; cboClass.ValueMember = "Id"; cboClass.DataSource = new ClassBll().GetAll(); dgvStudent.AutoGenerateColumns = false; // 列在設計器里手工摆好,顺序和宽度都可控 dgvStudent.AllowUserToAddRows = false; // 否则会多出一行空白行,取 SelectedRows 时容易取错 dgvStudent.SelectionMode = DataGridViewSelectionMode.FullRowSelect; BindGrid(); } private void BindGrid() { int classId = cboClass.SelectedValue == null ? 0 : Convert.ToInt32(cboClass.SelectedValue); dgvStudent.DataSource = _bll.GetByClass(classId); } private void btnAdd_Click(object sender, EventArgs e) { try { var s = new Student { Name = txtName.Text, Gender = rdoMale.Checked ? "男" : "女", Birthday = dtpBirthday.Checked ? (DateTime?)dtpBirthday.Value : null, ClassId = Convert.ToInt32(cboClass.SelectedValue) }; _bll.AddStudent(s, CurrentUser.UserName); BindGrid(); // 加完立刻刷新,别指望 DataGridView 自己知道 } catch (Exception ex) { MessageBox.Show(ex.Message, "操作失败"); } }AutoGenerateColumns = false是个分水岭。设成true时列名直接取数据库字段名,用户看到的是ClassId;设成false之后你在设计器里摆列,把DataPropertyName对到实体属性上,HeaderText写中文,宽度固定好,界面立刻像回事。AllowUserToAddRows = false同理,默认那个末尾的星号空行会让SelectedRows.Count在某些操作后变成 1 而你根本没选任何数据,是新手很常见的困惑。
dtpBirthday.Checked这个判断对应「生日可空」的设计,DateTimePicker默认总会给一个日期,只有配了ShowCheckBox = true才有「未选」状态,这点在改造时要一起改,否则用户的生日永远会被写成程序启动那天。
4. 跑起来:从附加数据库到首屏出数据的完整步骤
代码看懂了不代表能跑起来。这类项目 90% 的「跑不起来」都发生在打开 Visual Studio 之前,卡在数据库那一环。
4.1 环境准备:目标框架、平台目标与实例名
先确认三件事:装没装 Visual Studio 2015 或更高版本(更高版本能打开,但会提示升级工具集,选「不升级」也能编译);机器上有没有可用的 SQL Server 实例(Express 版、开发者版、LocalDB 都行);.NET Framework 4.5以上的运行时在不在。
打开解决方案后,右键启动项目看「属性」,重点关注两个地方。一是「目标框架」,如果显示 4.5 而你机器只装了 4.8,一般能自动向上滚动,问题不大;二是「生成」页里的「平台目标」,AnyCPU在 64 位系统上会以 64 位进程运行,如果项目里引用了只提供 32 位版本的组件(老式 Office 互操作、某些报表控件),就会在运行时报「未在本地计算机注册」,这时候把平台目标改成x86是最快的解法。
实例名要提前搞清楚。打开 SSMS 或命令行sqlcmd -L看一眼本机有哪些实例,.\SQLEXPRESS、(localdb)\MSSQLLocalDB、localhost是三种完全不同的写法,混用就是连不上。
4.2 数据库落地:附加 MDF 与改连接串
如果包里带的是.mdf文件,最省事的做法是通过 SSMS 右键「数据库」→「附加」,选中文件,记下附加后的数据库名,然后去改连接串。如果带的是.sql脚本,就用 SSMS 新建查询,把脚本整个粘进去执行:
-- 建库(已存在则跳过,方便重复执行) IF DB_ID('SchoolDb') IS NULL CREATE DATABASE SchoolDb; GO USE SchoolDb; GO -- 用户表:Name 用 NVARCHAR 才能存中文,VARCHAR 会变问号 CREATE TABLE dbo.[User]( Id INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(32) NOT NULL UNIQUE, PasswordHash NVARCHAR(64) NOT NULL, -- 存哈希,绝不存明文密码 RoleId INT NOT NULL DEFAULT(2), CreateTime DATETIME NOT NULL DEFAULT(GETDATE()) ); GO -- 初始管理员:密码哈希在代码里用同一套算法生成后再替换这一行 INSERT INTO dbo.[User](UserName, PasswordHash, RoleId) VALUES(N'admin', N'00000000000000000000000000000000', 1); GO这段脚本里有两个点值得多说。IF DB_ID(...) IS NULL是幂等写法,重复执行不会报「数据库已存在」,多人协作或者反复重装环境时特别省事。PasswordHash字段而不是Password字段,说明这个项目至少在密码存储上做了正确的事——如果包里的脚本是明文密码字段,我建议你在二次开发前先把它改掉,这是最容易埋雷的地方。
脚本跑完,回到App.config把Initial Catalog改成实际库名,Data Source改成实际实例名。还有一种「免附加」的写法,适合把 MDF 直接放在项目目录里随程序走:
<add name="SchoolDb" connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\SchoolDb.mdf;Integrated Security=True" providerName="System.Data.SqlClient" />|DataDirectory|是个占位符,运行时会被替换成程序的AppDomain.BaseDirectory(通常是bin\Debug),所以 MDF 文件必须放在那个目录下,或者改成绝对路径。这个写法方便打包,但换机器时一旦目录结构变了就会报「文件已在使用中」或「找不到文件」,我一般只在演示场景用它。
4.3 编译、首次登录与功能自检
按Ctrl+Shift+B编译。如果报「未能找到类型或命名空间」,八成是某个项目没引用到System.Configuration,右键「引用」→「添加引用」→「程序集」里勾上即可——ConfigurationManager在这个命名空间下,VS2015 建新项目时默认不会自动加。
首次运行建议按这个顺序点一遍,比瞎点有效得多:先用初始账号登录,登进去看主窗体有没有直接报错;然后点开一个有数据的列表页,确认DataGridView出数据;接着做一次新增,关掉程序再打开,确认数据还在(这一步验证的是提交真的落库了,而不是只更新了内存里的DataTable);最后做一次删除,看有没有二次确认弹窗和刷新。
删数据前加二次确认这件事,我在任何项目里都会强制自己检查。没有确认框的删除按钮就是一颗定时炸弹,用户手一抖,一条记录就没了,而这类应用通常没有完善的备份机制。
4.4 打包发布:把 exe 和配置文件一起带走
发布只需要bin\Release下那几样东西:主 exe、所有依赖的 dll、以及项目名.exe.config。很多人把 exe 单独拷给别人,结果对方一打开就闪退,99% 是配置文件没带过去,连接串读不到,ConfigurationManager.ConnectionStrings["SchoolDb"]直接返回null,后面一句.ConnectionString就是空引用异常。
如果你希望对方机器上不用装 SQL Server,就得选一个轻量数据库(SQLite 或 LocalDB),这属于二次开发的范畴,放到最后一章说。同目录下如果有x86或x64子目录,说明某些 dll 是分平台的,也要一起带。
5. 避坑排查:五个最容易被卡住的现场与处理办法
下面这五条,基本覆盖了我在拆这类项目时真正花时间的部分。每一条我都按「现象 → 原因 → 解决」写,遇到对应报错可以直接对照。
5.1 提示「网络相关或特定于实例的错误」,就是连不上
现象:程序一启动就弹出类似「在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误。未找到或无法访问服务器」,异常号常带 26 或 40。
原因:连接串里的实例名和机器上实际的实例对不上,或者对应的 SQL 服务根本没启动。还有一种可能是协议没开——默认安装的 Express 版有时只开了共享内存,命名管道和 TCP/IP 是关的。
解决:先去服务里确认 SQL 服务是「正在运行」状态,名字通常形如SQL Server (SQLEXPRESS);然后打开「SQL Server 配置管理器」,在「网络配置」里把 TCP/IP 和命名管道都启用;最后核对连接串的Data Source,实在不确定就先用(localdb)\MSSQLLocalDB试一次,能连上就说明是实例名的问题。
5.2 窗体设计器一打开就抛异常
现象:双击.cs文件能进代码,但切到「设计」视图就报「设计器加载时引发以下异常」,或者整块界面变成灰色带红色叉。
原因:设计器本质上是执行窗体的构造函数和InitializeComponent,如果构造函数里干了重活——比如连数据库、读配置文件、初始化需要许可证的第三方控件——设计器执行到那一行就挂了。另一个常见原因是有人手改了*.Designer.cs,把一个控件声明删了但引用还在。
解决:把构造函数里的数据库调用挪到Load事件里,构造函数只留InitializeComponent()。这是最干净的做法,顺便还加快了窗体加载速度。如果是Designer.cs被改坏了,从版本记录里回滚那个文件,或者把窗体上的控件逐个删掉重建(虽然土,但比重写整个文件快)。
5.3 表格里改完数据,关掉窗口就没了
现象:用户直接在DataGridView里编辑单元格,改完点保存提示成功,重新打开数据变回原样。
原因:绑定的数据源是List<T>,用户改的只是内存里那个对象的属性,没有任何代码把它同步回数据库。DataGridView不会自动回写,它只是显示层。
解决:要么改成显式编辑——选中行后弹一个编辑窗体,拿到用户输入再调BLL.Update;要么把数据源换成BindingList<T>并监听ListChanged事件,在事件里执行更新。我一般选前者,代码多二十行,但逻辑清晰,出错时知道去哪找。
5.4 中文变问号,排序也不对
现象:界面上输入的中文存进库变成???,或者列表按姓名排序时「张三」排到「李四」后面,毫无规律。
原因:字段类型用了VARCHAR而不是NVARCHAR,非 Unicode 编码存不下中文。排序乱是因为数据库的排序规则(Collation)不是中文的,按编码值排序自然不符合拼音或笔画习惯。
解决:把字段类型统一改成NVARCHAR,长度按实际需要给(姓名 32、地址 128 足够)。排序规则在ORDER BY里显式指定:ORDER BY Name COLLATE Chinese_PRC_CI_AS。如果整个库都要中文排序,建库时就把默认排序规则设成Chinese_PRC_CI_AS,省得每处都写。
5.5 发布到别人机器上闪退,什么都没有
现象:自己机器跑得好好的,拷给别人,双击之后毫无反应,或者一闪而过。
原因:大概率是配置文件缺失导致读连接串时抛异常,而Program.cs里没有全局异常兜底,异常直接结束进程;另一个可能是对方机器没有装对应版本的 .NET Framework。
解决:先确认项目名.exe.config和 exe 在同一个目录。然后在Program.cs里加一层兜底,把未捕获的异常写到文本文件里,这样至少能看到原因:
// Program.cs [STAThread] static void Main() { // 全局兜底:任何未捕获异常都落盘,避免"闪退无信息" AppDomain.CurrentDomain.UnhandledException += (s, e) => { var ex = e.ExceptionObject as Exception; File.AppendAllText( Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "crash.log"), DateTime.Now + " " + ex + Environment.NewLine); }; Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new FrmLogin()); }这段兜底代码我几乎每个 WinForms 项目都会加,成本不到十行,省下的排查时间是以小时计的。AppDomain.CurrentDomain.BaseDirectory保证日志写在 exe 旁边,不会因为权限问题写不进去。对方机器上没装运行时的话,会弹一个系统级的提示,那个好判断,照着提示装对应的 .NET Framework 版本就行。
6. 二次开发:把这份骨架换成你自己的业务系统
这套代码真正值钱的地方不是它实现了什么,而是它把「增删改查 + 登录 + 三层」这条主干搭好了,你往里填业务就行。我通常按下面的顺序改,从改动量小、风险低的地方入手。
| 改造点 | 具体动作 | 影响范围 |
|---|---|---|
| 换业务实体 | 改Model里的类,同步改建表脚本 | 全链路,但机械 |
| 加新模块 | 复制一套 Model/DAL/BLL/Form,改名 | 局部,互不干扰 |
| 换数据库 | 抽IDbHelper接口,把 SQL Server 实现换成 SQLite | DAL 层,两三天 |
| 换登录方式 | 哈希算法换成带盐的 PBKDF2,改User表结构 | BLL + Common |
| 界面美化 | 统一字体、列宽、按钮位置,抽一个基类窗体 | UI 层,纯体力 |
换数据库这一项值得展开说一句,因为它是最容易被低估的。很多人以为把System.Data.SqlClient换成System.Data.SQLite就完了,实际上 SQL 方言差异比想象中大:GETDATE()要换成datetime('now','localtime'),IDENTITY(1,1)要换成AUTOINCREMENT,NVARCHAR直接变TEXT。我一般会先抽一个接口出来,把差异全部关在实现类里:
public interface IDbHelper { DataTable Query(string sql, params IDbDataParameter[] ps); int Execute(string sql, params IDbDataParameter[] ps); IDbConnection OpenConnection(); }这样上层 BLL 完全不知道底下是哪个库,将来再换一次也不用动业务代码。抽接口的时机是「你确定要换库」的时候,如果只是想想,别过度设计,直接改SqlHelper就行。
还有一个很实用的验证方法:改完之后不要只点界面,写一个简单的控制台入口,把 BLL 的方法挨个调一遍。界面能跑通不代表业务逻辑没问题,很多空值判断、事务回滚的分支在界面上根本点不出来。我习惯在Common项目里临时加个SmokeTest类,把新增、查询、删除各跑一次,看数据库里的数据变化是否符合预期,跑完删掉。
最后说一个我自己的习惯。每次接手这类项目,我都会在动手改第一个字母之前,先把原始压缩包原样解压一份到另一个目录放着,当「后悔药」。刚学的时候我觉得这是多余动作,直到有一次改连接串改崩了配置文件、又没有版本记录,只能重新去翻下载记录。从那以后我每次改别人的项目都强制走一遍「先备份、再跑通、最后才改」,这个顺序看起来慢,实际是最快的。希望帮到你。
本文还有配套的精品资源,点击获取