简介面向金蝶K/3 Cloud BOS插件开发的初学者这份docx文档围绕动态表单模型与插件扩展机制展开帮助零基础读者建立从环境配置到插件注册运行的完整认知。内容涵盖团队服务器与客户端配置基准、SQL Server与Oracle的字符集及排序规则要求、动态表单、服务、校验规则和表单构建四类插件的职责划分并给出Visual Studio 2012开发环境、Silverlight组件、编译输出路径与跟踪工具的实操要点附Hello World菜单响应示例及插件注册流程。资源包共1个docx文件约1.9MB篇幅集中适合通读与随查随用。目前已有216人学习下载对刚接触二次开发的实施人员而言可对照文档理清BOS架构分层、少走环境搭建弯路并据此完成第一个插件的编写与调试。1. 从一份 docx 说起K/3 Cloud 插件开发到底卡在哪很多人第一次打开一份 K/3 Cloud BOS 插件开发讲义看到 Visual Studio 2012、Silverlight 5、IIS 6.0 这些词第一反应是这资料有点年头了。但真正动手才会发现卡住新人的从来不是版本新旧而是三件事环境跑不起来、插件注册上去不生效、事件选错了改半天没反应。这份《初学者-k3cloud插件开发基础.docx》的价值就在这儿——它没有堆概念而是把环境基准值、四类插件的分工、动态表单的 View/Model 分层、以及 OnInitialize → CreateNewData → AfterBindData 这条加载链路讲透了还配了批量修改、邮件缺省值、分录自动编号这几个能直接抄的例子。适合已经会用 BOS IDE 画单据、但一写 C# 插件就翻车的同学也适合要带新人做二开的团队拿它当入门底稿。2. 环境先立住配置基准与公共/个人双环境拆分2.1 为什么开发机必须和服务器环境对齐插件开发最容易出现的玄学问题是代码在本地能编译、跑到测试环境就报错或者干脆插件加载不上。八成原因不在代码而在环境。这份资料给出的基准值很朴素团队数据库服务器 4 核 2.0 GHz 以上SQL Server 给 8 GB 内存、Oracle 给 16 GB客户端双核 2.0 GHz、4 GB 内存起步服务器操作系统 Windows Server 2008 以上数据库 SQL Server 2008R2 以上或 Oracle 11g 以上Web 服务器 IIS 6.0 以上.NET Framework 4.0客户端浏览器 IE 8/9 加 Silverlight 5Firefox 12 及以上也行但仅限 32 位 Windows。这些数字单看没意义组合起来才是约束。插件编译产物最终要丢进K3Cloud\K3CloudServer\Bin\被主程序加载所以开发机的 .NET 版本、程序集版本必须和服务器一致BOS IDE 设计器依赖 Silverlight浏览器不支持就等于设计器打不开插件注册那一步直接断掉。我一般会把这几条当成硬门槛先核对一遍而不是等报错了再回头查。提示资料里明确写了两个数据库初始化条件——SQL Server 排序规则必须是Chinese_PRC_CI_ASOracle 字符集必须是AL32UTF8、国家字符集AL16UTF16。这两项装完再改代价极高装库前确认。2.2 公共环境与个人环境的拆分步骤资料把环境拆成公共和个人两层这个划分很实用公共环境只建一次个人环境谁开发谁装。公共环境做两件事配置一台数据库服务器装 SQL Server 2008R2配置一台 Web 服务器装 K/3 Cloud 产品并配置为管理中心站点。个人环境则是装 Visual Studio、装 K/3 Cloud 产品不配置管理中心然后把管理中心地址指过去。关键就在最后一步改配置文件!-- 路径K3Cloud\K3CloudServer\App_Data\Common.config -- !-- 查找 managementSiteUrl改为公共环境管理中心的实际 IP 与端口 -- add keymanagementSiteUrl valuehttp://192.168.73.40:8000/ /逻辑说明个人环境的 K/3 Cloud 安装目录里App_Data\Common.config决定这台机器去哪个管理中心注册和取元数据。不改成公共环境地址登录后看不到公共环境建的业务对象插件注册也就无从谈起。参数上要注意端口示例是 8000实际以公共环境 IIS 站点绑定为准IP 用局域网固定地址别用会变的动态地址否则过几天又要改一次。配套的开发工具清单是Visual Studio 2012、IE Silverlight 5、SQL Server 2008R2 或 Oracle 11G R2、抓包跟踪工具资料写的是 HttpWatch Pro 6.0以及插件编译输出路径K3Cloud\K3CloudServer\Bin\。最后这项不是建议是必须——编译到别处IDE 里浏览插件时选不到程序集。2.3 编译输出路径为什么要固定# Visual Studio 中设置项目属性 - 生成 - 输出路径 # 指向 K/3 Cloud 安装目录下的服务端 Bin C:\Program Files\K3Cloud\K3CloudServer\Bin\逻辑说明BOS 的插件是运行时动态加载的IDE 里注册插件时浏览到的就是一个程序集文件运行时主进程从这个 Bin 目录加载。输出路径不规范会引出两类问题一是每次编译完要手工拷贝忘了拷贝就出现代码改了没生效的经典翻车二是拷贝时主进程占用文件编译报文件被另一个进程使用。常见做法是把输出路径直接指到 Bin编译前先关掉 IIS 站点或回收应用程序池避免文件锁。参数上如果你和同事共用一台开发服务器输出程序集名要带自己前缀否则互相覆盖这类血泪经验在多人环境里尤其值钱。3. 四类插件怎么选先分清动态表单、服务、校验与表单构建3.1 从 BOS 架构看插件的位置资料里那张架构模型图图 10-1说得很清楚动态表单模型封装了表单标准服务、操作和扩展服务通过引擎和标准插件调用来组装业务标准插件可以被业务插件继承和替代从而实现客户个性化需求。这句话翻译成人话就是平台把标准逻辑做成了可替换的零件你写的插件是挂在零件位上的自定义件不是绕过平台另起炉灶。基于这个定位资料给出四类插件插件类型作用位置典型用途动态表单插件单据/基础资料/列表/动态表单的界面与业务事件菜单响应、字段显隐、数据校验、赋值最常用服务插件服务端服务编程接口供外部服务直接调用做集成入口校验规则插件业务规则校验保存前做业务合法性判断表单构建插件界面加载控件与元数据动态调整控件与元数据满足特殊界面需求选型的第一原则是能在 IDE 配的不要写代码。资料在正文里反复强调字段缺省值优先用实体服务规则配置AfterCreateNewEntryRow只用于新增分录后必须做判断的场景。这条经验能省掉大量维护成本——配置是可读的插件是黑匣子后来接手的人翻代码才知道你干了什么。3.2 动态表单插件的五个子类与继承体系动态表单插件往下再分五类单据插件、列表插件、过滤条件插件、账表插件、动态表单插件。继承关系上业务模型类继承自抽象类表单插件具体是单据插件Kingdee.BOS.Core.Bill.PlugIn.AbstractBillPlugIn基础资料插件Kingdee.BOS.Core.Base.PlugIn.AbstractBasePlugIn动态表单插件Kingdee.BOS.Core.DynamicForm.PlugIn.AbstractDynamicFormPlugIn列表插件Kingdee.BOS.Core.List.PlugIn.AbstractListPlugIn选基类的判断依据是你挂在哪张表单上采购收料单、销售订单这种业务单据继承AbstractBillPlugIn供应商、物料这类基础资料继承AbstractBasePlugIn纯动态表单比如批量修改界面继承AbstractDynamicFormPlugIn列表界面继承AbstractListPlugIn。继承错了不会编译报错但事件不触发——这是新人最常见的困惑来源明明重载了方法运行时一点反应没有。3.3 第一个 Hello World 的完整走法资料给的入门例子虽小但把六个步骤都串起来了值得原样走一遍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); // 判断点击的是哪个菜单项key 与 IDE 中菜单标识一致 if (e.BarItemKey HelloWorld) { this.View.ShowMessage(Hello world!, MessageBoxType.Notice); } } } }逻辑说明BarItemClick是主菜单单击事件的扩展接口参数e.BarItemKey携带被点菜单的标识与 IDE 里配置的菜单 Key 严格对应大小写不一致就进不去分支。this.View是视图对象ShowMessage弹提示框MessageBoxType.Notice控制提示类型。这行代码看着简单但它验证了三件事程序集能被加载、插件能注册到单据、事件能触发。任何一环断了弹窗都不会出现。参数说明命名空间MyDev.K3.SCM.Stock.Business.PlugIn是工程名建议按公司前缀 模块 业务组织后面插件多了不至于乱类名ReceiptEdit对应具体单据一张单据一个插件类别把多张单据的逻辑塞一个类里。步骤上新建工程 → 添加Kingdee.BOS和Kingdee.BOS.Core引用 → 新建类继承AbstractBillPlugIn→ 重载BarItemClick→ 编译到 Bin → 打开 IDE 设计器在单据属性窗口编辑采购收料单-_Bill的插件列表 → 注册插件 → 浏览选择编译好的组件 → 勾选确定 → 保存单据 → 运行测试。注意注册插件界面里可能已有其他插件它们在运行时会动态加载删除可能导致业务数据错误。只增不删除非你确认那个插件没被业务引用。4. 动态表单的 View 与 Model事件挂在哪一层决定成败4.1 两个接口、两条数据线资料把动态表单拆成外观和逻辑两块外观由视图View管理逻辑由模型Model管理具体数据存在元数据里布局元数据LayoutInfo记录外观业务元数据BusinessInfo记录逻辑这两个类分别由 View 和 Model 持有。对应两个插件接口IDynamicFormViewPlugIn接收 View 层事件比如菜单点击、按钮点击、F7 选择返回、页签切换、分录行单击双击。IDynamicFormModelPlugIn接收 Model 层事件比如值更新前、数据包创建、分录行新增、字段值改变。这个划分是整份资料最该先吃透的部分。为什么因为我重载了方法却不触发的根因九成是挂错层。举个典型想控制字段显隐属于外观用 View 的AfterBindData配合StyleManager.SetVisible想改数据值属于逻辑用 Model 的SetValue或BeforeUpdateValue。挂错层的代码能编译、能注册就是不干活。View 接口常用事件里BarItemClick管主菜单、EntryBarItemClick管分录菜单、ToolBarItemClick管工具栏、AfterF7Select管基础资料选完返回、BeforeClosed管关页面前收尾。Model 接口里AfterCreateNewData管数据包建好之后、AfterCreateNewEntryRow管分录行新增之后、BeforeUpdateValue管值改之前拦截、DataChanged管值改之后响应。4.2 加载链路七个钩子的执行顺序资料给出了一条明确的执行顺序这条链在排查为什么我的初始化没生效时是唯一的依据OnInitialize 页面初始化 CreateNewData 动态表单数据包创建仅新增触发 AfterCreateNewEntryRow 创建分录行后每次新增分录都触发 AfterCreateNewData 动态表单数据包创建后 BeforeBindData 绑定数据前 AfterBindData 绑定数据及控件状态加载与刷新都会调用 BeforeClosed 页面关闭前按这条链读代码很多疑问自然解开。CreateNewData只在新增时触发、打开已有单据不触发所以往里写加载历史数据的逻辑一定失败AfterCreateNewData设置的值不会进状态管理器是随整个数据包一起发到客户端的所以它适合做缺省值不适合做用户能撤销的修改AfterBindData在加载和刷新时都调用因此不能在里面做只该执行一次的重活否则每次刷新都跑一遍。AfterCreateModelData更特殊只在新增表单模型后触发且它的操作不引起Model.DataChanged适合展示性的初始化。4.3 三个可直接抄的实战片段第一个是新增时按当前组织取邮件缺省值属于典型的数据包补值public override void AfterCreateNewData(EventArgs e) { base.AfterCreateNewData(e); // 按当前组织过滤邮件缺省设置 OQLFilter ofilter new OQLFilter(); ofilter.Add(new OQLFilterHeadEntityItem { FilterString string.Format( FORGID {0} , this.Model.Context.CurrentOrganizationInfo.ID) }); DynamicObject[] obj BusinessDataServiceHelper.Load( this.View.Context, BAS_MAILDEFAULTSET, null, ofilter); if (obj ! null obj.Count() 0) { DynamicObject defaultSet obj[0]; // 把查到的服务端配置写进当前数据包的对应字段 this.View.Model.SetValue(FMessageType, defaultSet[FMessageType]); this.View.Model.SetValue(FServer, defaultSet[FOutgoingMailServer]); this.View.Model.SetValue(FSMTPPort, defaultSet[FSMTPPort]); } }逻辑说明OQLFilter构造查询条件BusinessDataServiceHelper.Load按表单标识BAS_MAILDEFAULTSET取数据Model.SetValue回填字段。参数上FORGID用的是当前组织 ID多组织场景下这个条件必须带否则会取到别的组织的配置这是很隐蔽的业务错误。第二个是分录自动编号展示新增行后按上一行推算的思路public override void AfterCreateNewEntryRow(CreateNewEntryEventArgs e) { base.AfterCreateNewEntryRow(e); if (e.Entity.Key.Equals(FSubEntity)) { IEnumerableDynamicObject subEntryDataCol this.Model.GetEntityDataObject(e.Entity); // 只在追加行时处理插入行不赋值 if (e.Row subEntryDataCol.Count() - 1) { int maxOperNumber subEntryDataCol .Select(w w.GetDynamicObjectItemValueint(FOperNumber)).Max(); // 向上取整到 10 的倍数封顶 9999 int newOperNumber (int)Math.Ceiling(((decimal)maxOperNumber 1) / 10) * 10; this.Model.SetValue(FOperNumber, newOperNumber 9999 ? 9999 : newOperNumber, e.Row); } } }逻辑说明e.Row Count() - 1是判断是否追加在末尾插入到中间行时不赋值避免打乱已有编号。GetDynamicObjectItemValueint取已有最大值Math.Ceiling做向上取整。参数上10 和 9999 是业务规则里的步长与上限实际项目要按自己的工序编号规则改e.Entity.Key要换成你单据体真实的标识写死字符串不优雅但直观工程上建议抽成常量类。第三个是数据包手动组装用于展示操作结果这类界面只展示、数据自己造的场景public override void CreateNewData(BizDataEventArgs e) { // 自行创建本界面需要的数据对象 e.BizDataObject new DynamicObject( this.View.OpenParameter.FormMetaData.BusinessInfo.GetDynamicObjectType()); BusinessInfo info this.View.OpenParameter.FormMetaData.BusinessInfo; Entity resultEntity info.GetEntity(FEntity); DynamicObjectCollection resultEntityData (DynamicObjectCollection)resultEntity.DynamicProperty.GetValue(e.BizDataObject); int row 0; foreach (OperateResult rowResult in _results) { DynamicObject rowData new DynamicObject(resultEntity.DynamicObjectType); // 逐字段赋值注意状态字段用 1/0 表示成功失败 info.GetField(FSeq).DynamicProperty.SetValue(rowData, row 1); info.GetField(FName).DynamicProperty.SetValue(rowData, rowResult.Name); info.GetField(FStatus).DynamicProperty.SetValue(rowData, rowResult.SuccessStatus ? 1 : 0); info.GetField(FMessage).DynamicProperty.SetValue(rowData, rowResult.Message); info.GetField(FType).DynamicProperty.SetValue(rowData, ((int)rowResult.MessageType).ToString()); resultEntityData.Add(rowData); row; } }逻辑说明先按元数据取到DynamicObjectType建空对象再取单据体实体和字段逐行Add到集合。参数上要注意FStatus用字符串 1/0、FType由枚举转 int 再转字符串这类元数据字段类型和代码类型不一致的坑在 BOS 里非常多写之前先在 IDE 里确认字段类型。_results是插件类里的私有集合由前面的操作过程填充。4.4 视图层菜单、字段显隐与标题外观类需求都落在 View 上。资料给的批量修改界面例子很有代表性OnInitialize里按列表的 formid 加载元数据遍历字段筛出允许批量修改且编辑态可见的塞进下拉列表。public override void OnInitialize(InitializeEventArgs e) { // 按父表单 formid 取元数据 metadata (FormMetadata)ServiceHelper.MetaDataServiceHelper.Load( this.View.Context, this.View.ParentFormView.BillBusinessInfo.GetForm().Id); // 设置标题子表单标题 父表单标题 string strTitle string.Format({1}-[{0}], metadata.GetLayoutInfo().GetFormAppearance().Caption, this.View.LayoutInfo.GetFormAppearance().Caption); LocaleValue formTitle new LocaleValue(); formTitle.Add(new KeyValuePairint, string(this.Context.UserLocale.LCID, strTitle)); this.View.SetFormTitle(formTitle); ListEnumItem list new ListEnumItem(); foreach (Field field in metadata.BusinessInfo.GetFieldList()) { // 字段未开启批量修改控制则跳过 if ((field.FunControl Field.FUNCONTROL_BULK_EDIT) ! Field.FUNCONTROL_BULK_EDIT) continue; // 编辑态不可见的字段不参与批量修改 Appearance app metadata.GetLayoutInfo().GetAppearance(field.Key); if (app ! null (app.Visible Appearance.VIS_EDIT) ! Appearance.VIS_EDIT) continue; EnumItem item new EnumItem(); item.Value field.Key; item.Caption field.Name; list.Add(item); } list list.OrderBy(p p.Caption[this.View.Context.UserLocale.LCID]).ToList(); if (list.Count ! 0) selectedFielKey list.FirstOrDefault().Value; this.View.GetControlComboFieldEditor(FCombo).SetComboItems(list); }逻辑说明两个过滤条件缺一不可——FUNCONTROL_BULK_EDIT位运算判断字段是否支持批量修改VIS_EDIT判断编辑态可见性漏掉后者会把界面上根本看不到的字段塞进下拉用户选了也没反应。标题用LocaleValue而非 string是为多语言留口子。参数上this.View.Context.UserLocale.LCID是当前语言区域 ID排序时按该语言的 Caption 排避免中文环境按拼音乱序。AfterBindData里则是控件状态控制public override void AfterBindData(EventArgs e) { base.AfterBindData(e); // 隐藏主菜单项、启用分录菜单项 this.View.GetMainBarItem(tbNew).Visible false; this.View.GetBarItem(Fentity, tbAdd).Enabled true; // 按类型控制页签可见性 if (_modelTypeId ElementType.ELEMENTTYPE_BASE.ToString()) { this.View.StyleManager.SetVisible(FTab_Field, null, false); } else if (_modelTypeId ElementType.ELEMENTTYPE_BILL.ToString()) { // 单据无单据类型字段时屏蔽字段参数页签 if (this._metaData.GetLayoutInfo().GetFieldAppearances() .Any(f f is BillTypeFieldAppearance)) { /* ... */ } } }逻辑说明GetMainBarItem取主菜单、GetBarItem取单据体菜单两者 API 不同别混用StyleManager.SetVisible是外观控制的推荐入口比直接改控件属性更稳。参数上SetVisible第二个参数传 null 表示不针对具体分录行。这一段代码资料里是截断的实际写的时候后半段要补全 if 分支内的逻辑。5. 避坑与排查插件不生效的五种典型现场5.1 编译成功但运行时完全不触发现象IDE 里注册了插件单据也保存了点菜单没反应断点也进不去。原因通常是程序集没落到K3Cloud\K3CloudServer\Bin\下或注册时浏览的是旧版本文件运行时加载的是另一份。排查顺序先去 Bin 目录确认文件修改时间是不是刚编译的再进 IDE 插件列表看引用的路径最后回收应用程序池重启站点动态加载的程序集在进程里是缓存的不重启可能一直用旧版。这条我踩过不止一次后来固定成改代码 → 编译 → 确认时间戳 → 回收池 → 测试四步。5.2 方法重载了但永远进不去现象代码里明明重载了AfterBindData加日志也没输出。原因多半是继承的基类不对或者事件挂在了错误的层。比如把本该在 View 层的BarItemClick写进了只实现 Model 接口的插件或者单据插件继承了AbstractDynamicFormPlugIn而不是AbstractBillPlugIn。解决办法对照资料里的继承体系表先确认表单类型单据/基础资料/列表/动态表单再选基类确认事件属于 View 接口还是 Model 接口选错接口时事件签名能对上但不会派发。5.3 新增时能赋值打开已有单据就没了现象AfterCreateNewData里写的缺省值新增单据时正常打开历史单据时字段是空的。原因就是执行链路CreateNewData/AfterCreateNewData只在新增时触发打开已有单据走的是绑定数据流程。解决办法缺省值逻辑留在AfterCreateNewData需要对历史数据也做处理的挪到AfterBindData里按需判断但要注意它在每次刷新时都会调用必须加条件避免重复执行导致数据被反复覆盖。5.4 值改完又被撤销回去现象插件里SetValue设了值界面显示一瞬间后回到原值。原因是赋值时机不对。资料明确写了AfterCreateNewData设置的值不进状态管理器是随数据包一起下发的而BeforeBindData里对数据的修改也不会被状态管理器记录。如果你在错误时机改了本该由状态管理器管理的值界面重绘时就会以服务端数据为准覆盖。解决办法需要参与状态管理的值改动用Model.SetValue在绑定完成后的合适事件里做纯展示性、下发给客户端就固定的值才放在数据包创建阶段。5.5 数据库装完才发现字符集不对现象单据保存中文乱码或者排序结果诡异。原因是安装时没选排序规则和字符集。资料写明 SQL Server 排序规则必须是Chinese_PRC_CI_ASOracle 字符集AL32UTF8、国家字符集AL16UTF16。这类问题属于后悔药都买不到的类型——库装好、数据进去之后再改代价极大。解决办法只有一个装库前把这两项写进检查清单逐项核对后再执行安装脚本。多人协作的团队更要把这条写进环境交接文档别指望下一个人自觉。6. 把调试手段固定下来抓包、断点与一份自查清单插件写到能跑之后真正拉开效率差距的是排查能力。这份资料里提到一个容易被忽略的工具HttpWatch Pro 6.0。它的用途是观察客户端与服务端之间的数据往返AfterCreateNewData里设置的值随数据包一起发到客户端这句话不看报文是没法验证的——你在代码里设了值界面上没显示到底是没设进去还是设进去了但客户端没渲染抓一次请求就能分清。我的习惯是三个手段配合用第一Visual Studio 附加到 K/3 Cloud 的 Web 进程打远程断点注意进程回收后断点会失效要重新附加第二关键节点写日志尤其是OnInitialize、AfterCreateNewData、AfterBindData三个位置各打一条带时间的记录从日志顺序就能验证加载链路是否符合预期第三抓包看数据包里字段到底有没有值。三者组合基本能把不生效类问题定位到具体环节。下面这份自查清单是我按资料内容整理后实际在用的新环境接入或者插件异常时逐项过一遍比漫无目的翻代码快得多检查项合格标准常见不合格表现编译输出路径指向K3Cloud\K3CloudServer\Bin\编到默认 bin\Debug忘了拷贝程序集引用Kingdee.BOS、Kingdee.BOS.Core版本与服务器一致引用了别的环境拷来的 dll基类选择与表单类型匹配单据插件继承动态表单基类事件层级View 事件进 View 插件Model 事件进 Model 插件显隐控制写进 Model 事件菜单 Key与 IDE 配置完全一致含大小写拼错或用了显示名称管理中心地址Common.config中指向公共环境 IP:端口用动态 IP过几天失效数据库初始化排序规则/字符集符合要求装完才发现乱码插件注册只增不删保留既有插件误删导致业务数据异常最后说一个进阶技巧把重复的取元数据 → 判断字段控制位 → 组装下拉/设置显隐抽成公共方法或基类。资料里批量修改那段OnInitialize字段过滤逻辑FUNCONTROL_BULK_EDIT位运算、VIS_EDIT可见性判断在别的界面同样要用每个界面抄一遍迟早抄错。我一般会建一个项目内的插件基类把这类通用筛选、标题设置、日志埋点收进去各单据插件只写差异部分。代价是基类一旦改错影响面很大所以基类里只放稳定逻辑不确定的宁可留在子类里重复几行。从那以后我每次接一个新的 K/3 Cloud 二开项目都强制先走一遍这份自查清单环境对齐、Bin 路径、基类与事件层级、管理中心地址四项过了再动代码比事后翻日志省太多时间。希望帮到你。本文还有配套的精品资源点击获取