本周主要学习了用Winform开发学生管理系统,主要用于演示 WinForm 应用程序的开发流程和关键技术。
一、项目概述
MySchool 是一个典型的 Windows Forms(WinForm)桌面应用,用于管理学校基础信息,涵盖学生、教师、课程、班级、年级等模块,并包含用户登录与权限控制。它不仅是教学案例,更是学习 WinForm + 三层架构 + ADO.NET 的经典综合项目。
它包括以下核心模块:
学生信息管理 – 学生档案的增删改查(已完成)
教师信息管理 – 教师基本信息管理
课程管理 – 课程设置与安排
年级管理 – 年级信息维护
班级管理 – 班级信息维护
用户权限管理 – 系统登录与权限控制
目前我只完成了学生信息管理部分以及登录页面;
二、技术架构
项目采用清晰的 三层架构(3-tier architecture),实现关注点分离:
| 表示层(UI) | MySchool.UI | WinForm 界面、用户交互、数据绑定 |
| 业务逻辑层(BLL) | MySchool.BLL | 业务规则、数据校验、调用 DAL |
| 数据访问层(DAL) | MySchool.DAL | 数据库操作(ADO.NET)、SQL 执行 |
| 模型层(共享) | MySchool.Models | 实体类(如 Student、Teacher) |
| 工具层(可选) | MySchool.Utils |
全局工具(如当前登录用户上下文) |
依赖关系:
UI 层→ BLL层 + Models层
BLL层 → DAL层 + Models层
DAL层 → Models层
基本项目搭建:
第一步: 创建一个MySchool的解决方案, 项目名为MySchool.UI , 项目模版: Windows窗体应用(.Net Framework)


图1
第二步: 在MySchool的解决方案,右键创建项目, 模型选择:类库(.Net Framework)



图2
依次创建MySchool.DAL MySchool.BLL
完整的项目结构如下:

图3
第三步: 为相同添加其他项目的依赖:
-
MySchool.UI 依赖MySchool.BLL和MySchool.Models
-
MySchool.BLL 依赖MySchool.DAL和MySchool.Models
-
MySchool.DAL 依赖MySchool.Models

图4
三、关键技术点详解
数据库设计
- 使用 SQL Server
- 数据库 MySchool
- 主要表:Admin、Student、Teacher、Class、Grade、Subject、UserState
- 外键关联(如 Student.ClassId → Class.ClassId)

图5
连接字符串配置
不要使用硬编码,可以写在App.config中;
图6
在项目中添加配置类库 ,通过在MySchool.DAL项目中创建数据库工具类: DBHelper.cs,并添加 ConfigurationManager.ConnectionStrings["MySchool"] 来读取
如图:

