AI编程+CSFrameworkV6旗舰版Winform开发技能:csframework_winform_skills.md

作者:jonny  发布日期:2026/10/24 11:41:45

AI编程+CSFrameworkV6旗舰版Winform开发技能:csframework_winform_skills.md

本文以真实 ERP 项目基于CSFrameworkV6旗舰版(.NET 8 WinForms + DevExpress 22.1 + EF Core + SQL Server)开发的实践为基础,反向提炼出一套可直接落地的 C/S 桌面应用开发标准,覆盖分层架构、命名、Designer.cs 职责、数据绑定、查询窗体、网格开发、事务落库与代码评审清单,可作为团队开发规范、代码评审依据与新人培训教材。

开发框架版本:https://www.csframework.com/cs-framework-6.0.htm (CSFrameworkV6.1旗舰版)

 

一、技术栈与分层架构

1、三层职责边界

C/S 桌面应用建议固定 UI 窗体 → 业务层(bll)→ 数据层(dal) 的单向调用链,各层职责不得越界:

层 命名约定 职责边界
UI 窗体 frm_业务码 取界面输入、调业务层、展示结果;禁止写 SQL、禁止直连数据库
业务层 bll_业务码 业务编排、读系统参数、调用数据层;不得被数据层反向引用(防循环依赖)
数据层 dal_业务码 全部 SQL/LINQ 落库、事务、业务校验(抛业务异常)、写数据日志

2、实体与目录组织

实体按用途分三类:tb_ 前缀(ORM 映射物理表)、req_ 前缀(查询参数类)、res_ 前缀(非 ORM 投影/展示类,不加 Table 特性、不写建表脚本)。窗体文件(.cs + .Designer.cs + .resx)成对存放在所属模块项目内,按业务模块划分程序集,而不是自造 Forms/Helper/Model 目录。

二、命名规范

1、类型命名

类型 前缀 示例
业务单据/查询窗体 frm_ frm_IN_Material(采购入库单)、frm_SettleLogQuery
模态弹窗 frmDialog_ frmDialog_SelectDO
数据层 / 业务层 dal_ / bll_ dal_PO / bll_PO
ORM 实体 tb_ / dt_ / sys_ tb_PO、dt_Product、sys_系统表
查询参数 / 投影类 req_ / res_ req_PurchaseHistoryQuery、res_tb_PO
报表 rpt_ rpt_FA.frx

2、控件命名

控件 约定命名 说明
GridControl / GridView gc / gv + 语义 汇总页 gcSummary/gvSummary,明细页 gcDetail/gvDetail,弹窗 gcList/gvList
网格列 col + 字段名 colCustomerCode、colSupplierName
查询条件输入 txt_ 前缀 txt_QueryInfo(快速搜索)、txt_DocDateFrom/To
按钮 btn + 动作 btnQuery(查询)、btnEmpty(清空)、btnOk/btnCancel
窗体标题 lblFormTitle 业务术语以此为准,避免以讹传讹

禁止 button1/textBox1 这类默认名;控件 Name 在属性面板修改,由设计器同步到 Designer.cs。窗体中文本术语以 lblFormTitle.Text、菜单 Text、报表为准——项目实测中曾出现记忆与实际标题不一致(如把"付款申请单"记成"费用申请单"),动手前先核对。

三、窗体基类体系

业务窗体必须继承框架基类,把日志、权限、按钮状态、数据加载等公共能力收敛到基类,禁止自建 BaseForm/MsgHelper 轮子:

基类 适用场景 关键能力
frmBaseChild 查询界面(上条件区 + 下网格) AssertSearch 查询条件校验
frmBaseDataForm 基础资料/数据字典维护 汇总页/明细页页签、导航条
frmBaseDialog 模态弹窗 内容区 + 确定/取消按钮区
frmBaseBusinessForm 业务单据窗体 查看/编辑状态机、按钮状态门控

生命周期分工:构造函数第一行 InitializeComponent(),之后只做轻量初始化(禁止查库);Form_Load 只做网格布局注册、字典绑定、快捷键绑定,查询窗体默认不自动查数据,由【查询】按钮触发。

四、Designer.cs 职责与常见坑

固定 UI(控件、布局、列、事件)固化在 Designer.cs;主 .cs 只写业务逻辑。Designer.cs 内禁止 if/循环/SQL/业务逻辑/耗时操作,禁止在 InitializeComponent 里 new GridColumn。

两个高频坑(均来自真实踩坑):

① 编辑页标题栏加按钮 = Designer 4 处改动,缺一处不显示或编译不过:

1、字段声明;2、InitializeComponent 里 new 实例;3、pnlFormTitle.Controls.Add(...);4、属性块(FlatStyle、Cursor、尺寸、Location、Text、Click)。往已有按钮行插入时,必须连带重排其后按钮的 X 坐标,并确认面板宽度够用。

② 网格加列必须固化到 Designer(五件套),不要运行时动态加列:

1、字段声明;2、new GridColumn 实例化;3、列配置块(Caption/FieldName/AllowEdit/Visible/VisibleIndex/Width);4、Columns.AddRange 数组插入;5、原 VisibleIndex ≥ 插入点的列全部顺延 +N。改完用脚本提取全部 VisibleIndex 与 0..N-1 连续序列比对终检,防止列重叠错乱。

另外注意:多数模块未开隐式 using,Designer 文件里引用工具类要写全限定名;图标走统一的 images 目录加载(如 Globals.LoadImage("32_Refresh.png")),不要为加图标去改 .resx 的 base64。

五、数据绑定规范

实测很多框架项目并不使用 BindingSource 作为中间层,而是封装了按控件名反射绑定的机制——控件命名 txt+字段名 后,一行代码完成整面板绑定:

//编辑面板整批绑定:按控件名 txt+字段名 反射绑定(框架封装)
CommonTools.DoBindingEditorPanel(pcDetailEditor, currentEntity, "txt");

//单控件绑定(绑 EditValue)
DataBinder.BindingTextEdit(txt_Customer, entity, "CustomerName");

//代码赋值并触发 EditValueChanged 事件
txt_Customer.SetBindingValue(value, true);

//网格数据源:直接赋值投影类 List
gcDetail.DataSource = list;
gvDetail.ClearFilters();

公共数据字典下拉用统一绑定方法(DisplayMember=名称字段、ValueMember=代码字段);多选场景用 CheckedComboBoxEdit,其 EditValue 本身就是分隔字符串,与 string 模型字段天然对齐。

六、查询窗体标准写法

查询条件区固定布局:快速搜索框(浅绿底提示可输入)+ 日期三段式(范围下拉/开始/结束 + 按范围查勾选)+ 查询/清空按钮(HotFlat 风格、手型光标、带图标)。两段必写逻辑:

private void btnQuery_Click(object sender, EventArgs e)
{
    //性能优化:查询条件不能为空
    if (!AssertSearch(gcFindGroup, chk_ByDate)) return;

    try
    {
        frmWaitingEx.ShowMe(this);
        var list = new bll_XXX().Query(GetQueryParams());
        gcDetail.DataSource = list;
        gvDetail.ClearFilters();
        ColumnFieldTool.SetColumnFieldTitle(gvDetail, "物理表名"); //自动读数据字典取中文列名
    }
    finally { frmWaitingEx.HideMe(this); }
}

private void Form_Load(object sender, EventArgs e)
{
    frmGridCustomize.RegisterGrid(gvDetail);                        //注册网格:还原用户自定义布局
    DevStyle.SetGridControlLayout(gcDetail, false);
    txt_QueryInfo.HandleEnterKeyButton(btnQuery);                   //回车触发查询
    txt_DateRange.BindingDateRange(txt_DocDateFrom, txt_DocDateTo); //日期快捷区间(本日/本周/本月)
}

private void btnEmpty_Click(object sender, EventArgs e)
{
    CommonTools.ClearContainerEditorText(gcFindGroup);              //一键清空条件区
}

数字列加合计(SummaryItemType.Sum);个别计算列在自动取中文列名之后手动覆盖 Caption。

七、网格(Grid)开发规范

① 布局持久化:统一用 frmGridCustomize.RegisterGrid(gv) 注册网格,框架负责保存/还原用户调整后的列宽、列位置。锁列必须双保险——Designer 静态设置一次,运行时在 RegisterGrid 之后再设一次(RegisterGrid 会还原布局覆盖设置):

private void Form_Load(object sender, EventArgs e)
{
    frmGridCustomize.RegisterGrid(gvDetail);   //还原用户布局
    LockColumns();                             //★ 锁列放在 RegisterGrid 之后(双保险)
}

private void LockColumns()
{
    colCustomerCode.OptionsColumn.AllowEdit = false;
    colCustomerCode.OptionsColumn.ReadOnly = true;
}

② 勾选多选:用 gv.GetCheckedRows<T>() 取勾选行,依赖 OptionsSelection.MultiSelect 且需 AllowGridMultiSelect(true) 显示勾选框列;未勾选时兜底取当前焦点行。

③ 右键菜单:菜单项常量集中定义(GridMenuNames 静态类),AddMenuItem 统一挂接图标与处理函数,在 DoExecuteCmd 的 switch 里分发——禁止在事件里散落写死。

④ 防闪烁:大批量刷新成对调用 BeginUpdate()/EndUpdate()(try/finally 保证成对)。

八、业务落库范式(下推生成/事务/日志)

"源单下推生成目标单据"是 ERP 高频场景,统一按以下 DAL 模板:校验、落库同一事务;异常抛 CustomException 回滚,不留半截数据;单号必须在事务内取号;数据日志写系统库且在事务提交后写:

public tb_Xxx GenerateBySource(List<string> listSourceNos)   //★ List 支持批量
{
    var nos = (listSourceNos ?? new List<string>())
              .Where(w => !string.IsNullOrWhiteSpace(w)).Distinct().ToList();
    if (nos.Count == 0) throw new CustomException("请选择要下推的源单据!");

    try
    {
        _Database.BeginTransaction();

        //1、取源单:存在性、审核标志(FlagApp=='Y')、业务规则校验,全部抛 CustomException
        //2、防重复:按源单类型+号码查目标明细是否已生成
        //   ★ 单据类型必须用 nameof(DocTypeNames.X) 取"代码",常量值是中文名,写错编译不报错
        //3、事务内取单据号
        var no = DocNoHelper.GetDocNo(_Database, "单据码", _Loginer.Account);

        _Database.Add<tb_Xxx>(master);
        _Database.Add<tb_XxxDetail>(details);

        _Database.CommitTransaction();
        _Log.Log(DataLogType.add, "生成单据", nameof(tb_Xxx), no, _Loginer.Account, new { nos });
        return master;
    }
    catch (Exception ex)
    {
        _Database.RollbackTransaction();
        _Log.LogException(ex);
        throw;
    }
}

配套的"弹窗选择列表"范式:窗体继承 frmBaseDialog、构造器私有、对外只暴露静态 Execute(参数, out message) 方法——在弹窗确定事件里直接调 DAL 写库(成功才 DialogResult=OK),返回值带回提示信息;候选数据用 res_ 投影类承载。

★ nameof 铁律:单据类型常量类(如 DocTypeNames)的常量名是单据代码、常量值是中文显示名。判断数据库里的 SourceDocType/DocType 字段必须写 nameof(DocTypeNames.X)(得到 "X"),直接引用常量会得到中文,条件永远不成立且编译不报错、静默失效——这类 Bug 极难排查,代码评审必须盯住。

九、消息、异常与等待

禁止零散 MessageBox.Show,统一走框架封装:

场景 统一写法
业务校验失败(DAL/BLL) throw new CustomException("…");
UI 承接业务异常 catch (CustomException ex) { frmMessageInfo.ShowWarning(ex.Message); }
UI 承接未知异常 catch (Exception ex) { Msg.ShowException(ex); }(不把堆栈抛给用户)
确认框 if (Msg.AskQuestion("确定要生成吗?")) { … }
耗时操作 frmWaitingEx.ShowMe(this) / HideMe(this)(try/finally)
数据日志 写系统库日志表;事务提交后写;原值在赋值前采集

十、金额精度与算术铁律

金额计算是财务类系统的重灾区,全项目必须统一口径:

项 口径
明细金额 统一 2 位小数
单价 / 数量 6 位 / 3 位(成本计算链 4~6 位单独保持)
税额 差额法:税额 = ROUND(含税金额,2) − ROUND(不含税金额,2),保证价税合计恒等式
本币金额 ROUND(原币金额 × 汇率, 2);本币单价由本币金额反推
舍入 Math.Round 必须显式 MidpointRounding.AwayFromZero(默认银行家舍入会差 1 分钱)
类型转换 服务端算术禁用有歧义重载的 ToDecimal()/ToDouble() 扩展,用 Convert.ToDecimal/ToDouble
decimal amount = Math.Round(qty * price, 2, MidpointRounding.AwayFromZero);

十一、代码评审检查清单

每张业务窗体提交评审时逐项检查:

1、分层正确:UI 无 SQL;校验在 DAL/BLL 并抛 CustomException;数据层未反向引用业务层。
2、窗体继承了正确的框架基类,未自造 BaseForm/MsgHelper 轮子。
3、控件命名符合规范(gc/gv 语义名、txt_ 查询条件、col 列名),无默认控件名。
4、Designer 改动完整:标题栏按钮 4 处、加列五件套 + VisibleIndex 连续性终检通过。
5、Designer 无业务逻辑;图标走 images 目录统一加载,未动 .resx。
6、判断单据类型一律 nameof(常量),无直接引用常量值。
7、取号在事务内;校验/落库同一事务,异常回滚;数据日志事务提交后写系统库。
8、提示统一框架封装;等待统一等待窗体;无零散 MessageBox、无空 catch。
9、网格已注册布局;锁列双保险;勾选用 GetCheckedRows 且开了 MultiSelect;新列已固化 Designer。
10、金额 2 位小数、税额差额法、Math.Round 带 AwayFromZero、Convert 显式转换。
11、模型加字段先出 SQL 脚本(只增不删、幂等、放统一脚本目录),部署顺序=先脚本再更新客户端。

这套标准的价值在于"全部来自真实项目的踩坑复盘"——每一条规则背后都有一次返工或一次线上问题作为依据。建议团队以此为骨架,结合自身项目持续修订,并通过代码评审清单把它变成肌肉记忆。

 

了解更多 CSFrameworkV6 快速开发框架,请访问 C/S框架网。

 

csframework_winform_skills.rar
14 KB · 免费下载
下载附件

如下载失败,请邮件通知我们,或 QQ:23404761 留言。