简介:面向 K/3 Cloud BOS 插件开发零基础学习者的入门文档,聚焦动态领域模型下的扩展开发,帮助读者理解 BOS 产品应用汇总中 BOSIDE、套打、万能报表等核心能力与插件开发的关系,适合刚接触金蝶 K/3 Cloud 二开的技术人员、实施顾问与在校学生作为起步材料。资源包共 1 个 docx 文件,约 1.9MB,内容以图文并排的说明文档形式组织,便于边看边在 Visual Studio 中对照操作。文档从环境配置要求讲起,列出团队服务器与客户端在 CPU、内存上的基准值,数据库、Web 服务器与浏览器插件(IE 8.0/9.0、Firefox 12、Silverlight 5)的版本要求,并提示 SQL Server 排序规则与 Oracle 字符集的注意事项;随后解析 BOS 架构模型与动态表单模型,区分动态表单插件、服务插件、校验规则插件、表单构建插件四类插件的适用场景,再以 Hello World 为例走完定义插件类、重载 BarItemClick、设置编译路径、注册插件到运行测试的完整流程,附有开发工具清单与公共环境、个人环境搭建要点。已有 216 人学习,适合作为查配置、认接口、跑通第一个插件的参考手册。
1. 从一个只能在菜单上点一下的需求说起
刚接到的需求很简单:采购收料单上要加一个自定义菜单,点一下弹个提示框,把当前单据的关键信息显示出来。翻遍标准功能找不到这个按钮,IDE 里的界面配置也只能对已有操作做加减,配不出一个新的入口。这类“标准产品没给出扩展点”的需求,就是 BOS 插件开发要用的地方:平台把一张单据拆成元数据、引擎、插件三层,菜单点击、控件显隐、数据包组装、分录行默认值都能在插件里接管。这份《插件开发基础》笔记适合三类人:刚接触 BOS 二次开发、想先把 HelloWorld 跑通的新手;需要判断某个需求该用插件还是该改元数据的实施;以及接手别人插件工程的维护者。它不讲理念,直接给环境版本、工程引用、编译路径,再落到 BarItemClick、CreateNewData、AfterBindData 这些真正要写代码的地方。
2. 环境与版本矩阵:Visual Studio 2012、Silverlight 5 与数据库排序规则
插件工程本身不挑机器,挑机器的是它要挂载的那个 BOS 运行环境。所以开工前先要把“跑得起来”这件事确定下来,否则后面写完代码编译通过、注册成功,打开单据一片空白,你会花很长时间去怀疑代码,实际上问题在客户端少装了一个 Silverlight 插件。
2.1 硬件基准:团队服务器与个人客户端的差别
平台是 Windows 上的 BOS 服务,开发工具是 Windows 上的 Visual Studio,没有 Linux 开发端这一说。按实际使用经验,硬件基准可以照下面这个表准备,不要为了省机器把数据库和 Web 服务挤在一台低配机器上,编译一次等三分钟,调试体验会崩。
| 角色 | CPU | 内存 |
|---|---|---|
| 团队数据库服务器 | 4 核,2.0 GHz 以上 | SQL Server 8 GB;Oracle 16 GB |
| 客户端 | 双核,2.0 GHz 以上 | 推荐 4 GB |
| 个人客户端 + 数据库合装 | 双核,2.0 GHz 以上 | SQL Server 4 GB 以上;Oracle 8 GB 以上 |
注意 Oracle 那条的差距:同样一台机器,Oracle 的内存基准是 SQL Server 的两倍。原因是 Oracle 实例自身的 SGA/PGA 占用更重,8 GB 内存跑一个“能连上”的库没问题,跑一个“能顺畅编译注册插件”的库就偏紧。个人开发机上如果只能装 Oracle,按 8 GB 起步,别卡在 4 GB。
服务端运行环境的版本要求:
| 组件 | 版本 | 操作系统 |
|---|---|---|
| 数据库服务器 | SQL Server 2008R2 以上(标准版、企业版);Oracle 11g 以上 | Windows Server 2008 以上;Linux x86-64;AIX |
| WEB 服务器 | IIS 6.0 以上;.NET Framework 4.0 | Windows Server 2008 以上 |
客户端环境的要求要单独记住,它是最容易翻车的一环:
| 浏览器 | 支持版本 | 支持架构 | 浏览器插件 |
|---|---|---|---|
| Internet Explorer | 8.0、9.0 | 32 位(x86)、64 位(x64) | Silverlight 5 |
| Firefox | 12 及以上 | 32 位(Windows) | Silverlight 5 |
BOS 客户端的表单是 Silverlight 渲染的。这句话的意思很具体:机器上没装 Silverlight 5,或者装了个版本对不上的,浏览器能打开登录页,但进入单据就是白屏或者只剩表头,控制台也没有明显报错。我一般装完系统第一件事就是把 Silverlight 5 装上,再确认 IE 的位数和插件位数一致,这一步花五分钟,能省掉后面半小时的瞎猜。
2.2 公共环境与个人环境:两台机器怎么分工
BOS 的标准做法是环境分两层,别在一台机器上凑齐所有东西。
公共环境负责“被连”:
- 配一台数据库服务器,安装 SQL Server 2008R2;
- 配一台 Web 服务器,安装产品,并配置为管理中心站点。
个人环境负责“开发”:
- 按上面的配置要求装好 Visual Studio,再装一份产品,但这一份不配置管理中心;
- 把个人环境的管理中心地址指向公共环境那台机器。
第 2 步是新手最容易漏的。产品装完之后,个人环境的配置文件里仍然指向localhost,插件注册到本机,运行时却连到公共环境,现象就是“插件在本地怎么点都有反应,一放到公共环境就没了”。要改的位置在产品安装目录下:
<!-- 产品安装目录\K3CloudServer\App_Data\Common.config --> <!-- 把 managementSiteUrl 指向公共环境里那台已经配置为管理中心的机器 --> <add key="managementSiteUrl" value="http://192.168.73.40:8000/" />这里有三个参数要核对:协议是http不是https;IP 是公共环境 Web 服务器的内网地址,不是本机回环地址;端口8000是管理中心站点的端口,如果你在 IIS 里改过站点绑定,这里必须同步改。改完保存,重启一下相关服务,再打开 IDE 登录,能看到公共环境的应用列表,说明这条链路已经对了。
2.3 装完才发现的两个初始化参数:排序规则与字符集
这两个参数只在建库那一刻设置有效,事后想改基本等于重装。
用 SQL Server 时,实例的排序规则必须是Chinese_PRC_CI_AS。用 Oracle 时,数据库字符集必须是AL32UTF8,国家字符集必须是AL16UTF16。这两条不是“建议”,是硬性要求。排序规则不对,轻则中文排序乱序、重则附加数据库直接失败;字符集不对,装完能跑,但一段时间后开始出现中文乱码,而且修起来比重装麻烦得多。
顺带把开发工具清单记一下,避免临时去找:
- Visual Studio 2012,主力开发环境;
- IE 插件 Silverlight 5,客户端渲染依赖;
- SQL Server 2008R2 或 Oracle 11g R2,数据库;
- HttpWatch Pro 6.0,抓客户端和服务端之间的数据包,调数据包问题时几乎必用;
- 插件编译输出路径:
K3Cloud\K3CloudServer\Bin\。
最后这一条是整篇里最重要的一个路径,下一章就围绕它展开。
3. 从工程到 Bin 目录:HelloWorld 插件的编译链路与 IDE 注册
新手写插件最容易产生的错觉是“代码写完就完事了”。实际上插件能不能被平台认出来,取决于三件事是否同时成立:工程引用对不对、DLL 有没有落到指定目录、IDE 里有没有把这个组件注册到目标单据上。三者缺一,代码写得再规范也看不到效果。
3.1 新建工程与必须加的引用
工程名建议按业务模块分好,例子里用的是MyDev.K3.SCM.Stock.Business.PlugIn——业务域在前,功能类型在后。这种命名不是强迫症,是为了半年后你能一眼看出这个 DLL 该注册到哪类单据上。
新建类ReceiptEdit,让它继承AbstractBillPlugIn。这是单据插件的抽象基类,继承它就自动获得了单据场景下的全部事件入口。工程里至少需要引用BOS和BOS.Core两个程序集。
提示:下文的命名空间统一省略了厂商前缀,你的环境里实际是「前缀 +
.BOS.Core.Bill.PlugIn」这种形式。照抄时先写using BOS.,剩下的靠 Visual Studio 的智能提示补全,比硬记全名可靠。
完整的最小可运行插件如下:
// 工程:MyDev.K3.SCM.Stock.Business.PlugIn // 引用:BOS、BOS.Core(指向产品安装目录下的 Bin) using BOS.Core.Bill.PlugIn; // AbstractBillPlugIn using BOS.Core.DynamicForm; // MessageBoxType using BOS.Core.DynamicForm.PlugIn.Args; // BarItemClickEventArgs namespace MyDev.K3.SCM.Stock.Business.PlugIn { /// <summary> /// 采购收料单插件:只做一件事——响应自定义菜单 HelloWorld /// </summary> public class ReceiptEdit : AbstractBillPlugIn { public override void BarItemClick(BarItemClickEventArgs e) { // 先跑基类:标准菜单的逻辑都在里面,吞掉它会导致原有按钮失效 base.BarItemClick(e); // BarItemKey 与 IDE 里菜单项标识严格一致,大小写敏感 if (e.BarItemKey == "HelloWorld") { // 第二个参数决定弹窗图标与按钮:Notice 只有一个确定按钮 this.View.ShowMessage("Hello world!", MessageBoxType.Notice); } } } }逐点说明这段代码里三个容易被忽略的地方。
base.BarItemClick(e)不能省。很多新手觉得“我又不关心别的按钮,基类不调也无所谓”,结果是注册插件后,单据上所有标准菜单都变成了点击无响应——因为标准逻辑正是由基类分发下去的。
e.BarItemKey是字符串比较,区分大小写。IDE 里菜单项标识写的是HelloWorld,代码里写成helloworld就永远不进 if,且没有任何报错。这是排查时最浪费时间的一类问题。
this.View.ShowMessage(...)里的View是视图对象,属于 View 层接口,负责展示。MessageBoxType.Notice是单按钮提示;如果要用户确认后继续,得换成带确认语义的重载,别拿 Notice 当确认框用。
3.2 编译输出路径:DLL 必须掉进 Bin
工程属性里把生成输出路径设成K3Cloud\K3CloudServer\Bin\。原因不玄学:平台在运行时是按程序集名去这个目录下反射加载插件的,你把 DLL 生成在bin\Debug里,IDE 里注册时浏览不到,或者注册完运行时报“找不到类型”。
我一般会额外挂一条生成后事件,省掉每次手动复制:
# Visual Studio 工程属性 → 生成事件 → 后期生成事件命令行 # $(TargetPath) 是本次生成的 DLL 完整路径,复制到产品 Bin 目录覆盖旧文件 copy /Y "$(TargetPath)" "D:\K3Cloud\K3CloudServer\Bin\MyDev.K3.SCM.Stock.Business.PlugIn.dll"参数就一个要注意:目标文件名必须和程序集名一致,重命名了 DLL 平台照样加载不到。另外平台位数和你的编译目标要匹配,没有特殊依赖时选AnyCPU最省事;如果引用了 32 位的第三方组件,就必须显式改成 x86,否则运行时直接抛BadImageFormatException。
改完代码后的标准动作是:重新生成 → 回收对应站点的应用池(或干脆重启 IIS)→ 刷新页面。跳过回收这一步,插件是旧版本在跑,你会以为自己的修改没生效。
3.3 IDE 里注册插件:插件列表要当生产配置对待
编译产物进了 Bin 之后,还差最后一步——告诉平台“这张单据要用这个插件”。
路径是:打开 IDE 设计器 → 找到单据属性窗口 → 编辑「采购收料单-_Bill」的单据属性 → 在插件列表里点注册插件 → 浏览选择刚编译好的组件 → 勾选插件类 → 确定退回 → 确定并保存单据。
这里的坑在插件列表本身。这个列表里可能已经注册了其他插件,它们在运行时是动态加载的,删掉某一条可能导致对应的业务数据出现异常——因为那些插件里可能包含数据校验、字段回写之类的逻辑。血泪经验是:只加不删,要停用某个插件就在测试账套里做,生产账套上不要随手清理。
保存之后直接运行测试:打开一张采购收料单,点 HelloWorld 菜单。能弹窗,说明引用、路径、注册三件事全部打通;弹不出来,按下一章的排查顺序走一遍,比盲目改代码高效得多。
4. 动态表单模型拆解:View 与 Model 两层接口和七个加载事件
HelloWorld 只能证明链路通了,真做需求时要动的是界面和数据。BOS 把这两块拆得很清楚:外观归 View,逻辑归 Model,两者都从元数据初始化出来,插件在中间插手。
4.1 视图层:IDynamicFormView 能拿到什么
一次表单访问的链路是这样的:客户端发起 HTTP 请求 → 服务端控制器服务接收 → 转给动态表单模型控制器 → 控制器访问动态表单视图。视图加载外观模型,并从模型层取数据。
视图对外只暴露两个接口:IDynamicFormView和IDynamicFormViewService。前者是插件直接用的,包含领域模型元数据、多视图模型接口、操作转发指令和通用属性方法;后者是框架内部的消息路由,插件开发不用管它。
IDynamicFormView上最关键的两个属性是BusinessInfo和LayoutInfo:前者是业务对象的逻辑元数据,后者是布局元数据,IDE 里配的表单外观全部落在这两个对象里。运行时先读元数据初始化它们,View 和 Model 再按元数据把界面画出来。
围绕这两个对象,视图接口给出了几类常用能力:
| 能力 | 典型调用 | 场景 |
|---|---|---|
| 访问菜单 | GetMainBarItem("tbNew")/GetBarItem("FEntity", "tbAdd") | 菜单隐藏、置灰 |
| 修改控件样式 | StyleManager.SetVisible("FTab_Field", null, false) | 页签、字段显隐 |
| 设置标题 | SetFormTitle(LocaleValue) | 多语言标题拼接 |
| 弹提示 | ShowMessage(...) | 操作结果反馈 |
| 执行操作 / 刷新 | 转发操作指令、发送客户端指令、打开表单 | 界面跳转与联动 |
SetFormTitle这类方法接的是LocaleValue而不是 string,因为标题要按用户语言环境存多份。初始化时通常按this.Context.UserLocale.LCID作为键塞进去,硬写一个中文字符串在单一语言环境里能跑,多语言环境下就露馅。
StyleManager.SetVisible的第一个参数是控件标识,第二个参数是行标识——分录体内的控件要传具体行,整块隐藏传null。这一步直接决定界面上“看不看得见”,不涉及数据,所以放在绑定完成之后做最稳。
4.2 模型层:IDynamicFormModel 与 DynamicObject 数据包
模型层同样是一对接口:IDynamicFormModel给插件用,IDynamicFormModelService是内部路由。IDynamicFormModel上最核心的属性是DataObject,也就是当前表单的数据对象。
DataObject是一个DynamicObject,单据头字段直接挂在它上面,单据体字段是一个DynamicObjectCollection,一张单据可以有多个单据体。平台默认用DynamicObject作为数据承载类型,也允许通过DynamicObjectType.ClrType指定自定义类,但那个自定义类必须派生自DynamicObject,否则加载时会失败。
模型层提供的方法都是围绕数据操作的:初始化、新增表单数据、复制数据、删除数据、定位当前分录行、设置值。所以一个经验性的分工是——要改“看起来怎么样”去 View,要改“值是什么”去 Model。写混了就会出现“界面变了但保存后没变”或者“数据变了但界面没刷新”这类问题。
4.3 四类插件与五个分支:继承体系与选型
平台层面把插件分成四类,用途差别很大:
| 插件类型 | 用途 | 使用频率 |
|---|---|---|
| 动态表单插件 | 介入单据上的各种事件、操作和服务,提供一系列事件/操作/服务接口 | 最常用 |
| 服务插件 | 基于动态表单服务端,提供服务编程接口,供外部服务直接调用 | 中 |
| 校验规则插件 | 针对业务规则做校验 | 中 |
| 表单构建插件 | 加载时对界面控件和元数据做动态调整 | 低 |
而动态表单插件内部又按业务形态分了五个分支,各自的基类不同,选错了会拿不到想要的事件:
| 分支 | 基类 | 典型场景 |
|---|---|---|
| 单据插件 | AbstractBillPlugIn | 采购收料单、销售订单等业务单据 |
| 基础资料插件 | AbstractBasePlugIn | 物料、客户、供应商等基础档案 |
| 动态表单插件 | AbstractDynamicFormPlugIn | 自定义弹窗、参数界面 |
| 列表插件 | AbstractListPlugIn | 列表界面的按钮、行操作 |
| 过滤条件插件 | 过滤条件专用基类 | 列表过滤方案 |
继承链上是从AbstractDynamicFormPlugIn派生出AbstractBillPlugIn,再往下才是AbstractBasePlugIn这一路。理解这条链的意义在于:单据插件天然拥有动态表单插件的全部事件能力,所以你在单据插件里既能接BarItemClick,也能接DataChanged。反过来,写在列表插件里的逻辑不要指望在单据界面上生效。
另外还有两个接口层面的抓手:实现IDynamicFormViewPlugIn可以接收 View 层事件,实现IDynamicFormModelPlugIn可以接收 Model 层事件。接口方式适合做横切逻辑(比如一批单据都要记操作日志),继承基类适合做单点业务定制。
4.4 加载顺序:七个事件谁先谁后,各自该干什么
这是整个插件体系里最需要背下来的一张顺序表。表单第一次被访问时,先加载元数据、初始化视图和模型、初始化页面,再创建数据包并绑定数据,插件就分布在这个过程的各个节点上。
| 顺序 | 事件 | 触发时机 | 适合做什么 | 不要做什么 |
|---|---|---|---|---|
| 1 | OnInitialize | 页面初始化 | 动态设置控件数据源、拼标题、准备全局参数 | 取业务数据 |
| 2 | CreateNewData | 数据包创建(仅新增触发) | 完全自定义数据包内容 | 依赖界面控件 |
| 3 | AfterCreateNewEntryRow | 每新增一行分录触发 | 分录行默认值、行内校验 | 一次性批量处理 |
| 4 | AfterCreateNewData | 数据包创建完成(仅新增) | 按参数动态赋默认值 | 频繁改数据 |
| 5 | AfterCreateModelData | 模型层数据包创建完成(仅新增) | 新增后联动展示 | 引起值变更 |
| 6 | BeforeBindData | 绑定数据前 | 按类型预建分录行、预置控件状态 | 期望改动被记录 |
| 7 | AfterBindData | 绑定数据及控件状态后 | 显隐控制、菜单启停 | 大量数据计算 |
| 8 | BeforeClosed | 页面关闭前 | 拦截关闭、保存提示 | 长耗时操作 |
两个关键区分点。
第一,CreateNewData和AfterCreateNewData只在“新增”时触发,打开已有单据不触发。所以“新增时给个默认值”和“编辑时根据字段变化重算”必须写在不同的地方。
第二,BeforeBindData里的数据修改不会被状态管理器记录,AfterBindData是加载和界面刷新都会调用的。这句话的实践含义是:你在BeforeBindData里新建的分录行会跟着数据包一起下发到客户端,但不会被当成“用户改动”,因此后续保存行为和你预期可能不一样。理解状态管理器这个概念,能省掉大量“值明明设了,保存后没了”的困惑。
5. 避坑与排查:插件不生效、字段不回写、分录行错位的五个现场
下面五条都是按“现象 → 原因 → 解决”整理的实操记录,顺序基本对应从环境到代码的排查路径。
5.1 部署与环境类
第 1 条:打开单据一片空白,只剩表头。
现象:登录正常,列表也正常,点进具体单据后界面区域全白,菜单栏有但没有任何字段,浏览器控制台没有明显异常。
原因:客户端 Silverlight 5 没装或版本不匹配。BOS 客户端的表单是 Silverlight 渲染的,缺少运行时就是渲染不出来,而不是报错。另一种可能是用了不受支持的浏览器版本,比如 IE 的兼容模式或者过高的 Firefox 版本。
解决:确认机器上装了 Silverlight 5,浏览器用 IE 8.0/9.0(32 位或 64 位,需与插件位数一致)或 Firefox 12 及以上 32 位版本。还有一个隐蔽原因——个人环境的Common.config里managementSiteUrl仍指向localhost,导致连的是本机那个没配管理中心的实例,表现出来同样是白屏,所以这一步要和浏览器插件一起查。
第 2 条:编译成功、注册成功,点菜单就是没反应。
现象:IDE 插件列表里能看到自己的插件,勾选也保存了,运行后点 HelloWorld 菜单毫无反应,也不报错。
原因:大概率为三类之一。一是 DLL 生成在bin\Debug而没有落到产品 Bin 目录,IDE 里注册的其实是旧版本文件;二是改了代码后没有回收应用池,跑的还是旧程序集;三是菜单标识字符串大小写不一致。
解决:先看输出路径,再回收应用池,最后核对e.BarItemKey与 IDE 里菜单项标识。排查顺序别反,先怀疑代码会让你在一个字符串上耗掉一下午。
5.2 事件与代码类
第 3 条:在创建数据包的事件里设的值,界面上看不到或者保存后丢失。
现象:在CreateNewData/AfterCreateNewData里SetValue赋值,界面打开时有值,但用户不动它直接保存,数据没落库;或者反过来,界面上压根没显示。
原因:这个阶段设的值不会进入状态管理器。数据包是在整体下发到客户端之前组装的,状态管理器没记录这次赋值,后续的保存逻辑自然不会把它当成“脏数据”提交。
解决:区分场景。新增时的缺省值用CreateNewData/AfterCreateNewData,这是对的;但如果是“根据某个字段变化重算另一个字段”,必须换到DataChanged(值改变后)或操作前事件里做,让改动进入状态管理器。实在需要在绑定阶段做,就明确这是一次性下发,不期待它被保存。
第 4 条:新增分录行时,序号出现重复或者跳号。
现象:在AfterCreateNewEntryRow里按“上一行最大值 + 1”算序号,结果新增两行序号一样,或者插入行之后序号乱掉。
原因:AfterCreateNewEntryRow在每次新增分录时都会触发,但“点击新增按钮追加”和“在中间插入一行”是不同路径。如果在取最大值时把当前这一行也算进去了,或者没判断e.Row是不是最后一行,插入场景就会算出错的值。
解决:先判断是对哪个实体触发的,再判断当前行位置,只在追加场景做批量取最大值。另外,这个事件每次新增行都会触发,如果赋值逻辑较重,应该挪到AfterCreateNewData里一次性处理。
/// <summary> /// 新增分录行后:仅在“追加到末尾”时补默认值 /// </summary> public override void AfterCreateNewEntryRow(CreateNewEntryEventArgs e) { base.AfterCreateNewEntryRow(e); // 只处理指定单据体,避免其他分录体触发同一段逻辑 if (!e.Entity.Key.Equals(CONST_ENG_Route.CONST_FSubEntity.ENTITY_FSubEntity)) return; var rows = this.Model.GetEntityDataObject(e.Entity); // 插入行不赋值:只有当前行是最后一行,才认为这是一次追加 if (e.Row != rows.Count() - 1) return; // 取已有最大值后按 10 向上取整,最大不超过 9999 int maxNo = rows.Select(w => w.GetDynamicObjectItemValue<int>( CONST_ENG_Route.CONST_FSubEntity.ORM_OperNumber)).Max(); int newNo = (int)Math.Ceiling(((decimal)maxNo + 1) / 10) * 10; this.Model.SetValue(CONST_ENG_Route.CONST_FSubEntity.KEY_FOperNumber, newNo > 9999 ? 9999 : newNo, e.Row); }这段里三个参数值得单独说:e.Entity.Key用来锁定单据体,一个单据有多个分录体时不做判断会互相串;e.Row是当前行的索引,配合Count() - 1判断追加/插入;this.Model.SetValue(key, value, row)的第三个参数是行号,漏掉它就直接写到单据头去了。另外提醒一句,纯字段默认值优先在 IDE 里配实体服务规则,能配出来的别写代码,后期维护成本差很多。
第 5 条:单据上有多个插件,字段值互相覆盖,只有一个“看起来生效”。
现象:同一个字段在两个插件里都赋值,运行结果每次不一样,或者明显是其中一个被另一个盖掉了。
原因:插件列表里注册了多个插件,对同一事件的执行顺序按注册顺序走,后执行的覆盖先执行的。加上如果其中一个插件是从标准插件继承后替代的,关系会更绕。
解决:把职责拆开,一个插件只负责一类事件的逻辑;公共逻辑抽到基类或者独立帮助类,不做重复赋值;能用元数据规则(实体服务规则、业务规则)表达的,就别塞进插件。另外注意,列表里已有的插件不要随手删——那些插件在运行时动态加载,删掉可能导致业务数据处理异常,尤其在已经有业务数据的账套上。
6. 二次开发改造:把 HelloWorld 变成能长期维护的扩展点
HelloWorld 只是入口,真正上线时这个插件会长出十几段逻辑。我一般按下面这张表做第一轮改造,把“能跑”变成“能维护”。
| 改造项 | 改造前 | 改造后 |
|---|---|---|
| 菜单标识 | 代码里硬编码"HelloWorld" | 提取为常量,或从配置读 |
| 按钮处理 | 一段 if 塞多个分支 | 每个菜单一个私有方法,if 只做分发 |
| 提示内容 | 固定字符串 | 按当前语言环境取多语言文本 |
| 数据取值 | 直接SetValue写死 | 走字段元数据对象取值,避免字段标识改名后静默失败 |
| 事件归属 | 所有逻辑堆在AfterBindData | 新增期逻辑放创建事件,变更期逻辑放DataChanged |
第一轮最值得做的是“事件归属”这一项。比如“新增单据时根据当前组织带出邮件服务器默认值”这种需求,正确位置是创建数据包之后的事件里,用组织 ID 构造过滤条件去查基础资料,再把查到的值写回当前数据包的字段。放在AfterBindData里也能“看起来正常”,但每次界面刷新都会重查一次数据库,单据一多查询压力会立刻显现。
第二件事是“字段访问走元数据”。写this.Model.SetValue("FServer", value)这种字符串直写,字段标识在 IDE 里一改名,代码编译照样通过、运行时静默不生效,属于典型的黑匣子问题。稳妥做法是先通过BusinessInfo.GetField("FServer")拿到字段对象,再用它的元数据属性去读写,改名字会直接抛异常,问题在开发期就暴露出来。
验证方法我固定走三步:先在测试账套里跑一遍新增(验证创建数据包那条链路)、再跑一遍打开已有单据(验证绑定与刷新)、最后跑一遍分录行追加和插入(验证e.Row的判断)。这三条路径覆盖了插件 90% 的出错场景。数据层面的核对用 HttpWatch 抓一次请求,看下发到客户端的数据包里字段到底有没有值,比盯着界面猜快得多。
还有一个习惯:任何插件注册前,先在测试账套把插件列表导出或者截图留档。有一次我在生产账套上“清理”了一个看起来很旧的插件,结果那是一个做数据校验的插件,第二天业务数据校验就出问题了,回头对账对了很久。从那以后我每次动插件列表之前都强制走一遍“先留档、再在测试账套验证”的流程,这条规矩比任何排查技巧都省事。希望帮到你。
本文还有配套的精品资源,点击获取