图7
数据库帮助类相关代码:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MySchool.DAL
{
/// <summary>
/// 数据库辅助类
/// </summary>
public class DBHelper
{
//获取数据库连接字符串
static string ConnStr = System.Configuration.ConfigurationManager.ConnectionStrings["MySchool"].ToString();
#region 增、删、改(普通的SQL语句)
/// <summary>
/// 增、删、改(普通的SQL语句)
/// </summary>
/// <param name="sql">普通的SQL语句</param>
/// <returns>返回的受影响的行数/returns>
public static int CUD(string sql)
{
SqlConnection con = null;
try
{
//创建数据库连接对象
con = new SqlConnection(ConnStr);
//打开数据库连接
con.Open();
//创建命令对象
SqlCommand com = new SqlCommand(sql, con);
int index = com.ExecuteNonQuery();
return index;
}
catch (Exception ex)
{
return -1;
}
finally
{
con.Close();
}
}
#endregion
#region 增、删、改(参数化SQL语句)
/// <summary>
/// 增、删、改(参数化SQL语句)
/// </summary>
/// <param name="sql">参数化SQL语句</param>
/// <returns>返回的受影响的行数/returns>
public static int CUD(string sql, List<SqlParameter> sqlParams)
{
SqlConnection con = null;
try
{
//创建数据库连接对象
con = new SqlConnection(ConnStr);
//打开数据库连接
con.Open();
//创建命令对象
SqlCommand com = new SqlCommand(sql, con);
//ToArray()用于将集合转换为数组
com.Parameters.AddRange(sqlParams.ToArray());
int index = com.ExecuteNonQuery();
return index;
}
catch (Exception ex)
{
return -1;
}
finally
{
con.Close();
}
}
#endregion
#region 执行查询操作,返回SqlDataReader对象
/// <summary>
/// 执行查询操作,返回SqlDataReader对象
/// </summary>
/// <param name="sql">普通的SQL语句</param>
/// <returns>返回的SqlDataReader对象</returns>
public static SqlDataReader GetRead(string sql)
{
Console.WriteLine(ConnStr);
SqlConnection con = null;
try
{
con = new SqlConnection(ConnStr);
con.Open();
SqlCommand com = new SqlCommand(sql, con);
//CommandBehavior.CloseConnection用于设置当SqlDataReader关闭时会自动关闭连接
SqlDataReader dr = com.ExecuteReader(CommandBehavior.CloseConnection);
//SqlDataReader dr = com.ExecuteReader();
return dr;
}
catch (Exception ex)
{
return null;
}
//finally
//{
// con.Close();
//}
}
#endregion
#region 执行查询操作,返回SqlDataReader对象(参数化SQL语句)
/// <summary>
/// 执行查询操作,返回SqlDataReader对象(参数化SQL语句)
/// </summary>
/// <param name="sql">参数化SQL语句</param>
/// <returns>返回的SqlDataReader对象</returns>
public static SqlDataReader GetRead(string sql, List<SqlParameter> parameters)
{
SqlConnection con = null;
try
{
con = new SqlConnection(ConnStr);
con.Open();
SqlCommand com = new SqlCommand(sql, con);
com.Parameters.AddRange(parameters.ToArray());
//CommandBehavior.CloseConnection用于设置当SqlDataReader关闭时会自动关闭连接
SqlDataReader dr = com.ExecuteReader(CommandBehavior.CloseConnection);
return dr;
}
catch (Exception ex)
{
return null;
}
//finally
//{
// con.Close();
//}
}
#endregion
#region 执行查询操作,返回DataTable对象(普通SQL语句)
/// <summary>
/// 执行查询操作,返回DataTable对象(普通SQL语句)
/// </summary>
/// <param name="sql">普通sql语句</param>
/// <returns>返回DataTable</returns>
public static DataTable GetDateTable(string sql)
{
SqlConnection con = null;
try
{
con = new SqlConnection(ConnStr);
con.Open();
SqlDataAdapter da = new SqlDataAdapter(sql, con);
DataTable dt = new DataTable();
da.Fill(dt);
return dt;
}
catch (Exception ex)
{
return null;
}
finally
{
con.Close();
}
}
#endregion
#region 执行查询操作,返回DataTable对象(参数化SQL语句)
/// <summary>
/// 执行查询操作,返回DataTable对象(参数化SQL语句)
/// </summary>
/// <param name="sql">带@参数化SQL语句</param>
/// <returns>返回DataTable</returns>
public static DataTable GetDateTable(string sql, List<SqlParameter> parameters)
{
SqlConnection con = null;
try
{
con = new SqlConnection(ConnStr);
con.Open();
SqlCommand com = new SqlCommand(sql, con);
com.Parameters.AddRange(parameters.ToArray());
SqlDataAdapter da = new SqlDataAdapter();
da.SelectCommand = com;
DataTable dt = new DataTable();
da.Fill(dt);
return dt;
}
catch (Exception ex)
{
return null;
}
finally
{
con.Close();
}
}
#endregion
#region 执行查询语句,返回第一行第一个单元格中的数据(普通SQL语句)
/// <summary>
/// 执行查询语句,返回第一行第一个单元格中的数据
/// </summary>
/// <param name="sql">普通的SQL语句</param>
/// <returns>返回第一行第一个单元格中的数据</returns>
public static object GetScalar(string sql)
{
SqlConnection con = null;
try
{
con = new SqlConnection(ConnStr);
con.Open();
SqlCommand com = new SqlCommand(sql, con);
object result = com.ExecuteScalar();
return result;
}
catch (Exception ex)
{
return null;
}
finally
{
con.Close();
}
}
#endregion
#region 执行查询语句,返回第一行第一个单元格中的数据(参数化SQL语句)
/// <summary>
/// 执行查询语句,返回第一行第一个单元格中的数据(参数化SQL语句)
/// </summary>
/// <param name="sql">参数化SQL语句</param>
/// <returns>返回第一行第一个单元格中的数据</returns>
public static object GetScalar(string sql, List<SqlParameter> parameters)
{
SqlConnection con = null;
try
{
con = new SqlConnection(ConnStr);
con.Open();
SqlCommand com = new SqlCommand(sql, con);
com.Parameters.AddRange(parameters.ToArray());
object result = com.ExecuteScalar();
return result;
}
catch (Exception ex)
{
return null;
}
finally
{
con.Close();
}
}
#endregion
}
}
封装了常用 ADO.NET 操作:
- CUD():执行增删改(支持参数化防 SQL 注入)
- GetDataTable() / GetRead():查询返回 DataTable 或 SqlDataReader
- GetScalar():获取单值(如 COUNT)
并且使用 using 或 try-finally 确保连接关闭,避免资源泄漏。
MDI多文档界面
- 主窗体设为 IsMdiContainer = true
- 子窗体通过 MdiParent = this 嵌入
- 防重复打开:遍历 MdiChildren 判断是否已存在同名窗体
开发遇见的问题:
当我们对学员列表点击多次, 那学员列表就会打开多个, 怎么避免重复打开同一个子窗口
解决方案: 检查是否已经打开了此MDI窗体,就弹窗提示,并结束;避免重复打开同一个子窗口
代码示例:
private void studentListToolStripMenuItem_Click(object sender, EventArgs e)
{
// 检查是否已经打开了此MDI窗体,避免重复打开同一个子窗口
foreach (Form childrenForm in this.MdiChildren)
{
// 检查是不是当前子窗体名称
if (childrenForm.Name == "StudentListForm")
{
// 是则显示,并激活该窗体
childrenForm.Visible = true;
childrenForm.Activate();
MessageBox.Show(childrenForm.Name + "已经打开,请勿重复打开同一个子窗口");
return;
}
}
StudentListForm studentListForm = new StudentListForm();
studentListForm.MdiParent = this; // 设置为主窗体的子窗体
studentListForm.Show(); // 显示子窗体
}
四、登录流程(含第三方UI)
使用SunnyUI的UILoginForm快速构建美观登录页
方法:

