☰
K/3 Cloud BOS插件开发基础:从HelloWorld到动态表单事件链路
2026/10/29 6:50:18 网站建设 项目流程

简介:这是一份面向K/3 Cloud BOS插件开发初学者的基础文档,围绕BOSIDE、套打、万能报表等BOS产品应用场景,帮助读者理解如何用插件完成企业个性化与非标准化业务定制。资源包内共1个docx文件,约1.9MB,内容集中,便于按章节查阅;目前已有216人学习。文档从开发环境配置讲起,说明Visual Studio、Silverlight、SQL Server/Oracle、IIS等软硬件要求,继而梳理BOS架构模型与动态表单、服务、校验规则、表单构建四类插件,并给出定义插件类、重载BarItemClick、编译、在IDE注册及运行测试的入门流程,同时涉及公共/个人环境搭建、数据库字符集与排序规则等注意事项,适合初学者建立插件开发整体框架并动手实践。

1. 从一个没反应的 HelloWorld 按钮说起:这份文档到底解决什么问题

标准单据上加一个按钮,点开弹一句提示——在 K/3 Cloud BOS 上第一次做这件事,十个新人里有八个会卡在同一个地方:类写了、编译过了、插件也注册了,鼠标点下去就是没动静。问题几乎从不出在 C# 语法上,而是出在平台侧的三件事:插件编译到哪个目录、注册绑到哪张单据、事件在什么时机被调用。这份《初学者-k3cloud插件开发基础》就是围绕这三件事展开的一份动手笔记,讲 BOS 产品的插件分类、动态表单的 View/Model 分层、以及从 OnInitialize 到 AfterBindData 这条加载链路上每个钩子能改什么。它不教语法,适合已经能写类、能读懂继承关系,但要在一个成熟 ERP 平台上做定制扩展的开发者,尤其是刚接手 BOS 产品应用汇总里那些二次开发任务、需要在标准单据上做加法而不能改标准代码的同学。

2. 环境与工程落地:从 Visual Studio 工程到 Bin 目录的一次闭环

BOS 插件开发是 Windows 平台上的活,开发工具是 Visual Studio,客户端跑的是带 Silverlight 插件的浏览器。这一套技术栈年龄不小,意味着版本和路径不能随意发挥——社区版 VS 2022、Chrome 浏览器、随手新建的类库工程,任何一项没对上,后面调试的时候就会以「插件注册成功但运行无反应」的形式还给你。这一章先把配置基准、公共环境与个人环境的差别、以及一个 Hello World 插件的完整七步走完。

2.1 配置基准与版本红线

团队服务器的配置要求按角色分开算,数据库机器吃内存,客户端机器吃的是浏览器内存。原始文档给的建议值整理如下:

角色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 以上

软件版本这边横向排开是这么几层:

组件版本要求操作系统
数据库服务器SQL Server 2008R2 标准版/企业版以上;Oracle 11g 以上Windows Server 2008 以上;Oracle 另支持 Linux x86-64、AIX
Web 服务器IIS 6.0 以上,Microsoft .NET Framework 4.0Windows Server 2008 以上
客户端IE 8.0 / 9.0(32 位 x86、64 位 x64)+ Silverlight 5;或 Firefox 12 及以上(32 位 Windows)+ Silverlight 5—

开发机上需要装的工具一共四项:

工具作用
Visual Studio 2012插件工程 IDE
IE 插件 Silverlight 5客户端运行时,不装则界面直接白屏
SQL Server 2008R2 或 Oracle 11G R2数据库,个人环境可与产品同机
HttpWatch Pro 6.0抓客户端请求,排查「点了没反应」这种事
插件编译输出路径K3Cloud\K3CloudServer\Bin\

两个数据库侧的硬性约束必须提前记下:用 SQL Server 时排序规则必须是Chinese_PRC_CI_AS;用 Oracle 时数据库字符集必须是AL32UTF8,国家字符集必须是AL16UTF16。这两项都是建库时一次性写进库里的属性,事后想改基本等于重建实例加迁移数据——真正意义上的血泪经验,不要等到导入业务数据之后才发现中文排序不对。

