C# WinForms 架构与项目实战:多窗体通信、三层架构、数据库、日志与配置
引言
前两篇分别讲了 WinForms 的入门基础和进阶技术。但单个窗口、单个功能写得再好,也不等于能交付一个可维护的项目。
真正的项目开发,绕不开这五件事:怎么管理多个窗体、怎么分层、怎么连数据库、出错怎么兜底、配置放哪里。
这篇就把这五块讲透,让你从"会写控件"跨到"能搭项目"。
代码基于 .NET 6/8 + Visual Studio 2022,数据库以 SQL Server 为例(
System.Data.SqlClient或Microsoft.Data.SqlClient),.NET Framework 4.x同样适用。
一、多窗体开发与窗体通信
一个项目必然有多个窗体。核心问题是:窗体之间怎么传数据、怎么回调结果。
1.1 模态与非模态
csharp
// 模态:阻塞当前窗体,直到子窗体关闭(常用于对话框)
using (var f = new EditForm())
{
if (f.ShowDialog(this) == DialogResult.OK)
{
// 用户点了确定
}
}
// 非模态:两者可同时操作
var f2 = new MonitorForm();
f2.Show(this); // this 作为 Owner,子窗体始终在父窗体之上
1.2 五种传值方式
方式一:构造传参(最简单,适合一次性传入)
csharp
public class EditForm : Form
{
private readonly int _id;
public EditForm(int id)
{
_id = id;
InitializeComponent();
LoadData(_id);
}
}
// 调用:new EditForm(123).ShowDialog();
方式二:公开属性(适合双向传值)
csharp
public class EditForm : Form
{
public string UserName { get; set; } // 父窗体写入
public bool Saved { get; private set; } // 父窗体读取结果
}
// 父窗体
var f = new EditForm { UserName = "张三" };
f.ShowDialog();
if (f.Saved) RefreshList();
方式三:事件回调(子窗体主动通知父窗体,耦合最松)
csharp
public class EditForm : Form
{
public event Action<string>? DataSaved; // 保存成功时回调
private void btnSave_Click(object sender, EventArgs e)
{
// ... 保存逻辑
DataSaved?.Invoke(textBox1.Text);
Close();
}
}
// 父窗体订阅
var f = new EditForm();
f.DataSaved += name => listBox1.Items.Add(name);
f.Show();
方式四:DialogResult(标准的"确定/取消"结果)
csharp
// 子窗体:确定按钮的 DialogResult = DialogResult.OK,取消 = Cancel
// 父窗体通过 ShowDialog() 的返回值判断
方式五:静态服务/中介者(跨多个窗体共享状态,如"当前登录用户")
csharp
public static class Session
{
public static string CurrentUser { get; set; }
}
1.3 选型建议
一次性传参用构造或属性;需要"子窗体通知父窗体刷新"用事件;简单的"确定/取消"用 DialogResult;全局状态用静态服务。
二、三层架构
三层架构把代码分成表示层(UI)、业务逻辑层(BLL)、数据访问层(DAL) ,核心目的:UI 不碰 SQL,业务规则集中,数据访问可替换。
WinFormsApp
├─ Models/ # 数据模型(实体类)
├─ DAL/ # 数据访问层:只负责存取数据库
├─ BLL/ # 业务逻辑层:校验、规则、流程
└─ UI/ # 表示层:窗体、控件,只调用 BLL
2.1 分层示例
csharp
// Models/User.cs ------ 实体
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
// DAL/UserRepository.cs ------ 数据访问层
public class UserRepository
{
public User GetById(int id)
{
// 只写 SQL 和参数,不写业务规则
using var conn = DbHelper.GetConnection();
using var cmd = new SqlCommand("SELECT * FROM Users WHERE Id=@id", conn);
cmd.Parameters.AddWithValue("@id", id);
conn.Open();
using var reader = cmd.ExecuteReader();
return reader.Read()
? new User { Id = id, Name = reader["Name"].ToString()!, Email = reader["Email"].ToString()! }
: null!;
}
}
// BLL/UserService.cs ------ 业务逻辑层
public class UserService
{
private readonly UserRepository _repo = new();
public User GetUser(int id)
{
var user = _repo.GetById(id);
if (user == null)
throw new Exception("用户不存在");
// 这里可以加业务校验、缓存、权限等
return user;
}
}
// UI ------ 表示层
private void btnQuery_Click(object sender, EventArgs e)
{
var service = new UserService();
var user = service.GetUser(int.Parse(textBox1.Text));
label1.Text = user.Name;
}
关键原则:调用方向只能"从上到下"------UI 调 BLL,BLL 调 DAL,绝不能反过来,也不能 UI 直接调 DAL。实体类(Model)作为层与层之间的"运输工具"贯穿三层。
三、数据库操作
WinForms 传统做法是 ADO.NET,简单直接;追求效率可用轻量 ORM(Dapper)。
3.1 防 SQL 注入:参数化查询
绝对不要拼接 SQL 字符串,用参数化:
csharp
string sql = "SELECT * FROM Users WHERE Name = @name";
using var cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@name", textBox1.Text); // 参数化,防注入
3.2 一个 DbHelper 封装
csharp
public static class DbHelper
{
private static readonly string ConnStr =
ConfigurationManager.AppSettings["ConnStr"]!;
public static SqlConnection GetConnection() => new(ConnStr);
// 执行查询,返回 DataTable
public static DataTable Query(string sql, params SqlParameter[] ps)
{
using var conn = GetConnection();
using var cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddRange(ps);
using var adapter = new SqlDataAdapter(cmd);
var dt = new DataTable();
adapter.Fill(dt);
return dt;
}
// 执行增删改,返回影响行数
public static int Execute(string sql, params SqlParameter[] ps)
{
using var conn = GetConnection();
using var cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddRange(ps);
conn.Open();
return cmd.ExecuteNonQuery();
}
}
要点:
SqlConnection、SqlCommand都要using,确保连接归还连接池。- 增删改用
ExecuteNonQuery,查单个值用ExecuteScalar,查多行用SqlDataAdapter或ExecuteReader。 - 事务用
SqlTransaction,多条写操作要么全成功要么全回滚。
3.3 用 Dapper 简化
不想手写映射,用 Dapper 一行搞定:
csharp
using Dapper;
var users = conn.Query<User>("SELECT * FROM Users WHERE Age > @age",
new { age = 18 }).ToList();
四、异常处理与日志
4.1 局部异常:try-catch-finally
csharp
try
{
var user = new UserService().GetUser(id);
}
catch (Exception ex)
{
Log.Error(ex); // 记录日志
MessageBox.Show("操作失败:" + ex.Message); // 给用户友好提示
}
原则:捕获你能处理的异常 ,别无脑 catch 后吞掉;finally 里释放资源。
4.2 全局兜底:未捕获异常
用户操作触发的任何异常,都不该让程序直接崩掉。在 Program.cs 里注册全局处理:
csharp
[STAThread]
static void Main()
{
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
// UI 线程异常
Application.ThreadException += (s, e) =>
{
Log.Error(e.Exception);
MessageBox.Show("发生错误:" + e.Exception.Message, "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
};
// 非 UI 线程异常(进程即将崩溃,只能记录)
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
Log.Error(e.ExceptionObject as Exception);
Application.Run(new MainForm());
}
4.3 日志框架
手写写文件不如用成熟框架,推荐 NLog 或 log4net。以 NLog 为例:
csharp
// 全局静态日志器
public static class Log
{
private static readonly NLog.Logger Logger =
NLog.LogManager.GetCurrentClassLogger();
public static void Error(Exception ex) => Logger.Error(ex);
public static void Info(string msg) => Logger.Info(msg);
}
NLog.config 配置输出到文件 + 控制台,支持按日期滚动、按级别过滤。生产环境务必配好日志,否则线上问题无从排查。
五、配置文件管理
配置(连接串、接口地址、开关)要写进配置文件,而不是硬编码在代码里,改了不用重新编译。
5.1 .NET Framework:App.config
xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="ConnStr" value="Server=.;Database=MyDb;Trusted_Connection=True;" />
<add key="ApiUrl" value="https://api.example.com" />
</appSettings>
</configuration>
读取(需引用 System.Configuration):
csharp
string conn = ConfigurationManager.AppSettings["ConnStr"]!;
5.2 .NET 6/8:appsettings.json
json
{
"ConnectionStrings": {
"Default": "Server=.;Database=MyDb;Trusted_Connection=True;"
},
"AppSettings": {
"ApiUrl": "https://api.example.com"
}
}
读取(需安装 Microsoft.Extensions.Configuration.Json):
csharp
var config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
string conn = config.GetConnectionString("Default")!;
string apiUrl = config["AppSettings:ApiUrl"]!;
要点:.json 文件要设置"复制到输出目录=始终复制";敏感信息(密码、密钥)别明文入库,用环境变量或加密。
总结
架构和实战,拼的是组织代码的能力:
- 多窗体通信先想清楚数据流向,再选传值方式,别一上来就全局静态变量满天飞。
- 三层架构让"UI 不碰 SQL、业务规则集中",是 WinForms 项目可维护的关键。
- 数据库操作必须参数化 ,连接和命令用完
using。 - 异常要分两层:局部
try-catch给友好提示,全局ThreadException兜底,配日志框架落盘。 - 配置外置到
App.config/appsettings.json,别硬编码。
把前两篇的技术 + 这篇的架构串起来,你就拥有了交付一个完整 WinForms 桌面项目的能力。下一篇想深入哪个方向(比如完整的权限管理、Dapper + 三层架构的完整 Demo 项目),评论区告诉我。
本文为原创,转载注明出处。有用的话,点赞、收藏、关注,感谢支持!