简介:面向金蝶K/3 Cloud BOS插件开发初学者的入门文档,围绕BOS产品应用汇总中的BOSIDE、套打、万能报表等应用背景,讲解如何通过插件扩展标准业务、实现企业个性化与非标准化需求。资源包仅含1个docx文件,大小约1.9MB,以单一文档集中呈现环境配置、架构概念与开发入门内容,便于按章节查阅。已有216人学习。文档涵盖服务器与客户端配置要求、数据库字符集与排序规则、BOS动态表单模型及四类插件(动态表单、服务、校验规则、表单构建)的定位,并介绍Visual Studio 2012、Silverlight5、HttpWatch Pro等工具和公共/个人环境搭建方法;还以Hello World及采购收料单为例,梳理插件类定义、组件引用、方法重载、编译路径设置、IDE注册与运行测试流程,适合需要快速建立K/3 Cloud插件开发整体认知的初学者。
1. 一张采购收料单改到第三遍,我才回头补 K/3 Cloud 插件开发基础
接到「给采购收料单加个按钮、点一下弹提示」的需求时,很多人的第一反应是找源码改。真上手才知道,K/3 Cloud BOS 平台上的标准单据不能这么动,正确路径是写一个插件挂上去,用继承和重载把行为插进既有业务流。这份《初学者-k3cloud插件开发基础.docx》讲的就是这条路径:从 SQL Server 排序规则、Visual Studio 2012 引用、Common.config 里的管理中心地址,到四类插件的分工、动态表单视图与模型两个接口的边界、七个加载回调的先后顺序,最后落到一个能编译、能注册、能点出对话框的 HelloWorld。适合已经能看懂 C#、但还没在 BOS IDE 里亲手注册过一次插件的人;也适合带新人时当一份环境清单,省得对方在排序规则上翻车。
2. 环境与工程骨架:把 Visual Studio 2012 到 Bin 目录这条链路搭通
动手写第一行代码之前,先把「代码写在哪、编译产物落到哪、运行时从哪加载」这三件事确定下来。插件开发的绝大部分低级故障都出在这条链路上,而不是业务逻辑本身。这一章按配置基准、工程引用、输出路径、客户端依赖四个环节,把链路一段一段接上。
2.1 配置基准值:别拿一台 4G 内存的笔记本硬扛 Oracle
文档给出的配置建议区分了团队服务器和个人开发机两种口径,这个区分很实用。团队环境里数据库服务器 CPU 双核 2.0 GHz 以上,SQL Server 内存 8 GB、Oracle 内存 16 GB;纯客户端双核 2.0 GHz 以上、4 GB 内存就够跑 IDE。个人开发机把客户端和数据库塞在一台机器上时,SQL Server 版本给 4 GB 以上,Oracle 版本直接给到 8 GB 以上。经验上 Oracle 那条线内存给少了,光是实例启动加 BOS 服务加载就能把机器拖到没法编译。
| 角色 | CPU | 内存 | 备注 |
|---|---|---|---|
| 团队数据库服务器 | 4 核,2.0 GHz 以上 | SQL Server 8 GB;Oracle 16 GB | 多人共用,别省内存 |
| 纯客户端 | 双核,2.0 GHz 以上 | 推荐 4 GB | 只装 VS 和浏览器 |
| 个人客户端 + 数据库 | 双核,2.0 GHz 以上 | SQL Server 4 GB 以上;Oracle 8 GB 以上 | 单机全栈,磁盘 IO 是瓶颈 |
运行环境的版本约束同样要提前对齐:数据库侧 SQL Server 2008R2 以上(标准版或企业版)、Oracle 11g 以上;操作系统 Windows Server 2008 以上;Web 服务器 IIS 6.0 以上;运行时 Microsoft .NET Framework 4.0。这几个版本号看着老,但 BOS 平台的程序集就是按这套目标框架编的,把 VS 工程的目标框架随手改成 4.5 以上,编译能过,运行时加载程序集照样出问题。
数据库这一侧有两个硬指标必须在建库时就定死。用 SQL Server 时,排序规则必须是Chinese_PRC_CI_AS;用 Oracle 时,数据库字符集必须是AL32UTF8,国家字符集必须是AL16UTF16。这两条不是建议,是前置条件。库建好、数据灌进去之后再想改排序规则,基本等于重建库,血泪经验就是这一步千万别抱着「先跑起来再说」的心态跳过。
2.2 新建工程与引用:Kingdee.BOS 和 Kingdee.BOS.Core 两个引用不能少
打开 Visual Studio 2012,新建一个类库工程,命名按团队约定走,文档里用的示例是MyDev.K3.SCM.Stock.Business.PlugIn,从命名就能看出模块(SCM)、业务对象(Stock)、层(Business)和类型(PlugIn)。工程建好后加两个程序集引用:Kingdee.BOS和Kingdee.BOS.Core。前者是平台基础类型,后者承载动态表单、插件基类、事件参数这些二开最常碰的东西。
<!-- 工程文件里的引用片段,HintPath 指向产品安装目录下的 Bin --> <ItemGroup> <Reference Include="Kingdee.BOS"> <HintPath>D:\K3Cloud\K3CloudServer\Bin\Kingdee.BOS.dll</HintPath> <!-- 不要复制到输出目录,避免覆盖平台自身的运行时文件 --> <Private>False</Private> </Reference> <Reference Include="Kingdee.BOS.Core"> <HintPath>D:\K3Cloud\K3CloudServer\Bin\Kingdee.BOS.Core.dll</HintPath> <Private>False</Private> </Reference> </ItemGroup>这两个引用块的关键点在Private标签。设成False表示编译时不把平台 DLL 拷到输出目录。曾经有人图省事留了默认的True,结果每次编译都往 Bin 里塞一份旧版本Kingdee.BOS.dll,覆盖掉产品自带的文件,整套站点起不来,排查方向还容易跑到代码上去。引用路径建议直接用绝对路径指向产品安装目录,别用相对路径或者 GAC 里的同名程序集,版本对不上时现象极其隐蔽。
目标框架保持 .NET Framework 4.0,跟服务端运行环境一致。平台目标这一项没有强制要求,但要注意跟 IIS 应用程序池的位数对齐:应用程序池开了 32 位模式,编译产物却是 AnyCPU 之外的其他配置,加载时就会报类型初始化失败。这类报错信息通常不会直接告诉你位数不匹配,只能靠对齐配置来避免。
2.3 输出路径与管理中心地址:Common.config 里那行 IP
编译输出目录直接设成产品安装目录下的K3Cloud\K3CloudServer\Bin\。这一步决定了插件能不能被平台发现——IDE 里注册插件时浏览到的就是这个目录,DLL 落在别处,注册完运行起来必然是「找不到类型」或者干脆没反应。
<!-- 工程属性里的输出配置 --> <PropertyGroup> <!-- 直接产出到平台加载目录,省掉手工拷贝这一步 --> <OutputPath>D:\K3Cloud\K3CloudServer\Bin\</OutputPath> <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> </PropertyGroup>输出到平台目录有个副作用要提前知道:只要站点在运行,w3wp.exe就会把 Bin 下的 DLL 锁住,编译时直接报「文件被另一个进程占用」。常见的处理办法是停掉对应站点或回收应用程序池再编译,而不是去改输出路径绕开——绕开之后你还得手工拷回去,反而多一步且容易忘。
个人开发机还要改一处配置才能连上公共环境的管理中心。打开K3Cloud\K3CloudServer\App_Data\Common.config,找到managementSiteUrl这个键,把值改成公共环境里已经配置好管理中心的那台服务器地址。
<!-- K3Cloud\K3CloudServer\App_Data\Common.config --> <appSettings> <!-- 末尾的斜杠不要省,URL 拼接时少一个斜杠会出现 404 --> <add key="managementSiteUrl" value="http://192.168.73.40:8000/" /> </appSettings>改完之后重启站点,让配置生效。这一步顺序不能反:先装产品再改地址,改完再启动服务。有人先启动服务、再回头改文件却忘了重启,登录时一直连到本机那个空的管理中心,界面上什么数据中心都看不到,还以为是数据库没配好。
2.4 浏览器与 Silverlight 5:客户端这一侧的隐性依赖
服务端和 IDE 都通了,最后一道坎在客户端。文档给出的浏览器支持矩阵是:IE 8.0、9.0,32 位(x86)和 64 位(x64)都可以;Firefox 12 及以上,限 32 位 Windows 版本。两种浏览器都依赖 Silverlight 5 插件。
| 客户端组件 | 支持版本 | 支持架构 |
|---|---|---|
| Internet Explorer | 8.0、9.0 | 32 位(x86)、64 位(x64) |
| Firefox | 12 及以上 | 32 位(Windows) |
| Silverlight | 5 | 随浏览器位数匹配 |
这里的坑在于位数匹配。64 位 IE 需要 64 位 Silverlight,32 位 IE 需要 32 位 Silverlight,装错版本的表现是页面能打开、但业务控件区域一片空白或者一直转圈,控制台里未必有明确报错。另外别顺手把浏览器升到最新大版本,Silverlight 插件在新版浏览器上是否还能正常加载,取决于具体环境,不在支持矩阵里的组合出问题时没有任何排查抓手。
3. 四类插件与继承体系:基类选错,事件一个都不会进
环境通了,接下来是选型。BOS 平台的插件不是一个大而全的基类,而是按介入位置切成几类,继承错了,你重载的方法根本不会被调用,断点永远不命中。这一章把插件分类、继承关系、视图与模型两个接口、以及加载顺序讲清楚,后面写代码才知道该往哪个方法里塞逻辑。
3.1 动态表单插件、服务插件、校验规则插件、表单构建插件怎么分工
文档按应用场景把插件分成四类,这个划分是选型的起点。
| 插件类型 | 主要职责 | 典型用途 |
|---|---|---|
| 动态表单插件 | 封装单据上的业务事件、操作和服务介入 | 单据界面上的按钮响应、字段联动、数据校验提示 |
| 服务插件 | 基于动态表单服务端,提供服务编程接口 | 被外部服务直接调用的业务能力封装 |
| 校验规则插件 | 针对业务规则做校验 | 保存、提交前的规则拦截 |
| 表单构建插件 | 对界面加载的控件和元数据动态调整 | 按客户需求改界面结构,属于较重的手段 |
日常二开里绝大多数需求落在第一类。改个按钮、控个字段可见性、加个默认值,都是动态表单插件的活。服务插件更偏「把能力暴露出去」,校验规则插件适合把规则从代码里抽出来做成可配置项,表单构建插件则是动界面骨架,改动面大、影响面也大,非必要不用。
选型上有个判断顺序可以照抄:先问「这个需求是不是在界面上发生的」,是就走动态表单插件;再问「是不是纯数据规则」,是就考虑校验规则插件;最后才问「是不是要动界面结构」,是才上表单构建插件。跳过前两问直接上构建插件,改出来的东西往往耦合度高、后续升级难。
3.2 单据、基础资料、动态表单、列表:AbstractBillPlugIn 这一族的继承链
动态表单插件下面还要再分一次。文档给出的基类清单是四支:单据插件用Kingdee.BOS.Core.Bill.PlugIn.AbstractBillPlugIn,基础资料插件用Kingdee.BOS.Core.Base.PlugIn.AbstractBasePlugIn,动态表单插件用Kingdee.BOS.Core.DynamicForm.PlugIn.AbstractDynamicFormPlugIn,列表插件用Kingdee.BOS.Core.List.PlugIn.AbstractListPlugIn。
继承体系上,最上层是AbstractDynamicFormPlugIn,单据插件和基础资料插件依次往下派生,列表插件单独一支。这个层级的实际意义在于:你继承AbstractBillPlugIn,就同时拿到了动态表单层和单据层的事件;继承AbstractDynamicFormPlugIn,只有通用表单层的事件。采购收料单是业务单据,所以继承AbstractBillPlugIn;如果挂到基础资料上却继承了单据基类,事件未必触发,反过来也一样。
命名空间也要对得上。AbstractBillPlugIn在Kingdee.BOS.Core.Bill.PlugIn,事件参数在Kingdee.BOS.Core.DynamicForm.PlugIn.Args,消息框类型MessageBoxType在Kingdee.BOS.Core.DynamicForm。这几个 using 漏一个,编译报的错是「找不到类型或命名空间」,看着像引用没加,实际只是少了一行 using。
3.3 IDynamicFormView 与 IDynamicFormModel:视图管外观,模型管数据
动态表单模型把「长什么样」和「怎么算」拆开了。外观由视图(View)控制,业务逻辑由模型(Model)控制,两者分别持有布局元数据LayoutInfo和业务元数据BusinessInfo,这两份元数据就是 IDE 里配的那些东西。运行时客户端发起请求,先读元数据初始化BusinessInfo和LayoutInfo,视图和模型再据此把表单渲染出来。
插件能直接访问的是IDynamicFormView和IDynamicFormModel两个接口。前者提供访问菜单、修改控件样式、设置标题、刷新界面、打开表单、动态注册插件这些能力,还负责操作转发;后者围绕数据,提供初始化、新增、复制、删除、定位当前分录行、设置值等方法。文档里提到的IDynamicFormViewService和IDynamicFormModelService是平台内部用的,二开不用碰。
模型侧最需要建立概念的是DataObject。它是当前表单的数据对象,类型是DynamicObject,包含单据头和单据体;单据体是集合对象DynamicObjectCollection,一张单据可以有多个单据体。平台默认用DynamicObject作数据承载类,也允许通过DynamicObjectType.ClrType指定自定义类,但要求这个类必须派生自DynamicObject。理解这一点,后面看到GetEntityDataObject、SetValue(key, value, row)这类带行号的写法就不会懵。
3.4 插件加载顺序:七个回调的先后关系
第一次访问表单时,平台先加载元数据,初始化视图和模型对象,初始化页面,然后创建数据包并绑定数据。二次开发能介入的时机就分布在这个过程里,顺序是固定的。
| 顺序 | 回调 | 时机说明 |
|---|---|---|
| 1 | OnInitialize | 页面初始化,可动态初始化控件数据源 |
| 2 | CreateNewData | 动态表单数据包创建,仅新增时触发 |
| 3 | AfterCreateNewEntryRow | 新增分录行后,每加一行触发一次 |
| 4 | AfterCreateNewData | 数据包创建完成,仅新增时触发 |
| 5 | BeforeBindData | 绑定数据前,可对数据做预处理,改动不进状态管理器 |
| 6 | AfterBindData | 绑定数据及控件状态后,加载和界面刷新都会调用 |
| 7 | BeforeClosed | 页面关闭前 |
这张表的用法很直接:需要在新增时给默认值,找第 2 或第 4 步;需要每加一行分录就补一个序号,找第 3 步;需要控制字段可见性、菜单可用性,找第 6 步。把可见性逻辑写到第 2 步,界面已经渲染完了才改属性,用户看到的是闪一下再变,体验和预期都对不上。
4. HelloWorld 全流程:从 BarItemClick 到 IDE 里勾选插件
前面都是铺垫,这一章把最小可运行闭环走完:写一个继承AbstractBillPlugIn的类、重载菜单点击事件、编译到 Bin、在 IDE 里注册、运行验证。这条链路走通一次,后面所有插件都是在这个骨架上加方法。
4.1 定义插件类并重载 BarItemClick
新建类ReceiptEdit,继承AbstractBillPlugIn。需求是点菜单弹一个提示框,对应重载BarItemClick。这个方法在菜单单击时触发,参数BarItemClickEventArgs里的BarItemKey就是 IDE 中菜单项的唯一标识。
using Kingdee.BOS.Core.Bill.PlugIn; using Kingdee.BOS.Core.DynamicForm; using Kingdee.BOS.Core.DynamicForm.PlugIn.Args; namespace MyDev.K3.SCM.Stock.Business.PlugIn { // 采购收料单属于业务单据,继承单据插件基类 public class ReceiptEdit : AbstractBillPlugIn { public override void BarItemClick(BarItemClickEventArgs e) { // 先调基类,保留平台自身的菜单处理逻辑 base.BarItemClick(e); // BarItemKey 与 IDE 中菜单项标识严格对应,区分大小写 if (e.BarItemKey == "HelloWorld") { this.View.ShowMessage("Hello world!", MessageBoxType.Notice); } } } }代码本身只有几行,三个细节值得单独说。第一,base.BarItemClick(e)必须放在最前面,平台内部的菜单处理、权限判断都在这条链上,漏掉会出现「某些菜单点了没反应」的怪现象。第二,BarItemKey是字符串硬比较,写错一个字母就是不进分支,而且不报错,只能靠断点确认。第三,弹框走this.View.ShowMessage,MessageBoxType.Notice是提示类型,需要警告或错误样式时换枚举值即可。
4.2 编译输出到 K3CloudServer\Bin
工程输出路径在 2.3 已经设成产品 Bin 目录,直接生成解决方案。如果这一步报文件占用,说明站点正在跑并且加载了这个 DLL,回收应用程序池或停站点再编译。
# 编译前先回收站点,避免 Bin 下的 DLL 被 w3wp 锁住 # 站点名按实际环境替换,回收后等几秒再执行编译 iisreset /stop # 编译完成后重新启动 iisreset /start用iisreset属于最省事的做法,生产环境当然不能这么干,本地开发机图快可以接受。更温和的方式是在 IIS 管理器里对单个应用程序池点「回收」,影响面小、速度快。编译成功后,去K3Cloud\K3CloudServer\Bin\下确认MyDev.K3.SCM.Stock.Business.PlugIn.dll的时间戳是刚才那一刻,这一步比任何日志都直观——时间戳没变,说明输出路径设错了或者编译走了别的配置。
4.3 在 BOS IDE 设计器里注册插件
注册这一步是新手最容易卡住的地方,因为它不在代码里,而在设计器里,顺序错了会以为前面白写了。完整步骤是:打开 IDE 设计器,找到目标单据(文档示例是采购收料单,单据标识类似_Bill的属性项);进入单据属性窗口,找到插件列表;点注册插件;在弹窗里点浏览,选中刚编译出来的 DLL;勾选要启用的插件类;确定返回;最后保存单据。
注册列表里可能已经存在其他插件,文档里专门提醒了一句:这些插件运行时会动态加载,删除它们可能导致业务数据错误。这句话的分量很重。正确做法是只动自己新增的那一条,不要去清理列表里看着陌生或者用不上的项——那些可能是标准产品的功能扩展点,删掉之后表象是某些业务字段不刷新或者校验失效,很难往「删了个插件」这个方向去想。
保存单据是必须的一步。只勾选不保存,注册信息只存在于当前设计器会话里,刷新页面就没了。保存之后,元数据才真正带上插件配置,运行时才会按配置动态加载。
4.4 运行验证与抓包定位
刷新单据页面,点 HelloWorld 菜单,应该弹出提示框。如果没弹,先按三个方向排查:插件列表里勾的是不是当前这个类、DLL 时间戳是不是刚编译的、菜单项的 Key 是不是和代码里写的一致。这三个都对还不行,就上抓包工具。
文档里提到的跟踪工具是 HttpWatch Pro 6.0,用途是看客户端和服务端之间到底发生了什么。插件里通过CreateNewData、AfterCreateNewData这类早期事件设置的值会随数据包一起下发到客户端,在 HTTP 响应里能直接看到。反过来,如果某个字段在界面上没值、但服务端日志显示赋过值,抓包就能判断是下发阶段丢了还是客户端绑定阶段被覆盖了。这个工具在排查「值到底有没有出服务端」这类问题时,比加一堆日志快得多。
5. 避坑排查:五个把我按在工位上的问题
这些坑的共同点是:报错信息指向的方向和真实原因基本无关。按「现象 → 原因 → 解决」记下来,下次遇到能少绕半小时。
现象一:注册完插件,点菜单毫无反应,断点也不进。原因通常有两个层面。浅层是 DLL 没有真正落到K3Cloud\K3CloudServer\Bin\,或者落的是上一版;深层是 IDE 里勾选的插件类跟 DLL 里的类不是同一个,改了命名空间或类名之后重新编译,注册信息还指着老类型。解决:先看 Bin 下 DLL 的时间戳,再回 IDE 插件列表确认类名全称,最后回收站点让新 DLL 真正被加载。改过类名的话,把旧注册项取消勾选再勾新的,别只是覆盖保存。
现象二:新增单据时默认值生效,打开已有单据却不生效。CreateNewData和AfterCreateNewData只在新增时触发,打开既有单据走的是加载已有数据的路径,这两个回调根本不进来。把「打开时也要处理」的逻辑写在这里,就等于只对新建单据有效。解决:区分需求语义,属于新建默认值的留在原处,属于每次打开都要执行的(比如根据当前组织刷新某个字段显示)挪到AfterBindData,那里加载和刷新都会调用。
现象三:在AfterCreateNewData里SetValue的值,保存时没进库。这个阶段数据包刚创建完,插件里设置的值不会进状态管理器,值会随整个数据包一起发到客户端,但从状态管理角度看这些字段没被「修改」过。如果后续逻辑依赖脏标记判断,就会出现值看着有、保存却没带上。解决:明确用途——这个回调适合做「初始展示」;需要参与状态管理、需要触发后续校验和保存的赋值,放到AfterBindData或者通过 IDE 里的实体服务规则配置,让平台自己管状态。
现象四:清理了插件列表里几个不认识的插件,业务数据开始出问题。单据属性里的插件列表可能包含标准产品注册的扩展点,运行时会动态加载。删掉或者取消勾选,表面上界面还能打开,但某些字段联动、校验、下推逻辑就静默失效了。这类故障没有明确报错,只能靠对比元数据发现差异。解决:改动插件列表前先备份单据元数据,只对自己新增的条目做增删,其他条目一律不动。真需要排查某个插件的作用,用取消勾选加单点验证的方式,别直接删。
现象五:中文乱码、字符串比较结果异常,或者安装检查直接不通过。根子在数据库。SQL Server 排序规则没设成Chinese_PRC_CI_AS,Oracle 的字符集不是AL32UTF8、国家字符集不是AL16UTF16,都会在中文字段排序、比较、存储上出问题,表现形式还特别分散,容易误判成代码问题。解决:建库阶段就按这两个值定死,装完再改的代价接近重建库。接手别人搭好的环境时,先查这两项再开始写代码,能省掉后面大量说不清的排查。
6. 事件里做减法的技巧:把可见性、赋值和状态管理分开放
AfterBindData是最容易被写成大杂烩的回调,加载和界面刷新都会进来,很多人把所有逻辑一股脑塞进去,结果每次刷新都全量重算,界面卡顿还查不出原因。我的习惯是把它拆成三块职责:菜单和控件状态、字段可见性、依赖当前数据的展示逻辑,各自独立成方法,AfterBindData里只做编排。
public override void AfterBindData(EventArgs e) { base.AfterBindData(e); // 主菜单项:参数是工具栏按钮的 Key,IDE 里能看到 this.View.GetMainBarItem("tbNew").Visible = false; // 分录菜单:第一个参数是实体 Key,第二个是菜单项 Key this.View.GetBarItem("FEntity", "tbAdd").Enabled = true; // 页签级隐藏:第二个参数传 null 表示整个页签 if (_modelTypeId == "BASE") { this.View.StyleManager.SetVisible("FTab_Field", null, false); } }三个调用的参数口径不一样,这是最容易写错的地方。GetMainBarItem只收一个菜单 Key,管的是主工具栏;GetBarItem要传实体 Key 加菜单 Key,管的是分录工具栏;StyleManager.SetVisible收控件 Key 加字段 Key,第二个参数传null表示作用于整个容器。把GetBarItem的实体 Key 写成单据头标识,运行时不会报错,只是设置不生效,这类静默失败只能靠对照 IDE 里的实体标识逐个核对。
判断逻辑也别写死在代码里。上面那个_modelTypeId应该来自单据类型字段的实际值,而不是常量;需要按单据类型分支的场景,用字段值判断比用单据标识判断更稳,因为同一张单据挂不同单据类型时,元数据是同一份。验证方式很简单:改一个单据类型保存,重新打开,看页签和菜单是否按预期切换;如果切了类型没变化,多半是判断值取错了来源。
还有一个我踩过的点:一段代码不知道放哪个回调时,先问「这个值需不需要进状态管理器」。需要,就往后放,让平台记录变更;不需要,只是给用户看的展示状态,就往前放。想清楚这一条,就不会把默认值、可见性、脏标记全挤在一个方法里。从那以后我每次新写插件,都强制按「菜单状态 / 字段可见性 / 数据赋值」三块分开落方法,再回头改需求时至少知道该动哪一块。希望帮到你。
本文还有配套的精品资源,点击获取