当前位置: 首页 > news >正文

手把手教你用C# WinForms + ADO.NET + 三层思想打造“许愿墙”小项目

最近做了一个很有趣的小项目——“许愿墙”。用户可以提交自己的愿望,还能查询愿望总数、随机看一条别人的愿望,或者根据ID精确查找。今天就把从零到一的开发过程、封装技巧以及踩过的坑分享出来,希望能帮助正在学习WinForms和数据库编程的朋友。

一、项目效果

先看下最终界面:

界面用GroupBox进行了分区,整体比较清晰。

二、技术栈 & 开发环境

  • 语言:C#(WinForms)

  • 数据库:SQL Server 2012+

  • 数据访问:ADO.NET(SqlConnection, SqlCommand, SqlDataReader)

  • 配置管理ConfigurationManager读取App.config中的连接字符串

三、数据库设计

数据库名为XYQ,表名为XYQ(有点重复但无伤大雅),结构如下:

CREATE DATABASE XYQ; GO USE XYQ; GO CREATE TABLE XYQ ( ID INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(50) NOT NULL, XYText NVARCHAR(500) NOT NULL, XYDate DATETIME DEFAULT GETDATE() ); GO
  • ID自增主键,方便按ID查询。

  • XYDate默认当前时间,记录许愿时刻。

四、项目架构思路

为了让代码更清晰、易于维护,我采用了简单的三层思想

  • UI层makeWishWallFrom窗体,只负责界面交互和用户输入验证。

  • 数据访问层:独立的DBhelper类,封装所有数据库操作(增删改、聚合查询、单值查询、单行查询)。

  • 配置文件层App.config存储连接字符串,避免硬编码。

这样做的好处:如果以后数据库地址或密码变了,只需修改配置文件,无需改动任何C#代码。

五、核心功能实现与代码详解