2.2 公共环境与个人环境差在哪:Common.config 里那一行地址

环境分两层。公共环境是整个团队共享的底座:一台数据库服务器装 SQL Server 2008R2,一台 Web 服务器装 K/3 Cloud 产品并配置为管理中心站点。管理中心是全局的,负责站点、数据中心、许可这些全局信息,所以一个团队只应该有一套。

个人环境则是每个开发自己那份:装 Visual Studio、装 K/3 Cloud 产品,但不配置管理中心。装完之后第一件事是改配置文件,位置在产品安装目录下的K3Cloud\K3CloudServer\App_Data\Common.config,把managementSiteUrl指向公共环境里那台管理中心的地址:

<!-- K3Cloud\K3CloudServer\App_Data\Common.config --> <!-- 指向团队公共环境的管理中心,个人环境不再自建 --> <add key="managementSiteUrl" value="http://192.168.73.40:8000/" />

这里的192.168.73.40:8000是文档中给出的示例地址,实际要替换成自己团队管理中心的内网 IP 和端口。改完这一行,个人环境的 Web 站点在启动时会去连公共管理中心拉配置和数据中心列表,本地开发机就不需要再维护一份独立的管理库了,多人协作时也不会出现「你本地注册的插件别人看不到」这种问题。

改配置这个动作本身只有一行,但它决定了后面所有调试行为是否成立。常见做法是:个人环境的站点部署好之后,先用浏览器打开一次,确认能正常登录进数据中心列表,再回头动代码。如果这一步就打不开,那配置地址、端口、防火墙三件事按顺序排一遍,别急着怀疑插件。

2.3 七步走通一个 Hello World 插件

流程本身不长,但每一步都有对应的产物,缺一个就断链。原始文档给的步骤是:定义插件类、分析业务定义重载方法、引用相关组件、重载方法编码、设置编译路径并编译、在 IDE 设计器里配置插件、运行测试。

第一步先建工程,命名建议照着业务模块来,比如MyDev.K3.SCM.Stock.Business.PlugIn,前缀是团队标识、中间是产品与模块、后面是业务名加插件类型。工程类型选类库,然后添加两个基础引用:Kingdee.BOS和Kingdee.BOS.Core。