图8
把继承基类的名字由Form改成UILoginForm
然后我们的登录页面就变成了SunnyUI的登录页面,此方法用于快速构建登录页面,你也可以不用它的,自己设计,也许会更好看;

图9
登录验证流程:
UI层→BLL.LoginO→ DAL查询 Admin 表
成功后将用户信息存入全局上下文(Admincontext.CurrentAdmin)
主界面通过该上下文显示当前用户名(如状态栏)
三层架构中的角色和职责:
| 表示层 (UI) | MySchool.UI | LoginForm.cs MainForm.cs |
接收用户输入 调用 BLL 登录方法 处理登录结果(跳转/提示) 保存当前用户到全局上下文 |
| 业务逻辑层 (BLL) | MySchool.BLL | AdminBLL.Login(string loginId, string pwd) | 参数校验 调用 DAL 获取用户 比对密码(明文 or 加密) 返回验证结果 |
| 数据访问层 (DAL) | MySchool.DAL | AdminDAL.GetAdminByLoginId(string loginId) | 执行 SQL 查询 从 SqlDataReader 构造 AdminModel 返回实体或 null |
| 模型层 | MySchool.Models | AdminModel | 封装 Admin 表字段(LoginId, LoginPwd, UserName 等) |
| 工具层 | MySchool.Utils | AdminContext.CurrentAdmin | 静态属性,保存当前登录用户 |
关键代码实现:
1.模型层:AdminModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MySchool.Models
{
//internal: 本项目可见
//public 公开的
public class AdminModel
{
public AdminModel()
{
}
public AdminModel(int adminId, string loginId, string loginPwd)
{
AdminId = adminId;
LoginId = loginId;
LoginPwd = loginPwd;
}
public int AdminId { get; set; }
public string LoginId { get; set; }
public string LoginPwd { get; set; }
public override string ToString()
{
return $"AdminId:{AdminId},LoginId:{LoginId},LoginPwd:{LoginPwd}";
}
}
}
2.数据访问层:AdminDAL.cs
using MySchool.Models;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MySchool.DAL
{
/// <summary>
/// Admin的数据访问类
/// </summary>
public class AdminDAL
{
/// <summary>
/// 根据主键查询
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public AdminModel QueryById(int? id)
{
string sql = "select * from admin where adminId = @Id";
List<SqlParameter> sqlParameters = new List<SqlParameter>() {
new SqlParameter("Id",id)
};
using (SqlDataReader dataReader = DBHelper.GetRead(sql, sqlParameters)) {
AdminModel admin = null;
if (dataReader.Read())
{
//创建对象
admin = new AdminModel();
//给属性赋值
admin.AdminId = Convert.ToInt32(dataReader["adminId"]);
admin.LoginId = dataReader["loginId"].ToString();
admin.LoginPwd = dataReader["LoginPwd"].ToString();
}
return admin;
}
}
public AdminModel QueryByUserName(string userName)
{
string sql = "select * from admin where loginId = @username";
List<SqlParameter> sqlParameters = new List<SqlParameter>() {
new SqlParameter("username",userName)
};
using (SqlDataReader dataReader = DBHelper.GetRead(sql, sqlParameters))
{
AdminModel admin = null;
if (dataReader.Read())
{
//创建对象
admin = new AdminModel();
//给属性赋值
admin.AdminId = Convert.ToInt32(dataReader["adminId"]);
admin.LoginId = dataReader["loginId"].ToString();
admin.LoginPwd = dataReader["LoginPwd"].ToString();
}
return admin;
}
}
}
}
使用 SqlParameter 防止 SQL 注入。
3.业务逻辑层 AdminBLL.cs
using MySchool.DAL;
using MySchool.Models;
using MySchool.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MySchool.BLL
{
/// <summary>
/// Admin业务类
/// </summary>
public class AdminBLL
{
private AdminDAL adminDAL = new AdminDAL();
public AdminModel FindById(int? id)
{
//数据校验
if (id == null)
{
throw new ArgumentNullException("id不能为空");
}
return adminDAL.QueryById(id);
}
// 密码存储在数据库, 加密的
/// <summary>
/// 登录方法
/// </summary>
/// <param name="userName">用户名</param>
/// <param name="password">密码</param>
/// <returns>true:登录成功, false: 失败</returns>
public bool Login(string userName, string password)
{
AdminModel admin = adminDAL.QueryByUserName(userName);
if (admin != null)
{
//判断密码是否一样
if (admin.LoginPwd == password) {
//把登录Admin信息保存
UserContext.LoginUser = admin;
return true;
}
}
return false;
}
}
}
表示层(UI层):LoginForm.cs(使用 SunnyUI)

