ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C# WinForms三层架构实战:学生信息管理系统从建库到增删查改

C# WinForms三层架构实战:学生信息管理系统从建库到增删查改 简介这份资源是一套基于 C# Windows 平台、采用三层架构开发的学生信息管理系统完整源码配套 SQL Server 数据库面向正在学习 WinForm 开发、数据库编程与分层设计的学生及初级开发者可用于课程设计、毕业设计参考或自学练手。系统实现用户登录注册、管理员信息修改以及学生信息的添加、删除、精确与模糊查找、修改等常规增删查改功能业务逻辑与数据访问分层清晰便于理解 BLL、DAL、Models 各层职责划分。压缩包共 127 个文件约 2.26MB以 38 个 cs 源码文件为核心辅以 csproj 项目文件、resx 与 resources 资源文件、config 配置、dll 与 pdb 编译产物及 exe 可执行程序结构完整可直接运行调试。目前已有 367 人学习下载适合作为三层架构入门与数据库操作实践的参考案例。1. 从一次课程设计答辩翻车说起三层架构到底在分什么很多同学做 C# Windows 学生信息管理系统第一版代码往往长这样一个 Form 里塞满按钮事件点“添加”直接拼 SQL 字符串点“查询”把 DataSet 绑到 DataGridView数据库连接串硬编码在窗体里。功能跑起来没问题但老师一问“业务逻辑写在哪”“换数据库要改几个文件”就答不上来。三层架构要解决的就是这个把界面、业务规则、数据访问拆成三块让改一处不牵连另外两处。这套结构在 C# WinForms 里落地成本很低一个解决方案建三个项目或者一个项目建三个文件夹都行。适合正在做数据库课程设计、需要交一份能讲清楚结构的作业或者刚入门 C# 想搞明白“分层”到底怎么分的人。下面按我实际写过的顺序从建库到跑通增删查改把每一步和踩过的坑都摊开。2. 三层架构在 WinForms 里的落点Model、DAL、BLL、UI 各管什么2.1 为什么不是 MVC而是三层热搜里常出现“MVC 三层架构”这个混搭说法其实在 WinForms 桌面程序里MVC 的 Controller 角色被窗体事件天然承担了一部分硬套 MVC 反而别扭。三层架构的划分更直接UI 层只负责显示和收集输入BLL 层负责判断“学号不能重复”“年龄必须在合理范围”DAL 层只负责和数据库对话。Model 层是贯穿三层的实体类它不算独立一层但必须有否则各层之间传参只能用 DataTable类型一乱就出玄学 bug。我一般建四个文件夹Models、DAL、BLL、UI。Models 放 Student 类DAL 放 StudentDALBLL 放 StudentBLLUI 放主窗体和编辑窗体。这样在解决方案资源管理器里一眼能看出依赖方向UI 引用 BLLBLL 引用 DAL 和 ModelsDAL 引用 Models。反向引用一律禁止这是分层不塌的第一条纪律。2.2 实体类怎么写才不给自己挖坑Student 实体类的属性要和数据库字段一一对应但类型要选对。比如性别用 string 还是 char出生日期用 DateTime 还是 string这些决定后面 BLL 里判断逻辑好不好写。我习惯全部用强类型Id 用 intName 用 stringGender 用 stringBirthDate 用 DateTimeClassId 用 int。可空字段用 int? 或 DateTime?不要用 object 糊弄。public class Student { public int Id { get; set; } public string StudentNo { get; set; } // 学号唯一 public string Name { get; set; } public string Gender { get; set; } public DateTime BirthDate { get; set; } public int ClassId { get; set; } public string Phone { get; set; } }这段代码没有技术难度但属性名和数据库列名保持一致能省掉后面手动映射的麻烦。如果数据库列叫 stu_no属性叫 StudentNo那在 DAL 里写 SQL 时就要用 AS 别名或者用 SqlDataReader 按序号取值。我倾向数据库列名直接用 PascalCase和 C# 属性名一样这样SELECT *回来直接反射赋值都不会错位。2.3 数据访问层把连接串和 SQL 关进一个房间DAL 层的职责单一到可以用一句话概括给 BLL 提供“查一个、查全部、增、删、改”五个方法内部处理连接打开关闭和参数化。连接串不要写在每个方法里抽一个静态类或者放到 App.config 的 connectionStrings 节点。用 App.config 的好处是换数据库时只改配置文件不用重新编译。public class StudentDAL { private static readonly string connStr System.Configuration.ConfigurationManager .ConnectionStrings[StudentDb].ConnectionString; public ListStudent GetAll() { var list new ListStudent(); string sql SELECT Id, StudentNo, Name, Gender, BirthDate, ClassId, Phone FROM Student; using (var conn new SqlConnection(connStr)) using (var cmd new SqlCommand(sql, conn)) { conn.Open(); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new Student { Id (int)reader[Id], StudentNo reader[StudentNo].ToString(), Name reader[Name].ToString(), Gender reader[Gender].ToString(), BirthDate Convert.ToDateTime(reader[BirthDate]), ClassId (int)reader[ClassId], Phone reader[Phone] DBNull.Value ? : reader[Phone].ToString() }); } } } return list; } }这里有几个参数和写法要说明。using包裹连接和命令保证异常时也能释放这是血泪经验早期我漏写 using 导致连接池耗尽程序跑一会儿就卡死。DBNull.Value判断不能省数据库里 Phone 允许为空时直接.ToString()会抛异常。Convert.ToDateTime比直接强转安全因为数据库返回的可能是 DateTime 也可能是字符串格式的日期取决于列类型。增删改方法结构类似区别是用ExecuteNonQuery返回受影响行数。插入时如果 Id 是自增SQL 里不写 Id 列用SELECT SCOPE_IDENTITY()拿回新 Id 是可选的课程设计里通常不需要。2.4 业务逻辑层校验和转换都放这里BLL 层是三层里最容易被写空的一层。很多同学 UI 直接调 DALBLL 只做转发那分层就白分了。BLL 至少要承担三件事输入校验、业务规则判断、把 DAL 返回的数据整理成 UI 需要的形状。public class StudentBLL { private readonly StudentDAL dal new StudentDAL(); public string AddStudent(Student s) { if (string.IsNullOrWhiteSpace(s.StudentNo)) return 学号不能为空; if (string.IsNullOrWhiteSpace(s.Name)) return 姓名不能为空; if (s.BirthDate DateTime.Now) return 出生日期不能晚于今天; if (dal.ExistsStudentNo(s.StudentNo)) return 学号已存在; dal.Insert(s); return OK; } }返回字符串表示结果UI 层根据是不是 OK 决定弹什么提示。这种写法不优雅但课程设计够用比抛异常再捕获简单。ExistsStudentNo是 DAL 里额外加的方法用SELECT COUNT(*)判断。注意校验顺序先判空再判业务规则最后才查数据库减少无谓的数据库往返。3. 从建库到跑通SQL Server 建表、连接配置和窗体绑定3.1 建库建表字段类型和约束一次定好用 SQL Server Management Studio 或者 Navicat 都行建一个 StudentDb 数据库然后执行建表语句。字段类型选择直接影响后面 C# 读取时的转换代码量。CREATE TABLE Student ( Id INT IDENTITY(1,1) PRIMARY KEY, StudentNo NVARCHAR(20) NOT NULL UNIQUE, Name NVARCHAR(50) NOT NULL, Gender NVARCHAR(4) NOT NULL, BirthDate DATE NOT NULL, ClassId INT NOT NULL, Phone NVARCHAR(20) NULL );IDENTITY(1,1)让 Id 自增UNIQUE约束保证学号不重复这样即使 BLL 漏了查重数据库也会拦一道。NVARCHAR而不是VARCHAR因为姓名和班级名可能有中文。DATE类型比DATETIME省空间C# 里用 DateTime 接收没问题。Phone 允许 NULL对应实体类里用 string 接收时要做 DBNull 判断。3.2 App.config 连接串和常见连接失败排查在项目里添加 App.config写入 connectionStrings 节点。连接串的 Server 部分写.\SQLEXPRESS还是(localdb)\MSSQLLocalDB取决于你装的 SQL Server 版本。Visual Studio 自带 LocalDB用(localdb)\MSSQLLocalDB最省事不用额外装服务。configuration connectionStrings add nameStudentDb connectionStringData Source(localdb)\MSSQLLocalDB;Initial CatalogStudentDb;Integrated SecurityTrue providerNameSystem.Data.SqlClient / /connectionStrings /configurationIntegrated SecurityTrue表示用 Windows 身份验证不用输账号密码。如果报“无法打开登录所请求的数据库”先确认数据库名拼写再确认当前 Windows 用户有没有权限。如果报“在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误”检查 SQL Server 服务有没有启动LocalDB 的话在命令行跑sqllocaldb start MSSQLLocalDB。3.3 主窗体DataGridView 绑定和按钮事件UI 层主窗体放一个 DataGridView 和四个按钮添加、编辑、删除、刷新。窗体加载时调 BLL 的 GetAll 方法把 List 绑到 DataGridView 的 DataSource。private void MainForm_Load(object sender, EventArgs e) { LoadStudents(); } private void LoadStudents() { var list studentBLL.GetAllStudents(); dataGridView1.DataSource null; dataGridView1.DataSource list; // 隐藏不需要显示的列 if (dataGridView1.Columns[Id] ! null) dataGridView1.Columns[Id].Visible false; } private void btnDelete_Click(object sender, EventArgs e) { if (dataGridView1.CurrentRow null) return; var student (Student)dataGridView1.CurrentRow.DataBoundItem; if (MessageBox.Show($确认删除 {student.Name}, 提示, MessageBoxButtons.YesNo) DialogResult.Yes) { studentBLL.DeleteStudent(student.Id); LoadStudents(); } }DataSource null再重新赋值是为了强制刷新直接赋新 List 有时 DataGridView 不更新这是 WinForms 的经典坑。DataBoundItem拿到的是绑定的 Student 对象比从单元格里读字符串再转换可靠得多。删除后重新加载而不是手动移除行保证界面和数据库一致。3.4 编辑窗体添加和修改复用同一个 Form添加和编辑的界面几乎一样区别只是编辑时要把已有数据填进去保存时调不同方法。我一般做一个 StudentEditForm构造函数接收一个 Student 对象为 null 表示添加不为 null 表示编辑。public StudentEditForm(Student student null) { InitializeComponent(); if (student ! null) { txtStudentNo.Text student.StudentNo; txtName.Text student.Name; cmbGender.Text student.Gender; dtpBirthDate.Value student.BirthDate; txtPhone.Text student.Phone; this.editId student.Id; } }保存按钮里根据 editId 是否大于 0 决定调 Add 还是 Update。注意学号在编辑时通常不允许改可以把 txtStudentNo 的 ReadOnly 设为 true避免改学号导致唯一约束冲突。4. 避坑与排查课程设计里最容易翻车的五个地方4.1 现象程序编译通过运行时点查询没反应也不报错原因通常是 DAL 里conn.Open()之后 SQL 写错但被 try-catch 吞了或者 DataGridView 的 DataSource 绑定了空列表但没刷新。解决在 DAL 方法里不要写空的 catch 块至少把异常信息弹出来或者写日志。调试时在cmd.ExecuteReader()那行打断点看 SQL 变量实际拼出来是什么。4.2 现象添加学生时提示“未将对象引用设置到对象的实例”原因多半是 UI 层从文本框取值时某个控件为 null或者 BLL 里dal.ExistsStudentNo返回时 DAL 内部 reader 读取了 DBNull 没处理。解决在 BLL 入口处对传入的 Student 对象做 null 判断DAL 里所有可能为 DBNull 的列都用reader.IsDBNull(序号)先判断再取值。4.3 现象删除后 DataGridView 还显示被删的行原因是直接操作了 DataGridView 的 Rows.Remove但数据库已经删了下次刷新又回来。或者绑定的 List 是同一个引用DataGridView 认为数据没变。解决删除后统一调 LoadStudents 重新查库并且赋值前先DataSource null。不要手动改 DataGridView 的行集合。4.4 现象换一台电脑运行报数据库连接失败原因是连接串里用了Data Source.或.\SQLEXPRESS但目标电脑装的是 LocalDB 或者 SQL Server 实例名不同。解决把连接串写到 App.config 里换环境只改配置文件。如果目标电脑没装 SQL Server可以改用 SQLiteDAL 层换一个实现类BLL 和 UI 不用动这就是分层的好处。4.5 现象学号重复插入时抛异常而不是返回友好提示原因是只依赖数据库的 UNIQUE 约束BLL 里没做查重或者查重和插入之间有并发。课程设计单用户场景下在 BLL 的 AddStudent 里先调ExistsStudentNo再插入就能解决。如果要做严谨可以在 DAL 的 Insert 里捕获 SqlException判断错误号 2627唯一约束冲突后返回特定错误码。5. 进阶技巧用接口把 DAL 抽象掉换数据库不改 BLL三层架构写到后面如果 BLL 里直接new StudentDAL()那 BLL 和 DAL 还是硬耦合。想换 SQLite 或者做单元测试时就得改 BLL 代码。一个成本很低的改进是给 DAL 抽一个接口。public interface IStudentDAL { ListStudent GetAll(); Student GetById(int id); void Insert(Student s); void Update(Student s); void Delete(int id); bool ExistsStudentNo(string studentNo); }然后 SqlServerStudentDAL 实现这个接口BLL 的构造函数接收 IStudentDAL 参数默认传 SqlServerStudentDAL 实例。这样以后加一个 SQLiteStudentDALUI 层只需要在创建 BLL 时换一个实现类BLL 一行不用改。这个技巧在课程设计答辩时是加分项能说明你真的理解“依赖抽象而不是具体”。验证方法很简单写一个假的 InMemoryStudentDAL用一个 List 模拟数据库在 BLL 的单元测试里注入进去不连数据库就能测校验逻辑。我一般会测三个用例学号为空返回错误、学号重复返回错误、正常添加后 GetAll 能查到。这三个用例覆盖了 BLL 的主要分支。最后一个习惯每次改完 DAL 的 SQL先在 SSMS 或 Navicat 里把 SQL 单独跑一遍确认结果正确再贴回代码。直接改代码跑程序调试出错了要分辨是 SQL 问题还是 C# 问题多花一倍时间。这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表