1. 添加愿望(INSERT + 参数化查询)
private void button1_Click(object sender, EventArgs e) { // 非空验证 if (string.IsNullOrWhiteSpace(txt_name.Text)) { MessageBox.Show("请输入许愿人!"); txt_name.Focus(); return; } if (string.IsNullOrWhiteSpace(txt_content.Text)) { MessageBox.Show("请输入许愿内容!"); txt_content.Focus(); return; } string name = txt_name.Text; string content = txt_content.Text; // 参数化SQL,防止SQL注入 string sql = "INSERT INTO XYQ (Name, XYText) VALUES (@name, @content)"; SqlParameter[] parameters = { new SqlParameter("@name", name), new SqlParameter("@content", content) }; int count = DBhelper.CUD(sql, parameters); if (count > 0) { MessageBox.Show("许愿成功!"); txt_name.Clear(); txt_content.Clear(); txt_name.Focus(); } else { MessageBox.Show("许愿失败......请重新许愿?"); txt_name.Focus(); } }

★ 亮点:使用SqlParameter参数化查询,彻底杜绝SQL注入风险。同时做了完整的输入校验。

2. 查询愿望总数(COUNT + ExecuteScalar)
private void but_slct_sum_Click(object sender, EventArgs e) { string sql = "SELECT COUNT(*) FROM XYQ"; int count = DBhelper.CSA(sql); lab_CountSum.Text = count.ToString(); }

对应的DBhelper.CSA方法:

public static int CSA(string sql) { using (SqlConnection con = new SqlConnection(str)) using (SqlCommand com = new SqlCommand(sql, con)) { con.Open(); return (int)com.ExecuteScalar(); } }

注意ExecuteScalar返回object,需要显式转换为int。如果表为空,返回的NULL会转换失败?实际测试COUNT(*)永远不会为NULL,所以安全。

3. 随机查看愿望(RANDOM + TOP 1)

随机查看的实现思路:

  • 先获取表中最大ID(假设ID连续且从1开始自增)。

  • 生成1到最大ID之间的随机数。

  • 根据随机ID查询愿望内容。

private void but_slct_random_Click(object sender, EventArgs e) { Random random = new Random(); // 获取最大ID string maxIdSql = "SELECT TOP 1 ID FROM XYQ ORDER BY ID DESC"; int maxId = Convert.ToInt32(DBhelper.RD(maxIdSql)); int randomId = random.Next(1, maxId + 1); // 根据随机ID获取愿望内容 string contentSql = $"SELECT XYText FROM XYQ WHERE ID = {randomId}"; string wish = DBhelper.RD(contentSql); txt_slct_random.Text = wish; }

⚠️潜在问题:如果表中的ID不是连续的(比如删除了某些行),随机生成的ID可能查不到内容。后面优化部分会讨论解决方案。

4. 根据ID查询单行愿望(SqlDataReader)
private void but_slct_n_Click(object sender, EventArgs e) { // ID非空校验 + 数字校验 if (string.IsNullOrWhiteSpace(txt_ID_n.Text)) { MessageBox.Show("请输入要查询的愿望ID!"); return; } if (!int.TryParse(txt_ID_n.Text, out int id)) { MessageBox.Show("请输入正确的数字ID!"); return; } string sql = $"SELECT * FROM XYQ WHERE ID = {id}"; DBhelper.READLINE(sql, out string name, out string content, out string time); txt_name_n.Text = name; txt_content_n.Text = content; txt_time_n.Text = time; }

READLINE方法封装了SqlDataReader的使用:

public static void READLINE(string sql, out string name, out string content, out string time) { name = ""; content = ""; time = ""; using (SqlConnection con = new SqlConnection(str)) using (SqlCommand com = new SqlCommand(sql, con)) { con.Open(); using (SqlDataReader dr = com.ExecuteReader()) { if (dr.Read()) { name = dr["Name"].ToString(); content = dr["XYText"].ToString(); time = dr["XYDate"].ToString(); } } } }

注意:这里又出现了字符串拼接SQL!应该改为参数化查询。后面“遇到的问题”中会重点分析。

六、DBhelper封装详解

这是我比较自豪的部分,将数据库操作抽象成四个静态方法,极大简化了UI层的代码。

方法名用途返回值
CUD增删改操作(支持参数化)受影响行数
CSA聚合函数(如COUNT、SUM)int
RD查询单个值(如最大ID、愿望内容)string
READLINE查询单行多列,通过out参数返回void

连接字符串从App.config读取:

<connectionStrings> <add name="XYQ" connectionString="server=.;uid=sa;pwd=123;database=XYQ"/> </connectionStrings>

C#中读取方式:

private static string str = ConfigurationManager.ConnectionStrings["XYQ"].ConnectionString;

别忘了在项目中引用System.Configuration.dll,否则会报错。

七、开发中遇到的问题 & 反思

🔴 问题1:随机查看愿望可能失败(ID不连续)

现象:当表中ID不是连续递增时(例如删除了ID=2的记录),随机生成的ID可能不存在,导致文本框显示空或“暂无”。

解决方案(两种):

1.查询所有ID存入List,然后随机取一个:

string sql = "SELECT ID FROM XYQ"; List<int> ids = DBhelper.GetIdList(sql); // 自定义方法 int randomId = ids[random.Next(ids.Count)];

2.使用OFFSET-FETCH(SQL Server 2012+)直接随机取一条:

string sql = "SELECT XYText FROM XYQ ORDER BY NEWID()"; string wish = DBhelper.RD(sql);

这边推荐第二种,简单高效。

🔴 问题2:but_slct_n_Click中存在SQL注入风险

在查询指定ID的愿望时,用了字符串拼接:

string sql = $"SELECT * FROM XYQ WHERE ID = {id}";

虽然id是整数,理论上安全,但习惯上应该坚持参数化。修正后:

string sql = "SELECT * FROM XYQ WHERE ID = @id"; SqlParameter[] paras = { new SqlParameter("@id", id) }; // 但READLINE方法目前不支持参数传递,需要扩展
🔴 问题3:READLINE方法未关闭DataReader

虽然代码中使用了using (SqlDataReader dr = ...),会自动释放,但有个细节:SqlCommandSqlConnection都在using块中,顺序正确。没有问题。

不过建议将SqlDataReader也包裹在using中,已做到。

🔴 问题4:随机数生成器Random实例每次都新建

but_slct_random_Click中每次都new Random(),由于时间间隔很短,可能生成相同随机数。应该将Random定义为类级别的静态实例,或使用ThreadStatic

优化:

private static readonly Random _random = new Random(); // 在方法内使用 _random.Next(...)
🔴 问题5:未处理数据库连接异常

比如SQL Server服务未启动、数据库不存在等情况,程序会直接崩溃。应该在所有数据库操作的外层加上try-catch,给用户友好提示。

八、进一步优化建议

  1. 全部改为参数化查询:尤其是READLINERD方法,应支持SqlParameter[]参数。

  2. 显示所有愿望列表:添加DataGridView,让管理员能看到全部内容。

  3. 防止重复提交:点击“许愿”后禁用按钮,操作完成后再启用。

  4. 日志记录:将异常写入日志文件,便于排查问题。

九、总结

通过这个“许愿墙”小项目,我们练习了:

  • WinForms常用控件(TextBox、Button、GroupBox、RichTextBox)

  • ADO.NET核心对象(Connection、Command、DataReader、ExecuteScalar)

  • 参数化查询防止SQL注入

  • 封装通用的DBhelper类降低耦合

  • 配置文件管理连接字符串

感触最深的一点:代码必须要有安全意识。就算只是课堂练习,也尽量使用参数化查询,养成良好的习惯,以后工作中才不会踩坑。

希望这篇博客能帮你理清C#连接数据库的开发思路。如果你也动手实现了这个项目,遇到了其他问题,欢迎在评论区交流!

http://www.jsqmd.com/news/760767/

相关文章:

  • 初次使用Taotoken从注册到完成第一次API调用的全过程
  • 前端焦虑?收藏这份AI转型指南,助你从程序员变身AI产品经理!
  • 语音风格识别技术VStyle:从原理到应用实践
  • WebSailor-V2:基于强化学习的智能浏览器操作框架解析
  • 2026汽车与工业场景NTC热敏电阻传感器:DS18B20数字温度传感器/热敏电阻(NTC)温度传感器/热电偶温度传感器/选择指南 - 优质品牌商家
  • curl学习
  • 开源RTS游戏Unknown Horizons移植Godot引擎:架构重构与模块化实践
  • 手把手教你CNVD漏洞挖掘 + 资产收集(看完你也可以轻松做到!)网络安全实战教程分享
  • Dify工作流卡顿、输出异常、节点失联?3步定位+4类日志解析法,今天必须搞定
  • ARM虚拟化与big.LITTLE架构核心技术解析
  • 数学推理轨迹评估:从算法到教学实践
  • 告别手动填Token!SpringDoc + OAuth2一键登录Swagger UI的保姆级配置
  • VLA-4D:4D视觉与语言融合的智能机器人操作框架
  • 2026车身刮痕修复全攻略:胶粘拉拔修复、钢圈修复、铝钣金修复、不刮腻子钣金、保留原车漆、冰雹凹痕拉拔、冰雹吸坑选择指南 - 优质品牌商家
  • WEAVE多模态基准测试:跨模态认知智能评估新标准
  • 腾讯大模型二面:你会怎么设计一个大模型应用的后端架构?
  • Dify权限配置避坑手册:5个99%团队踩过的细粒度授权雷区及修复方案
  • Adobe Illustrator ReplaceItems.jsx:批量对象替换的终极解决方案
  • 如何快速上手Hanime1插件:Android动漫播放器完整指南
  • 2026年四川UPS电源厂家TOP5排行及核心能力盘点:四川工业ups电源/四川工业蓄电池/四川机房ups电源/选择指南 - 优质品牌商家
  • 别再只会插卡了!用示波器实测SIM卡上电时序与通信波形(附故障排查)
  • 2026乐山靠谱特色小吃店铺名录:乐山美食推荐、乐山美食攻略、本地人吃的绵绵冰是哪家、乐小吃、乐山人爱吃得小吃美食推荐选择指南 - 优质品牌商家
  • 爬虫进化论:用 asyncio.gather 把 Python 协程并发推向极致——从单线程阻塞到毫秒级万页抓取的实战之路
  • ECS 实例启动失败报错 InvalidInstanceType 如何排查?
  • Word表格与图文排版:让你的文档告别“车祸现场“
  • Valori内存管理优化AI系统性能与稳定性
  • 2026宜宾橱柜定制:宜宾实木全屋定制/宜宾工厂直接做全屋定制/宜宾性价比高的全屋定制/宜宾新房装修定制/宜宾本地全屋定制工厂/选择指南 - 优质品牌商家
  • 如何完整备份微信聊天记录:开源工具WeChatExporter全面指南
  • VideoCoF:基于帧链推理的创新视频编辑技术解析
  • Docker Compose启动Jumpserver报错?手把手教你解决‘mkdir /host_mnt/opt: permission denied‘