图10
全局用户上下文:UserContext.cs MySchool.Utils层
using MySchool.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MySchool.Utils
{
/// <summary>
/// 存储登录的用户信息的帮助类
/// </summary>
public static class UserContext
{
/// <summary>
/// 私有的镜头字段
/// </summary>
private static AdminModel _loginUser;
/// <summary>
/// 静态属性, 存储的是当前登录用户信息
/// </summary>
public static AdminModel LoginUser
{
get
{
return _loginUser;
}
set
{
_loginUser = value;
}
}
}
}
使用方式(例如在 MainForm 中显示用户名)
lblWelcome.Text = $"欢迎你,{UserContext.LoginUser.UserName}!";
三层调用关系总结
UI (LoginForm) –> BLL (AdminBLL.Login) –>DAL (AdminDAL.GetAdminByLoginId) –>DB (SQL Server – Admin 表)
依赖方向:UI → BLL → DAL → Models
数据流向:DB → DAL(构造 Model)→ BLL(验证)→ UI(展示/跳转)
状态共享:通过 UserContext 静态类实现跨窗体用户信息共享
好处:
注意:
ADO.NET 技术实现数据库操作的标准流程:
查询:
增删改:
1. 查询操作逻辑
- 核心是通过 DBHelper.GetReader() 获取 SqlDataReader 对象,再将查询结果转化为业务模型(Model):
- 若返回多行数据,需将每行转为 Model 后存入 List 集合;
- 若仅返回单行,直接转为单个 Model 对象。
- 流程:编写SQL → 处理参数 → 执行查询获取数据读取器 → 解析数据为模型 → 返回数据。
2. 增删改(CUD)操作逻辑
- 核心是通过 DBHelper.CUD() 执行增删改操作,返回受影响的行数(int 类型)。
- 流程:编写SQL → 处理参数 → 执行CUD操作获取影响行数 → 返回数据。
关键差异
- 查询 侧重“数据读取与模型转换”,需处理 SqlDataReader 的读取和模型映射;
- 增删改 侧重“执行操作并获取影响行数”,直接通过 CUD() 方法返回执行结果(影响行数)。
效果:
输入admin数据库表中的用户的用户名和密码,即可跳转至主页面;


