简介:这份学生管理系统C#项目实例面向C#初学者、课程设计学生及需要参考桌面应用完整结构的开发者,基于Visual Studio 2008与WinForms构建,覆盖学号、姓名、性别、年龄、班级等信息管理,并延伸至成绩、出勤、奖惩等业务,可用于课程作业、毕业设计原型或桌面开发练习。压缩包共378个文件,约17.96MB,包含121个cs源码、51个resx资源、50个jpg图片、44个resources资源及36个ico图标,另有sln/csproj工程、exe程序、rpt报表、mdf/sql数据库等,便于打开工程研究界面与数据层实现。已有517人学习,可了解ADO.NET数据访问、数据绑定、异常处理、文件导出与设计模式在完整项目中的落地方式。
1. 一个 vs2008 时代的 C# 学生管理系统,放今天还值得拆吗
如果你手上正好躺着一个vs2008编译的学生管理系统 C#项目实例,双击.sln大概率会先弹一句「此项目与当前版本不兼容」的警告,然后才慢悠悠加载完——这个瞬间很容易劝退人。但这个包恰恰是 C#项目实例 里最适合拿来练手的一类:功能闭环完整,学生信息、班级、课程、成绩四张表就能串起增删改查、登录鉴权、报表统计。它不属于炫技型代码,属于「跑起来就能交作业、拆开能看分层」的工程骨架。适合两类人:一是要交课设、需要一份能看懂改得动的基础项目;二是工作几年但对 WinForms 三层架构只有模糊印象、想找个完整样本对照的开发者。
2. 拆包先看骨架:解决方案结构与四张表的字段取舍
拿到压缩包,别急着 F5。先用 VS2008 打开.sln,在解决方案资源管理器里把项目类型、目标框架、引用列表这三样过一遍,十分钟就能判断这份工程值不值得往下读。很多人血泪经验的来源就是:先跑起来再问「为什么跑不起来」,顺序反了。
2.1 工程结构与目标框架
典型的课设级结构长这样,四个项目各自职责很清晰:
StudentManage.sln ├── StudentManage.Model # 实体类,只有属性,无逻辑 ├── StudentManage.DAL # 数据访问层,拼 SQL、开连接 ├── StudentManage.BLL # 业务逻辑层,校验、组装 └── StudentManage.UI # WinForms 主程序,窗体与事件打开每个.csproj看<TargetFrameworkVersion>,如果是v3.5,说明它对应 .NET Framework 3.5,VS2008 原生支持,用 VS2010 及以上打开会触发升级向导。升级不是不能升,但升级后System.Core的引用方式、app.config里的startup节点都可能要手动调,课设场景我一般建议直接用 VS2008 或 VS2010 打开,别折腾迁移。
检查引用列表里有几项是必须存在的:System.Configuration(读连接串用)、System.Data(SqlClient 在这)、System.Windows.Forms、System.Drawing。少一项就是编译期一片红。
提示:如果打开时提示「未能找到类型或命名空间名称 ConfigurationManager」,不是代码写错,是没引
System.Configuration,右键引用 → 添加引用 → .NET 选项卡里勾上即可。
2.2 四张核心表的字段设计
数据库一般是 SQL Server 2005/2008 的.mdf文件,也可能给的是建表脚本。核心四张表:Student、Class、Course、Score,加上一张SysUser做登录。建表时最容易出问题的不是主键,是字符类型和约束。下面是这套工程里最值得原样照抄的部分:
-- 班级表:被学生表外键引用,先建 CREATE TABLE Class ( ClassId INT IDENTITY(1,1) PRIMARY KEY, ClassName NVARCHAR(50) NOT NULL, Grade NVARCHAR(20) NULL ); -- 学生表:StudentNo 业务唯一键,比自增主键更适合做查询条件 CREATE TABLE Student ( StudentId INT IDENTITY(1,1) PRIMARY KEY, StudentNo VARCHAR(20) NOT NULL UNIQUE, -- 学号,纯数字字母,varchar 够用 Name NVARCHAR(20) NOT NULL, -- 姓名,必须 nvarchar Gender NCHAR(1) NOT NULL CHECK (Gender IN (N'男', N'女')), BirthDate DATE NULL, ClassId INT NOT NULL, CONSTRAINT FK_Student_Class FOREIGN KEY (ClassId) REFERENCES Class(ClassId) ); -- 成绩表:一个学生一门课一条记录,用联合唯一约束防重复录入 CREATE TABLE Score ( ScoreId INT IDENTITY(1,1) PRIMARY KEY, StudentId INT NOT NULL, CourseId INT NOT NULL, Score DECIMAL(5,1) NULL CHECK (Score >= 0 AND Score <= 100), ExamDate DATE NULL, CONSTRAINT UQ_Score UNIQUE (StudentId, CourseId), CONSTRAINT FK_Score_Student FOREIGN KEY (StudentId) REFERENCES Student(StudentId) );这段 SQL 里有三个决定后期返工量的点。第一,Name用NVARCHAR而不是VARCHAR,中文姓名存进去才不会是问号;第二,StudentNo单独做UNIQUE,因为学号是业务主键,界面按学号查比按自增 Id 查更符合使用习惯;第三,Score上的UNIQUE (StudentId, CourseId)是防重复录入的最后一道闸,比在 BLL 里SELECT COUNT(*)再判断可靠得多——并发下后者会漏。
2.3 连接字符串放哪儿:三种写法的取舍
连接串硬编码在.cs里是这类工程最常见的毛病,换台机器就要改代码重编译。正确位置是App.config(UI 项目下),通过ConfigurationManager.ConnectionStrings读取:
<?xml version="1.0" encoding="utf-8"?> <configuration> <connectionStrings> <!-- Windows 身份验证:本机 SQL Server 且当前用户有权限时最省事 --> <add name="SchoolDB" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=SchoolDB;Integrated Security=True" providerName="System.Data.SqlClient"/> </connectionStrings> </configuration>三种写法对应三种部署场景,选错了就是「我这儿能跑,拷过去就报错」:
| 写法 | 关键参数 | 适用场景 | 主要限制 |
|---|---|---|---|
| Windows 身份验证 | Integrated Security=True | 本机开发、教师机统一域账号 | 换非域机器常因权限失败 |
| SQL 身份验证 | User ID=sa;Password=xxx | 需要拷给多人运行 | 密码明文躺在配置文件里 |
| 附加数据库 | `AttachDbFilename=|DataDirectory | \SchoolDB.mdf` | 不想装实例、单机演示 |
我一般会把连接串做成一个可切换的节点,用appSettings存一个UseAttach开关,在启动时决定用哪条。这样同一份代码,教师在实验室跑和学生在宿舍跑都不用改源码。
3. 三层架构不是摆设:Model、DAL、BLL、UI 各写一遍
课设级工程里三层架构经常被写成「三层文件夹」,DAL 里塞业务判断,UI 里直接写 SQL,分层形同虚设。这份工程值得学的地方在于它把边界划得还算干净:Model 只装数据,DAL 只负责跟数据库打交道,BLL 承接校验,UI 只管控件和事件。下面按调用顺序从下往上写一遍,每层落一段能直接用的代码。
3.1 Model 层:实体类与字段映射
实体类没有逻辑,属性名跟表字段保持一一对应,方便后面DataReader按序号或列名赋值。注意 VS2008 支持自动属性,写法可以很简洁:
// StudentManage.Model/Student.cs public class Student { public int StudentId { get; set; } // 自增主键 public string StudentNo { get; set; } // 学号,业务唯一 public string Name { get; set; } // 姓名 public string Gender { get; set; } // 性别,男/女 public DateTime? BirthDate { get; set; } // 可空,必须用 Nullable<DateTime> public int ClassId { get; set; } // 外键,指向 Class // 只读展示字段,不映射数据库,绑定 DataGridView 时用来显示班级名 public string ClassName { get; set; } }BirthDate声明成DateTime?而不是DateTime,是因为数据库列允许NULL,读到空值直接赋给非空DateTime会抛SqlNullValueException。ClassName是冗余展示字段,在联合查询时填充,不参与写库,注意别在INSERT语句里带上它。
3.2 DAL 层:SqlHelper 先封好,后面全受益
DAL 层的第一件事不是写StudentDAL,是写一个SqlHelper。把开连接、加参数、执行这三步收拢到一处,后面所有 DAL 类都只写 SQL 和参数,重复代码能砍掉一大半:
// StudentManage.DAL/SqlHelper.cs public static class SqlHelper { // 连接串只在此处读取一次,避免各处 new SqlConnection 时重复拼字符串 public static readonly string ConnStr = ConfigurationManager.ConnectionStrings["SchoolDB"].ConnectionString; // 执行增删改,返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] ps) { using (SqlConnection conn = new SqlConnection(ConnStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (ps != null && ps.Length > 0) { cmd.Parameters.AddRange(ps); // 参数化,从根上挡掉拼接注入 } conn.Open(); return cmd.ExecuteNonQuery(); } } // 执行查询并返回 DataTable,供 DataGridView 直接绑定 public static DataTable ExecuteDataTable(string sql, params SqlParameter[] ps) { using (SqlConnection conn = new SqlConnection(ConnStr)) using (SqlDataAdapter adapter = new SqlDataAdapter(sql, conn)) { if (ps != null && ps.Length > 0) { adapter.SelectCommand.Parameters.AddRange(ps); } DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } }两个方法覆盖九成场景。参数说明上,params SqlParameter[]允许调用方不传参也不报错,比如ExecuteNonQuery("DELETE FROM Student WHERE StudentId=@id", new SqlParameter("@id", 3))。using保证连接在异常时也能关闭,这个在 VS2008 的using语句里能嵌套两个,C# 3.0 已支持。
封好之后写具体 DAL 就很薄:
// StudentManage.DAL/StudentDAL.cs public class StudentDAL { public List<Student> GetAll() { string sql = @"SELECT s.StudentId, s.StudentNo, s.Name, s.Gender, s.BirthDate, s.ClassId, c.ClassName FROM Student s LEFT JOIN Class c ON s.ClassId = c.ClassId ORDER BY s.StudentNo"; DataTable dt = SqlHelper.ExecuteDataTable(sql); List<Student> list = new List<Student>(); foreach (DataRow row in dt.Rows) { list.Add(new Student { StudentId = Convert.ToInt32(row["StudentId"]), StudentNo = row["StudentNo"].ToString(), Name = row["Name"].ToString(), Gender = row["Gender"].ToString(), BirthDate = row["BirthDate"] == DBNull.Value ? (DateTime?)null : Convert.ToDateTime(row["BirthDate"]), ClassId = Convert.ToInt32(row["ClassId"]), ClassName = row["ClassName"] == DBNull.Value ? "" : row["ClassName"].ToString() }); } return list; } }DBNull.Value的判断不能省,Convert.ToDateTime(DBNull.Value)会抛InvalidCastException。用LEFT JOIN而不是INNER JOIN,是为了班级被删但学生还在时,查询不至于整条记录消失——这在课设里是常见的数据不一致来源。
3.3 BLL 层:校验放这儿,别塞进 UI
业务校验的位置决定后期改需求时的工作量。学号重复、分数越界、班级不存在,这些都该在 BLL 拦下来,UI 只负责把消息弹给用户:
// StudentManage.BLL/StudentBLL.cs public class StudentBLL { private StudentDAL dal = new StudentDAL(); // 返回值用 out 参数传出失败原因,UI 直接 MessageBox.Show(msg) public bool Add(Student s, out string msg) { if (string.IsNullOrEmpty(s.StudentNo) || s.StudentNo.Trim().Length == 0) { msg = "学号不能为空"; return false; } if (s.Name == null || s.Name.Trim().Length == 0) { msg = "姓名不能为空"; return false; } if (dal.ExistsByNo(s.StudentNo)) { msg = "该学号已存在,请检查是否重复录入"; return false; } int rows = dal.Insert(s); msg = rows > 0 ? "添加成功" : "添加失败,请查看日志"; return rows > 0; } }ExistsByNo在 DAL 里是一条SELECT COUNT(1) FROM Student WHERE StudentNo=@no,用参数化。这里要清醒地知道:ExistsByNo加INSERT不是原子操作,高并发下仍可能插重,真正的保险是数据库表的UNIQUE约束。BLL 这层判断的价值在于给出友好提示,而不是保证一致性。
3.4 UI 层:WinForms 数据绑定别偷懒
UI 层最容易写歪的地方是「把DataGridView当文本框用」——查询出DataTable直接DataSource = dt,列顺序、列标题全跟着 SQL 走,改个字段名界面就崩。正确做法是关掉自动生成列,手动绑定:
// StudentManage.UI/FormStudentList.cs private void FormStudentList_Load(object sender, EventArgs e) { dgvStudents.AutoGenerateColumns = false; // 关掉自动列,改由设计器控制 dgvStudents.DataSource = new StudentBLL().GetAll(); } // 设计器里给每列设置 DataPropertyName,必须与 Student 属性名完全一致 // ColumnStudentNo.DataPropertyName = "StudentNo"; // ColumnName.DataPropertyName = "Name"; // ColumnClassName.DataPropertyName = "ClassName";AutoGenerateColumns = false之后,DataPropertyName拼错一个字母,整列就是空白,而且不报错——这是新手最容易卡住的地方。建议绑定后加一句dgvStudents.AutoResizeColumns(),否则列宽挤在一起看不出问题。
4. 核心功能逐个跑通:登录鉴权、增删改查、成绩统计
骨架搭好后,功能代码就很直白了。这一章挑三个最能暴露问题的功能:登录(涉及密码与角色)、增删改查(涉及参数化和事务)、成绩统计(涉及聚合查询与空值),每个都给出可直接落地的写法和需要留意的参数。
4.1 登录:先明确密码怎么存
课设级工程里密码大多是明文存SysUser表,这在教学环境可以接受,但你得清楚边界:明文只适合本地演示,任何联网或多人使用的场景都该换成哈希。哪怕只用System.Security.Cryptography里的MD5或SHA1也比明文强,虽然它们今天已不建议用于生产。下面先看登录主流程:
// StudentManage.UI/FormLogin.cs private void btnLogin_Click(object sender, EventArgs e) { string user = txtUser.Text.Trim(); string pwd = txtPwd.Text.Trim(); if (user.Length == 0 || pwd.Length == 0) { MessageBox.Show("账号和密码不能为空", "提示"); return; } UserBLL bll = new UserBLL(); SysUser info = bll.Login(user, pwd); if (info == null) { MessageBox.Show("账号或密码错误", "提示"); return; } // Role:0 管理员,1 教师,2 学生 // 管理员全功能;教师只能录入成绩;学生只能查自己 this.Hide(); FormMain main = new FormMain(info); main.ShowDialog(); this.Close(); }UserBLL.Login内部调用 DAL,SQL 必须是参数化的:
// StudentManage.DAL/UserDAL.cs public SysUser GetByAccount(string account, string pwd) { string sql = @"SELECT UserId, Account, Role, StudentId FROM SysUser WHERE Account = @Account AND Pwd = @Pwd"; SqlParameter[] ps = new SqlParameter[] { new SqlParameter("@Account", SqlDbType.VarChar, 30) { Value = account }, new SqlParameter("@Pwd", SqlDbType.VarChar, 50) { Value = pwd } }; DataTable dt = SqlHelper.ExecuteDataTable(sql, ps); if (dt.Rows.Count == 0) return null; DataRow row = dt.Rows[0]; return new SysUser { UserId = Convert.ToInt32(row["UserId"]), Account = row["Account"].ToString(), Role = Convert.ToInt32(row["Role"]), StudentId = row["StudentId"] == DBNull.Value ? (int?)null : Convert.ToInt32(row["StudentId"]) }; }参数里显式写SqlDbType.VarChar, 30的好处是长度固定,SQL Server 能复用执行计划,也避免传超长字符串时被截断而不报错。StudentId允许为空,是因为管理员和教师账号不关联学生。
注意:登录成功后一定要用角色控制菜单可见性,而不是只在按钮点击里判断。只在事件里判断的话,用户仍能通过快捷键或窗体入口绕过去,这在验收时很尴尬。
4.2 增删改查:事务和参数化缺一不可
单表增删改很简单,真正容易翻车的是「删除班级时把学生也删了」这类级联操作。要么在数据库建外键并设ON DELETE CASCADE,要么在 BLL 里用事务包住两步操作。我倾向于后者,逻辑更显式:
// StudentManage.DAL/ClassDAL.cs public bool DeleteClassWithStudents(int classId, out string msg) { using (SqlConnection conn = new SqlConnection(SqlHelper.ConnStr)) { conn.Open(); using (SqlTransaction tran = conn.BeginTransaction()) { try { // 先删成绩,再删学生,最后删班级,顺序不能反 string sql1 = @"DELETE FROM Score WHERE StudentId IN (SELECT StudentId FROM Student WHERE ClassId=@cid)"; string sql2 = "DELETE FROM Student WHERE ClassId=@cid"; string sql3 = "DELETE FROM Class WHERE ClassId=@cid"; SqlCommand cmd = new SqlCommand(sql3, conn, tran); cmd.Parameters.AddWithValue("@cid", classId); cmd.CommandText = sql1; cmd.ExecuteNonQuery(); cmd.CommandText = sql2; cmd.ExecuteNonQuery(); cmd.CommandText = sql3; cmd.ExecuteNonQuery(); tran.Commit(); msg = "删除成功"; return true; } catch (Exception ex) { tran.Rollback(); // 任何一步失败整体回滚 msg = "删除失败:" + ex.Message; return false; } } } }三个DELETE复用同一个SqlCommand对象,改CommandText前不需要清参数——参数@cid三条语句都用。顺序必须是「成绩 → 学生 → 班级」,反了就会撞外键约束。
查询功能上,模糊查询最常见:
public List<Student> Search(string keyword, int classId) { string sql = @"SELECT s.*, c.ClassName FROM Student s LEFT JOIN Class c ON s.ClassId = c.ClassId WHERE 1=1"; List<SqlParameter> ps = new List<SqlParameter>(); if (!string.IsNullOrEmpty(keyword)) { sql += " AND (s.Name LIKE @kw OR s.StudentNo LIKE @kw)"; ps.Add(new SqlParameter("@kw", "%" + keyword + "%")); } if (classId > 0) // 0 表示「全部班级」 { sql += " AND s.ClassId = @cid"; ps.Add(new SqlParameter("@cid", classId)); } return MapToList(SqlHelper.ExecuteDataTable(sql, ps.ToArray())); }WHERE 1=1是动态拼 SQL 的标准起手式,后面每个条件以AND开头拼接,避免判断「是不是第一个条件」。关键字用通配符包进参数值,而不是拼进 SQL 文本,注入问题就不存在。
4.3 成绩统计:把聚合放到 SQL 里
成绩统计如果拉全表到内存里用for循环算,数据量一大界面就假死。正确做法是把分组、平均、排名都交给 SQL,WinForms 只负责显示:
-- 每门课的平均分、最高分、最低分、及格率 SELECT c.CourseName, COUNT(sc.ScoreId) AS 参考人数, AVG(sc.Score) AS 平均分, MAX(sc.Score) AS 最高分, MIN(sc.Score) AS 最低分, SUM(CASE WHEN sc.Score >= 60 THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(sc.ScoreId), 0) AS 及格率 FROM Course c LEFT JOIN Score sc ON c.CourseId = sc.CourseId WHERE sc.Score IS NOT NULL GROUP BY c.CourseName ORDER BY 平均分 DESC;两个细节值得留意。NULLIF(COUNT(...), 0)用来防除零,没有成绩记录时返回NULL而不是报「遇到以零作除数错误」。* 100.0里的.0不能省,两个整数相除在 SQL Server 里会先截断成整数,及格率会全是 0 或 100。
绑定到DataGridView后,导出 CSV 也是常见需求:
private void btnExport_Click(object sender, EventArgs e) { SaveFileDialog sfd = new SaveFileDialog(); sfd.Filter = "CSV 文件|*.csv"; if (sfd.ShowDialog() != DialogResult.OK) return; DataTable dt = (DataTable)dgvReport.DataSource; StringBuilder sb = new StringBuilder(); // 表头 for (int i = 0; i < dt.Columns.Count; i++) { sb.Append(dt.Columns[i].ColumnName); sb.Append(i == dt.Columns.Count - 1 ? "\r\n" : ","); } // 数据行:字段里含逗号时用双引号包住,避免列错位 foreach (DataRow row in dt.Rows) { for (int i = 0; i < dt.Columns.Count; i++) { string val = row[i] == DBNull.Value ? "" : row[i].ToString(); if (val.Contains(",")) val = "\"" + val + "\""; sb.Append(val); sb.Append(i == dt.Columns.Count - 1 ? "\r\n" : ","); } } // 用 UTF8 带 BOM 写,Excel 打开中文才不乱码 File.WriteAllText(sfd.FileName, sb.ToString(), Encoding.UTF8); MessageBox.Show("导出完成"); }Encoding.UTF8在 .NET Framework 3.5 里默认带 BOM,Excel 能正确识别中文;如果换成new UTF8Encoding(false)反而不带 BOM,Excel 打开会乱码。
5. 常见问题排查:五条血泪记录,每条都带解法
跑这类老工程,报错信息往往只有一个英文异常名,栈里全是系统方法。下面这几条是我在不同机器上重复遇到过的,按「现象 → 原因 → 解决」写,能省掉大部分搜索引擎时间。
5.1 编译与引用类问题
现象:一编译就是一片红,「未能找到类型或命名空间名称ConfigurationManager」「未能找到类型SqlConnection」。
原因:VS2008 新建项目默认不引用System.Configuration,System.Data虽然一般带着,但如果项目是从更早版本迁过来的,引用节点可能被清掉了。
解决:右键「引用」→「添加引用」→ .NET 选项卡,勾选System.Configuration和System.Data。DAL 项目还可能在引用里缺System.Data.SqlClient;.NET 3.5 里SqlClient是并入System.Data的,不需要单独引。加完重新生成,using System.Configuration;就不会再报错。
5.2 运行时空引用:NullReferenceException集中在窗体加载
现象:点「学生管理」按钮时报「未将对象引用设置到对象的实例」,堆栈指向FormStudentList_Load,但代码里每行看起来都正常。
原因:多半是设计器里改过控件Name,但FormStudentList.Designer.cs里旧的this.dgvStudents引用没同步;或者是AutoGenerateColumns = false之后,DataPropertyName写错,绑定成功但列不显示,随后代码里按列索引访问dgvStudents.Columns[3]就越界了。
解决:先看Designer.cs里有没有null初始化的控件变量,再检查所有DataPropertyName是否与Student属性名逐字一致。养成习惯:改控件名之后按F6重新生成一次,让设计器代码同步。
5.3 数据库连接报错 26:本机能跑,拷过去就挂
现象:在开发机上一切正常,拷到实验室机器上启动就弹「在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误」,错误号 26。
原因:连接串里写的Data Source=.\SQLEXPRESS依赖目标机器装了 SQL Server Express 且实例名一致;如果目标机装的是完整版默认实例,实例名就是机器名,连接串对不上。
解决:打开目标机的 SQL Server 配置管理器,确认实例名和是否启用了「命名管道」与「TCP/IP」协议。最稳的做法是把Data Source改成localhost或(local),同时把Initial Catalog换成实际库名,避免用AttachDbFilename拼路径——路径里的空格和中文最容易出问题。
5.4 中文变问号:字符类型选错
现象:学生姓名录入「张伟」,查出来是「??」,控制台输出正常,数据库里就是问号。
原因:建表时Name用了VARCHAR而不是NVARCHAR,参数类型也用了SqlDbType.VarChar。VARCHAR按数据库排序规则存储,非 Unicode 字符集下中文会被替换成问号。
解决:把列类型改成NVARCHAR(20),实体属性保持string,参数用SqlDbType.NVarChar, 20。注意N前缀:字符串字面量写成N'男',否则在部分排序规则下CHECK约束里的中文会失效,插入任何值都不报错。
5.5 DataGridView 一片空白:数据有,列没绑上
现象:断点看DataSource里确实有 50 行,但表格里一行都不显示,列头倒是在。
原因:AutoGenerateColumns = false时,每个列必须显式设置DataPropertyName,值要和数据源的属性名或DataTable的列名一致。常见错误是把DataPropertyName设成了HeaderText的中文名,或者大小写不一致(StudentIdvsStudentID)。
解决:在设计器里逐个列检查DataPropertyName,跟 Model 属性名做逐字符比对。临时排查可以把AutoGenerateColumns设回true,如果此时数据出来了,就确认是列绑定问题,再改回false逐个修。
6. 进阶:把课设代码收拾到能交付的水准
功能跑通只是及格线。这类工程真正拉开差距的是三件事:参数化做彻底、异常有兜底、部署能一键。
参数化我在第 3 章已经贯穿到底,这里补一个容易漏的口子:模糊查询里的LIKE通配符。很多人图省事写"... WHERE Name LIKE '%" + kw + "%'",一旦用户输入里带单引号就报语法错误,带--就能改写整条语句。记住所有用户输入一律走SqlParameter,字符串拼接只用于拼接结构(比如条件子句),绝不拼值。
异常兜底上,UI 层的事件入口最好统一包一层try/catch,把ex.Message弹出来、把ex.ToString()写进日志文件,避免程序直接黑掉看不出原因:
private void btnSearch_Click(object sender, EventArgs e) { try { dgvStudents.DataSource = new StudentBLL().Search(txtKeyword.Text.Trim(), GetClassId()); } catch (Exception ex) { // 用户看简短提示,日志留完整堆栈 File.AppendAllText("error.log", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " " + ex + "\r\n"); MessageBox.Show("查询失败,请稍后重试或联系管理员", "错误"); } }部署环节,VS2008 自带 Setup Project 模板:新建项目 → 其他项目类型 → 安装和部署 → 安装项目,把 UI 项目的输出加进「应用程序文件夹」,.mdf文件加到「应用程序文件夹」并在属性里设置TargetName,快捷方式拖到「用户桌面」。打包前记得把App.config的连接串改成相对路径写法,否则装到别人机器上还是指向你本机的实例。
从那以后我每次拿到一个 C#项目实例,都强制先做三步:看.csproj的目标框架、跑一遍建表脚本、把连接串换成相对路径再按 F5。这三步走完,基本能判断这个包值不值得继续读下去,也能避免「在我机器上是好的」这种最没营养的结论。希望帮到你。
本文还有配套的精品资源,点击获取