第二步新建类并继承基类。演示用的是采购收料单,类名叫ReceiptEdit,继承Kingdee.BOS.Core.Bill.PlugIn.AbstractBillPlugIn:

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 { // 单据插件统一继承 AbstractBillPlugIn public class ReceiptEdit : AbstractBillPlugIn { /// <summary> /// 主菜单点击事件:菜单 Key 匹配时弹提示 /// </summary> public override void BarItemClick(BarItemClickEventArgs e) { base.BarItemClick(e); // BarItemKey 对应 IDE 中菜单项的标识,不是显示名称 if (e.BarItemKey == "HelloWorld") { this.View.ShowMessage("Hello world!", MessageBoxType.Notice); } } } }

BarItemClickEventArgs里最关键的是e.BarItemKey,它对应单据在 IDE 里那个菜单项的 Key 属性,而不是你在界面上看到的显示文字,写错了就是「代码没问题但点了不弹」。this.View.ShowMessage走的是视图层弹窗,第二个参数MessageBoxType.Notice决定提示框的图标类型,一般提示用 Notice,报错场景换成 Error 更符合用户预期。

第三步是编译配置,这是最容易被跳过、也最容易翻车的一环:工程属性里的输出路径必须设成产品安装目录下的K3Cloud\K3CloudServer\Bin\。产品运行时是从这个目录动态加载插件的,dll 生成在bin\Debug里不会有任何报错,只会表现为插件静默失效。

第四步进入 IDE 设计器注册。找到单据属性窗口,编辑「采购收料单-_Bill」的单据属性,在插件列表里点击注册插件,浏览选中刚编译出来的 dll,勾选插件类,确定后保存单据。

注意:插件列表里通常已经注册了其他插件,这些插件在运行时会被动态加载,删除它们可能导致业务数据错误。注册插件时只做加法,不要做减法。

第五步运行测试。打开采购收料单,点那个新加的 HelloWorld 菜单,看到提示框就算这条链路打通了。打不通的时候按「dll 在不在 Bin 目录 → dll 时间戳是不是刚编译的 → 注册的类全名对不对 → BarItemKey 对不对」这个顺序排查,比盯着代码看快得多。

3. 架构模型拆解:View 管外观、Model 管逻辑,四类插件各管一段

BOS 平台的扩展思路不是改标准代码,而是把业务抽象成动态领域模型,模型对外暴露标准接口和标准服务,个性化需求通过插件挂到这些接口上。理解了这套分层,才知道需求来了该往哪个钩子上挂。

3.1 动态表单模型:外观归 View,逻辑归 Model

动态表单模型由两部分组成:表单外观和表单业务逻辑。外观管的是界面控件张什么样、摆在哪个位置,由视图(View)控制;业务逻辑管的是服务、校验器、操作、业务规则,由模型(Model)控制。这两部分都在 IDE 里配置,配置结果保存在动态表单模型的元数据里——具体是LayoutInfo记录外观数据、BusinessInfo记录业务逻辑数据,前者由 View 持有,后者由 Model 持有。

客户端发起一次表单访问时,链路是这样的:客户端发 HTTP 请求,服务端由控制器服务接收并转送到动态表单模型控制器,控制器再去访问动态表单视图;视图加载外观模型,同时从动态表单模型里取数据模型。这条链路上有两组接口:IDynamicFormView和IDynamicFormViewService,IDynamicFormModel和IDynamicFormModelService。带 Service 后缀的那两个是平台内部做消息路由用的,插件开发基本不用碰;不带后缀的两个才是插件直接访问的入口。

所以写插件时你手里其实就两把钥匙:this.View负责界面,this.Model负责数据。改标题、改控件可见性、隐藏菜单,走 View;读写字段值、加分录行、改数据包,走 Model。分清楚这个,后面看几十个事件的时候就不会乱。

3.2 四类插件:写业务最多的是第一种

BOS 平台按用途划分了四类插件,选型时先对号入座:

  1. 动态表单插件。把业务逻辑封装成插件,用于在单据的各类事件、操作和服务上介入,平台提供了一系列事件、操作和服务接口。这是最常用的一类,绝大多数「点按钮弹窗」「字段联动」都属于它。
  2. 服务插件。基于动态表单服务端的插件,提供服务编程接口,供外部服务直接调用。
  3. 校验规则插件。专门针对业务规则做校验的插件。
  4. 表单构建插件。对界面加载的控件和元数据进行调整,实现特殊界面需求。

动态表单插件内部还可以再往下分五类:单据插件、列表插件、过滤条件插件、账表插件、动态表单插件。其中单据插件对应单据类界面,列表插件对应列表界面,账表插件对应报表界面——根据你要挂的界面类型选基类,不要一套基类打天下。

3.3 继承体系与命名空间对照

这层对应关系建议直接记住,建类时闭着眼也不会选错:

插件类型基类命名空间
单据插件AbstractBillPlugInKingdee.BOS.Core.Bill.PlugIn
基础资料插件AbstractBasePlugInKingdee.BOS.Core.Base.PlugIn
动态表单插件AbstractDynamicFormPlugInKingdee.BOS.Core.DynamicForm.PlugIn
列表插件AbstractListPlugInKingdee.BOS.Core.List.PlugIn

继承链条从上往下是:System.Object→AbstractDynamicFormPlugIn→AbstractBillPlugIn→AbstractBasePlugIn。也就是说四个基类并不是平级的兄弟,底层能力都在AbstractDynamicFormPlugIn里,越往后越具体。

Kingdee.BOS.Core.DynamicForm.PlugIn命名空间下还有几个类值得先记名字:AbstractDynamicFormDataBinder(动态表单数据绑定器抽象类)、AbstractDynamicWebFormBuilderPlugIn(动态表单页面元数据构建插件)、AbstractOperationServicePlugIn(操作服务插件)。接口方面只有两个:IDynamicFormViewPlugIn接收 View 层事件,IDynamicFormModelPlugIn接收 Model 层事件。实际开发中很少直接实现接口,都是从抽象基类继承、按需重写方法,因为基类已经帮你把不需要关心的事件做了空实现。

4. 加载链路拆解:从 OnInitialize 到 AfterBindData 的七个钩子

动态表单第一次被访问时,平台会先加载元数据、初始化视图和模型对象、初始化页面,然后创建数据包并把数据绑定上去。这条流水线上留了一串扩展点,插件的价值就是在正确的点上做正确的事。挂错时机不会报错,只会得到「界面显示对了但保存后没落库」这种玄学问题。

4.1 七个事件谁先触发、各自能看到什么

顺序是固定的:

顺序事件触发时机典型用途
1OnInitialize页面初始化初始化控件数据源、设置页面标题
2CreateNewData新增时创建数据包从其他服务取数作为默认数据
3AfterCreateNewEntryRow每新增一条分录行分录行的编号、序号自动计算
4AfterCreateNewData数据包创建完成按参数动态补字段缺省值
5BeforeBindData数据绑定到界面之前按类型增删分录行
6AfterBindData数据绑定完成,界面已就绪控制可见性、菜单状态、样式
7BeforeClosed页面关闭前关闭前的收尾校验

两个容易混的点:CreateNewData和AfterCreateNewData都只在新增时触发,打开已有单据不走这两个;AfterBindData则在加载和界面刷新时都会调用,所以可见性这类逻辑放这里最稳。

View 层的事件清单更长,常用的包括BarItemClick(主菜单单击)、EntryBarItemClick(分录菜单单击)、ToolBarItemClick(工具栏单击)、AfterF7Select(基础资料选择返回)、EntityRowClick/EntityRowDoubleClick(分录行单击/双击)、FieldLabelClick(字段标题单击)、TabItemSelectedChange(页签切换)、DataChanged(字段值改变后)。Model 层则集中在BeforeUpdateValue(值更新前)、DataChanged、CreateNewData、AfterCreateNewData、AfterCreateNewEntryRow这几个。

4.2 OnInitialize:初始化控件数据源和页面标题

这个事件负责动态表单实例的初始化,包括单据的全局参数、动态初始化控件数据源。典型场景是批量修改界面:要把所有允许批量修改的字段塞进一个下拉列表。

/// <summary> /// 界面初始化:把允许批量修改的字段装入下拉列表 /// </summary> 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 KeyValuePair<int, string>(this.Context.UserLocale.LCID, strTitle)); this.View.SetFormTitle(formTitle); List<EnumItem> list = new List<EnumItem>(); 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) { if ((app.Visible & Appearance.VIS_EDIT) != Appearance.VIS_EDIT) continue; } _lstFields.Add(field); 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.GetControl<ComboFieldEditor>("FCombo").SetComboItems(list); }

几个参数的含义值得单独说。metadata是FormMetadata类型,通过ServiceHelper.MetaDataServiceHelper.Load按 FormId 反查得到,它同时持有BusinessInfo(字段定义)和LayoutInfo(界面布局)。field.FunControl是个位标志,用按位与判断有没有FUNCONTROL_BULK_EDIT这一位,这种判断方式在 BOS 里到处都有,Appearance.Visible判断VIS_EDIT也是同一个套路。EnumItem的Value存字段 Key、Caption存显示名,下拉框提交时传回的是 Value。最后一句SetComboItems直接替换整个下拉数据源,比逐条 Add 干净。

LocaleValue那段是处理多语言的常规写法,LCID取当前用户语言,把标题按语言维度存进去,多语言环境下同一个界面在不同语言账号下显示不同标题。

4.3 CreateNewData:数据包从哪来

新增单据时,平台会调用这个方法创建数据对象,你什么都不写它也会给你一张空白单据——因为默认会创建空数据。需要自定义来源时,直接替换e.BizDataObject:

/// <summary> /// 新增时用单据类型参数作为界面数据源 /// </summary> public override void CreateNewData(BizDataEventArgs e) { if (!billFormId.IsNullOrEmptyOrWhiteSpace()) { // 整包取出单据类型参数,直接作为界面数据源 DynamicObject obj = BusinessDataServiceHelper.LoadBillTypePara( context, businessInfo, formId, false); e.BizDataObject = obj; } base.CreateNewData(e); }

e.BizDataObject是交给界面的那个DynamicObject,谁赋值谁的算。LoadBillTypePara的最后一个是布尔参数,控制是否连带加载关联数据,演示场景传false就够。

另一个更常见的用法是「操作执行完之后,把结果回显到一张结果表单上」。这时候数据包是空的,需要自己 new 出来,再按单据体结构逐行填:

/// <summary> /// 操作结果回显:手工构造结果表格的数据包 /// </summary> 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"); Field seqField = info.GetField("FSeq"); Field nameField = info.GetField("FName"); Field statusField = info.GetField("FStatus"); Field messageField = info.GetField("FMessage"); Field typeField = info.GetField("FType"); DynamicObjectCollection resultEntityData = (DynamicObjectCollection)resultEntity.DynamicProperty.GetValue(e.BizDataObject); int row = 0; foreach (OperateResult rowResult in _results) { // 每一条结果就是单据体的一行 DynamicObject rowData = new DynamicObject(resultEntity.DynamicObjectType); seqField.DynamicProperty.SetValue(rowData, row + 1); nameField.DynamicProperty.SetValue(rowData, rowResult.Name); statusField.DynamicProperty.SetValue(rowData, rowResult.SuccessStatus ? "1" : "0"); messageField.DynamicProperty.SetValue(rowData, rowResult.Message); typeField.DynamicProperty.SetValue(rowData, ((int)rowResult.MessageType).ToString()); resultEntityData.Add(rowData); row++; } }

赋值这里有个细节:字段值不是通过rowData["FSeq"] = ...这种索引器写的,而是先取出Field,再调field.DynamicProperty.SetValue(rowData, value)。这是 BOS 动态实体体系的约定——字段的元数据和取值逻辑都挂在Field上,绕过它赋值在某些场景下不会进状态管理器。单据体是DynamicObjectCollection,Add一行就是新增一行,可以有多条单据体,每个都从BusinessInfo.GetEntity取。

提示:动态实体默认用DynamicObject作数据承载类,也可以通过DynamicObjectType.ClrType指定自定义类,但指定的类型必须派生自DynamicObject,否则运行时会报类型不匹配。

4.4 AfterCreateNewData 和 AfterCreateModelData:缺省值与二次加工

AfterCreateNewData在数据包创建完成之后触发,只在新增时走一次。字段缺省值优先在 IDE 里配实体服务规则,但值依赖运行期参数(比如当前组织)的时候就得上插件:新增单据时按当前组织去取邮件默认配置,回填到当前数据包。

/// <summary> /// 新增后按当前组织回填邮件缺省配置 /// </summary> 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是这个平台上的查询过滤对象,FilterString里写的是一段类 SQL 的过滤表达式,FORGID对应组织字段。这里取的是第一条匹配记录,再逐个字段SetValue回当前模型。

有一点必须清楚:这个方法里SetValue的值不会进入状态管理器,它是跟着整个数据包一起发到客户端的,界面上看得到,但客户端提交时未必会被当作「用户改动」处理。所以要参与保存落库的字段,放在AfterBindData阶段用Model.SetValue再走一遍更稳。原始文档里也提到,这个阶段设的值可以通过客户端的 HTTP 数据监控查看到,调试的时候从那里核对着实方便。

AfterCreateModelData则更靠后一层,是模型层数据包创建完毕,同样只在新增表单后触发。它的特点是这个阶段的改动不会引起Model.DataChanged,适合做「静默」的数据加工。文档里的例子是订单变更查询:打开查询界面时,默认展示第一行基准版本的订单内容。

/// <summary> /// 模型数据包创建完毕,默认展示第一行版本对应的订单 /// </summary> public override void AfterCreateModelData(EventArgs e) { if (listVersions != null && listVersions.Count() > 0) { baseOrderData = SCMCommon.DeserializeJsonStringToDynamicObject( orderBusinessInfo, listVersions[0].JsonData); ShowOrderBillVersion(); } }

4.5 AfterCreateNewEntryRow 和 BeforeBindData:分录行级别的处理

AfterCreateNewEntryRow每次新增分录都会触发,适合做行级编号。文档里工序号的例子很典型:新增分录时自动算出下一个工序号,规则是「对(当前最大值 + 1)除以 10 向上取整后再乘 10」,上限 9999。

/// <summary> /// 新增分录行:仅在末尾追加时自动生成工序号 /// </summary> public override void AfterCreateNewEntryRow(CreateNewEntryEventArgs e) { base.AfterCreateNewEntryRow(e); if (e.Entity.Key.Equals(CONST_ENG_Route.CONST_FSubEntity.ENTITY_FSubEntity)) { IEnumerable<DynamicObject> subEntryDataCol = this.Model.GetEntityDataObject(e.Entity); // 只有末尾追加才编号,插入行不参与 if (e.Row == subEntryDataCol.Count() - 1) { int maxOperNumber = subEntryDataCol .Select(w => w.GetDynamicObjectItemValue<int>( CONST_ENG_Route.CONST_FSubEntity.ORM_OperNumber)) .Max(); int newOperNumber = (int)Math.Ceiling(((decimal)maxOperNumber + 1) / 10) * 10; this.Model.SetValue( CONST_ENG_Route.CONST_FSubEntity.KEY_FOperNumber, newOperNumber > 9999 ? 9999 : newOperNumber, e.Row); } } }

e.Entity是当前操作的单据体,e.Row是行号。this.Model.GetEntityDataObject(e.Entity)拿到整个单据体的数据集合,GetDynamicObjectItemValue<int>按字段取值并转成指定类型。末尾那句if (e.Row == ...Count() - 1)是关键:插入行也会触发这个事件,不加判断就会给插入的行套上末尾编号,导致工序号重复。

BeforeBindData在数据绑定到界面之前触发,这个阶段对数据的改动同样不会被状态管理器记录,适合按类型决定界面上要出现哪些分录。

/// <summary> /// 绑定数据前:按模型类型决定出现哪些分录 /// </summary> public override void BeforeBindData(EventArgs e) { base.BeforeBindData(e); if (_modelTypeId == ElementType.ELEMENTTYPE_BASE.ToString()) { // 基础资料形态,只出搜索条件分录 this.View.Model.CreateNewEntryRow("FSearchControl"); } else if (_modelTypeId == ElementType.ELEMENTTYPE_BILL.ToString()) { // 业务单据形态,出字段参数分录 this.View.Model.CreateNewEntryRow("FFieldParamControl"); } this.View.Model.CreateNewEntryRow("FPARAMOPERATION"); }

批量加行有专门的方法this.Model.BatchCreateNewEntryRow(string key, int rowCount),需要一次铺多行时用它比循环CreateNewEntryRow高效。

4.6 AfterBindData:可见性、菜单、样式的主战场

这是最常用的一个事件,加载和界面刷新都会调用它,所以任何「界面状态应该长什么样」的逻辑都可以放这里,不怕漏刷。文档里的例子是按模型类型切换界面形态:

/// <summary> /// 数据绑定完成后:按形态控制菜单和页签可见性 /// </summary> 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()) { // 基础资料 <p> <a href="https://download.csdn.net/download/visoni/12619214" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询