图11
输入表中对应的用户名和密码,点击登录按钮,登录成功跳转至主页面,并且底部显示了用户信息;用户名密码输入错误,也会弹框反馈给用户;点击取消按钮,退出程序;



图12
五、查询、添加、修改、删除模块
1. 复杂查询:多表联查
为了在列表中同时显示学生、班级和状态信息,必须进行 三表联查 (Student JOIN Class JOIN UserState)。
模型设计:StudentModel 中包含 ClassModel 和 UserStateModel 两个导航属性,实现对象关系映射(ORM 的简易版)。
SQL 示例:
SELECT s.*, us.UserState, c.ClassName, c.GradeID
FROM Student s
JOIN UserState us ON s.UserStateId = us.UserStateId
JOIN Class c ON s.ClassId = c.ClassId
2. 性能优化:分页查询(Pagination)
面对大量数据,一次性加载所有记录是不可行的。解决方案是 服务端分页。
- 核心思想:每次只从数据库获取当前页所需的数据。
- 关键技术:使用 SQL Server 的 OFFSET … FETCH NEXT 语法。
- 分页模型:创建通用的 PageModel<T> 封装分页信息(当前页、页大小、总记录数、总页数、当前页数据)。
- 流程:
- DAL 层提供 SelectPage(pageIndex, pageSize) 方法执行分页 SQL。
- BLL 层调用 DAL 获取数据,并计算总记录数(通过 Count() 方法),填充 PageModel。
- UI 层接收 PageModel,绑定到 DataGridView 并更新分页控件。
3. 高级功能:动态条件分页查询
用户需要根据姓名、性别、班级、地址、状态等任意组合进行筛选。
- 条件封装:创建 StudentCondition 类,将所有可能的查询条件集中管理。
- 动态 SQL 拼接:在 DAL 层,根据 StudentCondition 对象的值,动态构建 WHERE 子句。
- 使用 StringBuilder 拼接 SQL。
- 使用 参数化查询 防止 SQL 注入。
- 初始条件设为 WHERE 1=1,方便后续 AND 拼接。
- UI 交互:
- 班级和状态下拉框 (ComboBox) 的选项需要从数据库动态加载(通过 ClassBLL 和 UserStateBLL)。
- “查询”按钮点击时,收集表单数据构造 StudentCondition 对象,并触发第一页的条件分页查询。
- 分页控件的 PageChanged 事件也需要带上相同的 StudentCondition 对象,以保证翻页时条件不变。
4. CRUD 全链路实现
新增 (Create):表单验证(非空检查);构造 StudentModel 对象;调用 StudentBLL.AddStudent() → StudentDAL.Insert(),执行 INSERT 语句。
- 修改 (Update):
- 数据传递:通过 EditStudentForm 的 Student 属性,将选中行的数据从列表页传递到编辑页。
- 编辑页 Load 事件中,将 Student 对象的属性回填到各个控件。
- 提交时,将修改后的数据组装成 StudentModel,调用 Update 方法执行 UPDATE … WHERE StudentId = @StudentId。
- 删除 (Delete):
- 在 DataGridView 的 CellContentClick 事件中捕获“删除”按钮点击。
- 弹出确认对话框。
- 调用 DeleteById 方法执行 DELETE 语句,并从 DataGridView 中移除对应行。
5. UI/UX 细节处理
窗体复用:通过 FormNavigator 工具类统一管理 MDI 子窗体的打开逻辑,避免重复代码,并确保同一类型窗体不被多次打开。
数据绑定:使用 BindingSource 作为 DataGridView 的数据源,这是一种更优雅和强大的绑定方式。
空值处理:在读取数据库字段时(如 Phone, Address),使用 dataReader.IsDBNull() 检查是否为 NULL,避免异常。
总结:
效果展示:
运行程序

图13
添加学生


图14
学生列表有我们刚刚添加的信息;

图15
再刷新数据库,看学生表是否新增了这条信息;

图16
可以看到数据库有我们的刚刚手动添加的学生信息,证明添加学生模块完成;
查询模块:
1.查询所有,与数据库信息同步,DataGridView里共10条记录,因为使用了分页查询,让每一页只显示1条信息;并且到了第一页就不能让用户点击上一页按钮,到了第10页就不能让用户点击下一页按钮,符合设计标准;

图17
2.带条件查询(可以单条件,也能多条件),实现条件筛选;
单条件:


图18
多条件:
本质是使用了模糊查询;


图19
查询用户苏洋的信息:

图20
修改苏洋的信息:
点击DataGridView的编辑按钮,会显示用户的原本信息;


图21
我们修改我们要修改的信息,比如我把地址改为旧金山、登录密码改为1301、班级改为S2103;

图22
点击修改按钮,弹框显示修改成功,点击确定;

图23
这时候我们再点击查询,就查不到了,因为用户苏洋的相关信息已经被修改了;


图24
再返回数据库进行刷新,看看是否真正修改成功了;

图25
数据库信息已经被修改成功,说明ADO操作数据库成功;
删除模块:点击删除按钮,删除用户即可
六、总结
| UI 层硬编码数据库逻辑 | 三层架构分离(UI→BLL→DAL) | 业务变更无需修改 UI |
| 一次性加载全量数据 | 服务端分页 + 动态条件查询 | 10万条数据流畅无卡顿 |
| 密码明文存储 | 参数化 SQL + 安全设计 | 防止 SQL 注入(教学版) |
| 全局状态散落各处 | UserContext 单例管理 | 跨窗体权限控制零成本 |
当学生管理模块需要新增“状态筛选”时,只需在 StudentCondition 类中加一行属性,BLL 层自动适配,UI 层无需改动——这就是分层架构的终极价值。
关键技术突破点
1. 分页查询:从“卡顿”到“丝滑”的革命
// DAL 层:动态生成分页 SQL(避免全表扫描)
public PageModel<StudentModel> GetPage(int pageIndex, int pageSize, StudentCondition condition)
{
string baseSql = "SELECT s.*, c.ClassName, us.UserState FROM Student s …";
string whereSql = BuildWhereCondition(condition); // 动态拼接 WHERE
string countSql = $"SELECT COUNT(*) FROM Student {whereSql}";
int total = DBHelper.GetScalar(countSql); // 获取总记录数
string pageSql = $"{baseSql} {whereSql} ORDER BY StudentId OFFSET {pageIndex} ROWS FETCH NEXT {pageSize} ROWS ONLY";
var list = new List<StudentModel>();
using (var reader = DBHelper.GetReader(pageSql))
{
while (reader.Read())
list.Add(ConvertToModel(reader));
}
return new PageModel<StudentModel>(total, list);
}
早期项目常因 SELECT * 一次性加载 10 万条数据导致卡死,而 服务端分页 让查询时间从 10s+ 降至 200ms。
2.登录流程

图26
3. 全局状态管理:告别“传参地狱”
// 任意窗体中获取当前用户
public partial class MainForm : Form
{
public MainForm()
{
lblWelcome.Text = $"欢迎,{AdminContext.CurrentAdmin.UserName}!";
}
}
// 登录成功后设置
AdminContext.CurrentAdmin = admin; // 1 行代码,全局生效
从“在 10 个窗体中传递 AdminModel” → “1 行代码全局访问”,开发效率提升 40%
MySchool 项目的价值,不在于它实现了多少功能,而在于它用最朴素的 WinForm + ADO.NET 诠释了企业级开发的核心原则:
分层(让变更成本可控)
分页(让性能不成为瓶颈)
状态管理(让代码逻辑清晰)
这个项目还是有很多值得学习的地方,还需继续钻研…


![P11246 [GESP202409 六级] 小杨和整数拆分-171主机测评](https://www.171host.com/wp-content/uploads/2026/11/20261108100631-6af049a75855c-220x150.png)


