开发工具
Visual studio 2012
IE插件Silverlight5
SQLServer 2008R2 或 Oracle 11G R2
跟踪工具(HttpWatchPro6.0)
插件Building路径(K3Cloud\K3CloudServer\Bin\)
注意事项:
使用SQLServer2008排序规则为Chinese_PRC_CI_AS
使用Oracle时,数据库字符集必须是:AL32UTF8,国家字符集必须是:AL16UTF16
开环境搭建
公共环境:
1. 配置一台数据库服务器,安装SQLServer2008R2;
2. 配置一台web服务器,安装K/3Cloud产品,配置为管理中心站点;
个人环境:
1. 根据环境配置要求,安装visual studio,安装K/3Cloud产品(不需要配置管理中心)。
2. 检查并更改管理中心地址,
打开K/3Cloud产品安装目录K3Cloud\K3CloudServer\App_Data下Common.config文件,查找managementSiteUrl,把地址更改为公共环境下建立的管理中心ip。
<addkey="managementSiteUrl"value="http://192.168.73.40:8000/" />
开发插件的步骤
插件开发的步骤 |
1. 定义插件类
打开Visual studio 2012,新建工程:
MyDev.K3.SCM.Stock.Business.PlugIn;
添加引用组件:
Kingdee.BOS
Kingdee.BOS.Core
新建类:
ReceiptEdit,继承自Kingdee.BOS.Core.Bill.PlugIn.AbstractBillPlugIn
2. 分析业务定义重载方法;
这里,我们先简单实现一个Hello World:
点击菜单HelloWorld,弹出一个Hello World对话框。
点击菜单要重载BarItemClick方法;
3. 引用相关组件(参照组件引用规则);
增加using:
C# |
|
using Kingdee.BOS.Core.Bill.PlugIn; using Kingdee.BOS.Core.DynamicForm; using Kingdee.BOS.Core.DynamicForm.PlugIn.Args; |
4. 重载方法编码;
重载BarItemClick方法,输入以下代码:
C# |
|
public override void BarItemClick(BarItemClickEventArgs e) { base.BarItemClick(e); if (e.BarItemKey == "HelloWorld") { this.View.ShowMessage("Hello world!",MessageBoxType.Notice); } } |
保存;
5. 设置编译路径,编译组件;
编译路径:K3Cloud\K3CloudServer\Bin;
6. 打开IDE设计器,配置插件;
先找到单据属性窗口,编辑“采购收料单-_Bill”单据属性:
在插件列表界面,点击注册插件:
(注意该列表中可能已注册有其他插件,这些插件在运行时会动态加载,删除插件可能会导致业务数据错误)
选择插件界面点击浏览:
选择编译好的组件:
勾选插件,确定返回
确定并保存单据。
7. 运行测试;
2、动态表单插件
2. 动态表单插件 |
动态表单插件提供了丰富的接口,通过这些接口可以在插件中对表单编辑和列表界面样式、操作进行控制,也可以对显示数据进行各种处理。
再来回顾一下动态表单元数据结构和继承关系:
动态表单模型包含表单外观和表单业务逻辑,表单外观管理界面控件外观及样式,在模型中由视图(View)来控制,表单业务逻辑管理包括服务、校验器、操作和业务规则等,由模型(Model)来控制。
动态表单外观和逻辑都是在IDE中设置的,设置的数据保存在动态表单模型元数据中,具体由布局元数据(LayoutInfo)记录表单外观数据,由业务元数据(BusinessInfo)记录表单逻辑数据,这2个类分别由View和Model持有。
(图 10 – 2 动态表单元模型)
为了方便使用和提高开发效率,我们将动态表单模型分解为各种表单领域模型,同时为各种模型提供了相应插件:
(图 10 – 3 领域模型-动态表单模型关系)
动态表单插件分为5大类:
1. 单据插件
2. 列表插件
3. 过滤条件插件
4. 账表插件
5. 动态表单插件
继承关系如下:
(图 10 – 4 插件继承关系)
动态表单视图
动态表单视图 |
前面已经介绍,外观是由视图来管理,我们先看看动态表单视图模型。
根据BOS架构图可以看到,客户端首先向服务发起HTTP请求,服务端由控制器服务接受请求并转送到动态表单模型控制器,再有动态表单控制器访问动态表单视图。动态表单视图加载外观模型,并从动态表单模型获取数据模型。
动态表单视图提供2个视图接口,IDynamicFormView和IDynamicFormViewService。
IDynamicFormView是视图接口,包含领域模型元数据、多视图模型接口、操作转发指令和通用属性方法。该接口可由插件直接访问。
IDynamicFormViewService是动态表单内部使用的接口定义,包含Controller消息路由方法,插件开发不需关注。
IDynamicFromView有2个重要属性,BusinessInfo和LayoutInfo,分别表示业务对象逻辑元数据和布局元数据。包含在IDE中设置的表单的所有信息。在运行时,客户端发出访问表单请求后,首先读取元数据初始化BusinessInfo和LayoutInfo,View和Model根据元数据定义的界面数据和布局信息展示出表单。
IDynamicFromView接口提供了访问BusinessInfo和LayoutInfo的一些方法,供插件调用以实现业务,例如:访问菜单,修改控件样式,设置标题,更新界面等。
IDynamicFromView接口同时提供操作控制和调用Model的方法,如:调用表单服务,执行操作,发送客户端指令,刷新界面,打开表单,动态注册插件等。
本章节通过一些示例做详细介绍。
先看看界面元素的访问。在实际业务中,经常需要对单据扩展,增加功能,那么就需要访问菜单、字段显示隐藏锁定等。
动态表单模型
动态表单模型 |
动态表单模型接口:IDynamicFormModel和IDynamicFormModelService。
设计思想同动态表单视图一样,将逻辑和插件模型分开。
IDynamicFormModel是模型接口,包含领域模型元数据、数据操作方法。该接口可由插件直接访问。
IDynamicFormModelService是动态表单内部使用的接口定义,插件开发不需关注。
IDynamicFormModel也有BusinessInfo,和IDynamicFromView一样,表示业务对象逻辑元数据。这里BusinessInfo的意义是根据元数据定义绑定数据。
另外一个重要属性DataObject是当前表单的数据对象。该数据是个DynamicObject,包含单据头和单据体数据,其中单据体是集合对象DynamicObjectCollection,并且可以有多个.
K/3Cloud BOS动态实体类型,默认使用DynamicObject作为数据承载类,可以通过DynamicObjectType.ClrType属性指定自定义类。但我们要求指定的类型必须派生自DynamicObject。
IDynamicFormModel提供的主要是针对数据进行操作的系列方法,包括:初始化、新增表单数据、复制数据、删除数据、定位当前分录数据行、设置值等方法。
动态表单插件
动态表单模型是通过插件代理实现业务逻辑,对外部的接口主要是插件,这些接口可以提供给二次开发使用。
命名空间
命名空间 |
Kingdee.BOS.Core.DynamicForm.PlugIn
主要类及说明:
Class |
Description |
|
AbstractDynamicFormDataBinder |
动态表单数据绑定器抽象类 |
|
AbstractDynamicFormPlugIn |
动态表单插件抽象基类 |
|
AbstractDynamicWebFormBuilderPlugIn |
动态表单页面元数据构建插件 |
|
AbstractOperationServicePlugIn |
操作服务插件抽象类 |
主要接口:
Interface |
Description |
|
IDynamicFormModelPlugIn |
动态表单Model层插件控制接口;实现本接口的插件,可以接收Model层的事件 |
|
IDynamicFormViewPlugIn |
动态表单View层插件接口;实现本接口的插件可以接收动态表单View层事件 |
继承体系
继承体系 |
动态表单插件分4类,单据、基础资料、动态表单和列表。
业务模型 |
类(插件、服务) |
继承自抽象类 |
表单插件 |
单据插件 |
Kingdee.BOS.Core.Bill.PlugIn.AbstractBillPlugIn |
基础资料插件 |
Kingdee.BOS.Core.Base.PlugIn.AbstractBasePlugIn |
|
动态表单插件 |
Kingdee.BOS.Core.DynamicForm.PlugIn.AbstractDynamicFormPlugIn |
|
列表插件 |
列表插件 |
Kingdee.BOS.Core.List.PlugIn.AbstractListPlugIn |
单据插件
命名空间
Kingdee.BOS.Core.Bill.PlugIn
继承体系
继承体系 |
System.Object
Kingdee.BOS.Core.DynamicForm.PlugIn.AbstractDynamicFormPlugIn
Kingdee.BOS.Core.Bill.PlugIn.AbstractBillPlugIn
Kingdee.BOS.Core.Base.PlugIn.AbstractBasePlugIn
接口
视图访问接口
接口名:IdynamicFormViewPlugIn
动态表单View层插件接口;实现本接口的插件可以接收动态表单View层事件。
Name |
Description |
|
AfterBarItemClick |
菜单单击事件完成后处理扩展接口 |
|
AfterBindData |
绑定数据后事件处理后扩展接口 |
|
AfterButtonClick |
按钮单击之后调用 |
|
AfterCopyRow |
分录行拷贝后调用 |
|
AfterDoOperation |
操作完成后调用 |
|
AfterEntryBarItemClick |
分录菜单单击事件处理扩展接口 |
|
AfterF7Select |
基础资料选择返回后调用 |
|
AfterToolBarItemClick |
工具栏单击事件处理扩展接口 |
|
BarItemClick |
主菜单单击事件处理扩展接口 |
|
BeforeBindData |
绑定数据前事件处理后扩展接口,主要用于加载数据到界面前对控件状态进行设置 |
|
BeforeClosed |
页面准备关闭 |
|
BeforeDoOperation |
操作开始前调用 |
|
BeforeF7Select |
基础资料界面调出之前抛出 |
|
ButtonClick |
按钮单击时调用 |
|
EntityRowClick |
分录行单击事件 |
|
EntityRowDoubleClick |
分录行双击事件 |
|
EntryBarItemClick |
分录菜单单击事件处理扩展接口 |
|
EntryButtonCellClick |
表格按钮单击时调用 |
|
FieldLabelClick |
字段标题单击事件 |
|
FireEntryCheck |
单据体列全选事件 |
|
ListViewClick |
列表控件单击事件 |
|
OnInitialize |
页面初始化 |
|
TabItemSelectedChange |
页签控件的页签选中事件 |
|
ToolBarItemClick |
工具栏单击事件处理扩展接口 |
|
TreeDragDrop |
KDTree 拖拽事件 |
|
TreeNodeClick |
TreeView 节点单击之后调用 |
|
TreeNodeDoubleClick |
TreeView 节点双击之后调用 |
模型访问接口
接口名:IdynamicFormModelPlugIn
动态表单Model层插件控制接口;实现本接口的插件,可以接收Model层的事件。主要包括:
Name |
Description |
|
AfterCreateNewData |
业务对象创建后的扩展接口 |
|
AfterCreateNewEntryRow |
新增、插入、多行输入后调用 |
|
BeforeUpdateValue |
值改变更新前的扩展接口 |
|
CreateNewData |
创建新业务对象扩展接口,插件可以更加需要自己创建对象 |
|
DataChanged |
字段值改变后扩展接口 |
加载机制
动态表单元模型包括外观模型和表单逻辑模型,第一次访问时会先加载元数据,初始化视图和模型对象,初始化页面,然后创建数据包并绑定数据。
对于二次开发提供了一系列插件允许二次开发在加载表单时对视图、模型、数据包及界面进行控制,插件在加载过程中的执行顺序如下:
OnInitialize 页面初始化
CreateNewData 动态表单数据包创建
AfterCreateNewEntryRow 创建分录行后
AfterCreateNewData 动态表单数据包创建后
BeforeBindData 绑定数据前事件
AfterBindData 绑定数据及控件状态
BeforeClosed 页面关闭前
初始化方法
OnInitialize
该插件负责动态表单实例初始化,包括单据Global参数(当然有些参数仅仅在使用时候才获取),动态初始化控件数据源等。
比如,批量修改界面初始化时将允许修改的字段加入到下拉列表。
C# |
|
/// ///界面初始化 /// /// public override void OnInitialize(Core.DynamicForm.PlugIn.Args.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 fieldin 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 = 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); } |
创建数据包
创建数据包 |
CreateNewData
动态表单数据包创建,只在新增时触发,打开表单不触发。
我们在IDE里画好单据和基础资料后,不需要编写任何代码,打开界面,可以看到已经创建好一张新的空单据,这是因为新建时候会调用CreateNewRow创建空数据。很多时候,我们需要创建有缺省值或者新增时候从其他服务获取数据显示过来,我们就可以通过该事件来加载数据。
示例:简单的加载动态表单数据。
C# |
|
public override void CreateNewData(BizDataEventArgs e) { if (!billFormId.IsNullOrEmptyOrWhiteSpace()) { DynamicObject obj =BusinessDataServiceHelper.LoadBillTypePara(context,businessInfo, formId, false); e.BizDataObject = obj; } base.CreateNewData(e); } |
示例:操作结束后,在动态表单上显示操作结果。
C# |
|
/// ///创建数据包事件处理;由插件处理数据包的创建过程,界面仅展示 /// /// 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 rowResultin _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++; } } |
AfterCreateNewEntryRow
创建分录行后事件。字段值设置优先考虑使用IDE进行实体服务规则配置。
该事件通常用于新增分录后对数据进行判断处理。需要注意,这个事件是在每次新增分录都会触发,对于不需要在界面上显示的可以在新建分录后(如AfterCreateNewData事件)一次性处理。
C# |
|
/// ///创建新的分录行事件 /// /// public override void AfterCreateNewEntryRow(CreateNewEntryEventArgs e) { base.AfterCreateNewEntryRow(e); if (e.Entity.Key.Equals(CONST_ENG_Route.CONST_FSubEntity.ENTITY_FSubEntity)) { IEnumerable if (e.Row == subEntryDataCol.Count() - 1) // 插入行不赋值 { // 设置工序号=上取整((MAX(工序号)+1)/10)*10,且不大于9999 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); } } } |
AfterCreateNewData
动态表单数据包创建后事件。该方法仅在新增表单后触发。主要用于新建表达根据元数据定义初始化数据包后,根据特殊需求,改变当前数据。
通常我们在IDE里通过配置实体服务规则实现表单字段的缺省值赋值:
但有时需要根据一些参数动态设置值时,就需要用插件实现。下面举一个例子,新增单据时根据当前组织获取邮件的缺省值,赋值到当前数据包。
C# |
|
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"]); } } |
因为该插件属于创建数据包,在该插件里设置的值不会加到状态管理器中,因此该方法设置的值是整个数据包一起发送到客户端的。客户端数据可以通过Http数据监控查询:
AfterCreateModelData
模型层数据包创建完毕。该事件只在新增表单模型后触发,用于对新增后表单模型进行相关操作。此插件的操作不会引起Model.DataChanged值改变。
例:
订单变更查询中,需要在界面上,根据查询列表中的版本显示订单内容,在打开查询时缺省打开第一行基准版本的订单。
插件代码:
C# |
|
/// ///模型数据包创建完毕,显示订单界面 /// /// public override void AfterCreateModelData(EventArgs e) { if (listVersions != null && listVersions.Count() > 0) { baseOrderData = SCMCommon.DeserializeJsonStringToDynamicObject(orderBusinessInfo, listVersions[0].JsonData); ShowOrderBillVersion(); } } |
数据绑定
数据绑定 |
BeforeBindData
绑定数据前事件。该插件可以在数据绑定前对数据进行处理,对数据修改不会被状态管理器记录。
例如:单据插件中根据类型增加分录行。
C# |
|
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(stringkey, int rowCount)方法。
AfterBindData
绑定数据及控件状态,该事件较常用,加载和界面刷新都会调用该插件。通常该事件处理数据可见性样式等。
如:单据插件根据类型设置单据字段可见性。
C# |
|
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 => fis BillTypeFieldAppearance)) { this.View.StyleManager.SetVisible("FTab_Field",null, true); } } } |
设置背景颜色。
C# |
|
public override void AfterBindData(EventArgs e) { //获取单据体表格,参数为单据体Key,示例代码假设为FEntity EntryGrid grid = this.View.GetControl<EntryGrid>("FEntity"); //设置第一行的背景色,参数:颜色,6位16进制符号,每2位代表一种基色;从0开始,行序号 grid.SetRowBackcolor("#FFC080", 0); //设置第二行F1字段的背景色,参数:字段Key;颜色;行序号 grid.SetBackcolor("F1","#FFC080", 1); } |
加载和关闭
加载和关闭 |
OnLoad
页面加载。该事件在BeforeBindData前触发,并且不受StyleManager管理,在此事件设置单据字段的可见性和锁定性无效。
OnLoad时,数据已经获取到,通常我们在此事件处理一些数据设置。
例如:过滤界面插件设置缺省值和页签可见性。
C# |
|
public class SaleCollectFilter : AbstractCommonFilterPlugIn { public override void OnLoad(EventArgs e) { base.OnLoad(e); //设置日期缺省值 this.View.Model.SetValue("FStartDate", dateFrom.ToString("yyyy-MM-dd")); this.View.Model.SetValue("FEndDate", dateTo.ToString("yyyy-MM-dd")); //隐藏过滤界面排序页签 this.View.StyleManager.SetVisible("FTab_P21",null, false); } } |
列表界面隐藏分组滑动控件。
C# |
|
public class SPMPromotionPolicyList : AbstractListPlugIn { public override void OnLoad(EventArgs e) { base.OnLoad(e); // 隐藏分组滑动控件(默认不展开) this.View.GetControl this.View.GetControl("FPanel").SetCustomPropertyValue("BackColor","#FFEEEEEE"); } } |
注:该事件在每次UpdateView()时候都会调用。
BeforeClosed
页面关闭前插件。对于单个表单关闭,该插件基本不需要处理。对于多个表单交互,或者嵌入式表单,通常需要关闭窗体时,返回数据时,通过该插件实现。
如:关闭时刷新父窗体。
C# |
|
public override void BeforeClosed(BeforeClosedEventArgs e) { object isDataChanged = this.View.OpenParameter.GetCustomParameter("Changed"); if (isDataChanged != null && (bool)isDataChanged) { this.View.ParentFormView.Refresh(); this.View.SendDynamicFormAction(this.View.ParentFormView); } base.BeforeClosed(e); } |
关闭时传递数据到父窗体。
C# |
|
public override void BeforeClosed(BeforeClosedEventArgs e) { this.View.ReturnToParentWindow(_data); base.BeforeClosed(e); } |
关闭窗体判断数据是否修改并提示保存。
C# |
|
/// ///界面关闭前事件:判断用户是否修改了数据,提示保存 /// /// public override void BeforeClosed(BeforeClosedEventArgs e) { if (this._dataChanged ==true) // 仅关注模型数据发生了改变的情况 { e.Cancel = true; string msg = "内容已经修改,是否保存?"; this.View.ShowMessage(msg,MessageBoxOptions.YesNoCancel,new Action<MessageBoxResult>((result) => { if (result == MessageBoxResult.Yes) // 保存 { this.View.GetControl("FDesignPanel").InvokeControlMethod("Save"); } else if (result == MessageBoxResult.No)// 不要保存 { this._dataChanged =false; this.View.Close(); } })); } } |
本文档由未注册的 Word-2-CHM软件自动从 Word 文件生成。
单据操作
单据操作 |
BeforeSave
单据保存前插件。单据内置保存操作,自动将修改数据保存到数据库。插件BeforeSave可以在保存前对单据数据进行处理。通常处理有两个:
数据校验;
计算和更新数据;
在BOS平台当客户端发起请求,到web服务器后,领域模型框架调用运行时,加载插件运行。用户执行操作时,运行时调用操作服务进行数据模型的操作。而插件中调用服务也是先向服务框架请求服务。
通常应用都是在业务保存前进行数据校验,校验通过后,调用保存服务保存,在大多数系统中都是这样应用。在BOS平台中,架构设计上支持集成服务,所有操作都是设计有服务接口,二次开发可以很容易将所有操作发布成服务供外部系统调用。这样对外部系统来说,调用服务保存将会很容易。但如何保证数据的正确性?大部分设计是由外部系统保证,但对复杂业务系统来说,外部系统很难保证每个业务数据的正确性,甚至用大量访问系统来获取验证数据。为此,BOS平台在操作上提供了校验服务,这样在系统内部通过插件调用服务前会自动执行校验服务。而外部系统访问的是BOS操作发布的服务本身也带有校验。
因此建议将数据校验按业务逻辑分开成两类,一类是界面输入校验,如字符、数字类型、格式化和表达式校验等,可以在插件保存前进行校验;而数据业务的校验,如库存校验信用检查等,通过校验服务校验。
校验方法如下:
1. 优先通过IDE配置校验数据,如输入格式,最大最小值限定;
2. 操作控制类校验在表单的操作前插件检查;
3. 业务控制类校验在表单校验服务校验。
该事件中可以通过设置参数的Cancel终止保存操作。
下面例子是保存前更新数据(信用评分单据保存设置信用等级标准)。
C# |
|
public override void BeforeSave(BeforeSaveEventArgs e) { DynamicObject doGradeScheme = this.Model.GetValue("FScheme")as DynamicObject; decimal deSumScore =Convert.ToDecimal(this.Model.GetValue("FSumScore")); int iGradeSchemeId =Convert.ToInt32(doGradeScheme["Id"]); // 保存前判断当前信用评分表的综合得分属于哪一个信用等级标准 DynamicObjectCollection docGrades = CreditServiceHelper.GetCreditGrades(this.Context, 0, iGradeSchemeId); for (int i = 0; i < docGrades.Count(); i++) { DynamicObject doGrade = docGrades[i]; decimal deScoreFrom =Convert.ToDecimal(doGrade["FSCOREFROM"]); decimal deScoreTo = Convert.ToDecimal(doGrade["FSCORETO"]); if(deSumScore >= deScoreFrom && deSumScore <= deScoreTo) { this.View.Model.SetValue("FGrade",Convert.ToInt32(doGrade["FID"])); this.View.Model.SetValue("FSuggestion", doGrade["FDESCRIPTION"]); } } } |
AfterSave
单据保存后插件。主要用于保存后界面的控制、控件的显示以及不需要事务保证的其他数据更新。
3.服务插件
BOS平台抽象了领域模型,针对领域模型定义各种操作并提供操作服务。但很多时候,内置的操作并不一定满足需要。为此在APP服务层提供服务插件,以方便二次开发扩展应用。
服务插件配置是在BOS IDE中操作编辑里:
服务插件运行在App层,因此,在外部系统调用集成服务接口时,随着操作服务的发布,服务插件也会有效。
和校验器配合使用
运行于App层
命名空间
Kingdee.BOS.Core.DynamicForm.PlugIn
继承体系
所有服务插件都应继承自抽象服务插件类。
插件模型 |
继承自抽象类 |
服务插件 |
Kingdee.BOS.Core.DynamicForm.PlugIn.AbstractOperationServicePlugIn |
接口
接口 |
IOperationServicePlugIn
Name |
Description |
|
AfterExecuteOperationTransaction |
执行操作事务后的逻辑处理,后续事情不影响当前操作事务的可以放在此处理 |
|
BeforeExecuteOperationTransaction |
执行操作事务前事件,通知插件对要处理的数据进行排序等预处理 |
|
BeginOperationTransaction |
调用操作事务前触发 |
|
EndOperationTransaction |
调用操作事务成功后触发 |
|
InitializeOperationResult |
操作成功后触发 |
|
OnAddValidators |
通过此事件,通知插件进行添加自定义数据校验器 |
|
OnPrepareOperationServiceOption |
通过此事件,通知插件进行选项设置 |
|
OnPreparePropertys |
准备操作对象实体属性事件,在此事件中可以将校验过程需要的属性对应的Key添加进来以便统一从数据库中加载数据 |
BeforeExecuteOperationTransaction
执行操作事务前插件。通常用于执行操作前数据处理,该插件在webservice服务调用时也会执行。该事件是操作事务前允许处理数据的最后一个插件,为保证操作事务时间最短,在性能优化时会将不需要事务保护的部分服务逻辑放到这个插件里处理。
该插件中不适合用于数据校验,数据校验方法请参考数据校验章节。
例如:
在直接调拨单中,增加保存服务插件,在保存事务前,计算未结算的关联数量。这个数据在结算业务逻辑中使用,必须保证数据准确有效,不需要调拨界面显示。如果在web插件中计算会有2个问题:
1. 数据操作修改后必须重新计算,多次修改要多次计算,效率低;
2. 外部接口调用保存服务时,需要自己计算好填到数据包,如果涉及到本地化设置(如数据精度)等问题,还要调用方特殊处理;
在保存操作增加服务处理步骤:
1. 定义服务插件类StockTransferDirect.SaveService,插件继承AbstractOprerationService;
2. 重载BeforeExecuteOperationTransaction方法,示例代码:
C# |
|
// 保存操作事务前,计算单据上的“未结算关联数量” public override void BeforeExecuteOperationTransaction(BeforeExecuteOperationTransaction e) { if (e.SelectedRows ==null || e.SelectedRows.Count() == 0) { // 没有数据,取消操作(通常此类判断应在web端进行,避免不必要的资源消耗,此处仅示例如何取消操作) e.Cancel = true; return; } DynamicObject[] objs = (from pin e.SelectedRows select p.DataEntity).ToArray(); foreach (DynamicObject datain objs) { DynamicObjectCollection dataentrys = data["TransferDirectEntry"]as DynamicObjectCollection; foreach (DynamicObject entryin dataentrys) { //“未结算关联数量”=“调拨数量”-“关联退货数量”-“结算关联数量”。 decimal qty = Convert.ToDecimal(entry["Qty"]); decimal baseQty =Convert.ToDecimal(entry["BaseQty"]); decimal receiveQty =Convert.ToDecimal(entry["ReceiveQty"]); decimal baseJoinQty =Convert.ToDecimal(entry["BaseJoinQty"]); decimal baseSettQty =Convert.ToDecimal(entry["JoinBaseSettQty"]); decimal SettQty =Convert.ToDecimal(entry["JoinSettleQty"]); entry["JoinUnSettleQty"] = qty - receiveQty - SettQty; entry["JoinBaseUnSettQty"] = baseQty - baseJoinQty - baseSettQty; } } } |
3. 编译后,运行系统,在IDE中配置保存服务插件;
4. 调试测试;
AfterExecuteOperationTransaction
执行操作事务后插件。通常用来处理操作后的相关的数据处理,如生成其他单据、更新状态、运行业务运算等。该插件在操作事务外,执行结果不影响操作,因此该插件要考虑执行失败的逻辑处理。
AfterExecuteOperationTransaction参数:
Name |
Description |
|
DataEntitys |
本次操作事务处理成功的数据实体集合 |
|
SelectedRows |
当前操作校验通过的所有行对象 |
(参数命名空间:BOS.Core.DynamicForm.PlugIn.Args)
审核结束自动生成付款单的代码示例:
C# |
|
public override void AfterExecuteOperationTransaction(AfterExecuteOperationTransaction e) { //审核时,如果弹出信用相关提示警示信息时,e.DataEntitys将没有记录。此时直接退出 if (e.DataEntitys.IsNullOrEmpty() || e.DataEntitys.Count() == 0) { return; } foreach (var dataEntityin e.DataEntitys) { if (this.IsCanSoureBillPush(dataEntity))//申请借款,下推付款申请单 { DynamicObject[] objs = new DynamicObject[1] { dataEntity }; //初始化备用信息 this.OnInit(objs); //生成下游单据 this.ProduceBill(this.OperationResultas ConvertOperationResult); } } } |
BeginOperationTransaction
操作事务开始插件。用于在执行操作前处理数据,该方法与BeforeExecuteOperationTransaction区别主要在于该插件在操作事务内,出错后系统会回滚事务。该插件开发时要特别关注对性能的影响,建议对分录的所有处理考虑批量进行。
参数:
CancelFormService
是否取消执行本操作所关联的表单服务;即终止服务插件,不执行其他表单服务插件。
CancelOperation
是否取消本操作;即终止操作。
简单生产领料单保存前,根据当前单据删除的领料单分录获取关联的源单分录,在保存后,检测简单领料分录是否仍存在该分录ID上拉的行,然后再判断应该更新简单领料分录还是源单分录,重置该分录行的领料标识。
C# |
|
//更新操作前,获取删除的分录数据,在更新后做处理 public override void BeginOperationTransaction(BeginOperationTransactionArgs e) { //获取删除的行 IDbDriver driver = new Kingdee.BOS.App.Data.OLEDbDriver(this.Context); IDataManager manager = DataManagerUtils.GetDataManager(this.BusinessInfo.GetDynamicObjectType(), driver); ISaveDataSet updateData = manager.GetSaveDataSet(e.DataEntitys.ToArray()); DeleteRows = updateData.Tables["T_SP_PICKMTRLDATA"].DeleteRows; if (DeleteRows != null && DeleteRows.Length > 0) { List<long> lstDeleteIds = DeleteRows.Select(w => w.Oid).ToList(); //根据领料单ENTRYID获取关联的源单SRCENTRYID listInStockEntryId = ServiceFactory.GetSpInStockService(this.Context).GetInStcokEntryByPickMtrlEntryId(this.Context, lstDeleteIds); base.BeginOperationTransaction(e); } } |
C# |
|
//更新操作后,根据更新前获取的删除分录的数据,重新计算领料标识 public override void EndOperationTransaction(EndOperationTransactionArgs e) { base.EndOperationTransaction(e); ISpInStockService service= ServiceFactory.GetSpInStockService(this.Context); // 检测简单领料分录是否仍存在该分录ID上拉过来的行,否则更新简单入库的分录行领料标识 if (listInStockEntryId !=null && listInStockEntryId.Count > 0) { // 批量检测 service.ResetIsPickInInStcokEntry(this.Context, listInStockEntryId); } foreach (var dataEntityin e.DataEntitys) { IEnumerable<long> lstEntryId = dataEntity.GetDynamicObjectItemValue // 根据分录ID重置该分录行的领料标识(批量更新) service.UpdateIsPickInInStcokEntry(this.Context, lstEntryId.ToList()); } } |
EndOperationTransaction
操作事务结束插件,此插件在事务内运行,出错后系统会回滚事务。
示例参照BeginOperationTransaction
4、应用案例介绍
4. 应用案例介绍 |
收货单提供以下功能:
1. 增加下拉列表,显示单据头的所有字段;
2. 在分录菜单上增加库存查询(FQueryInventory)菜单项;
3. 点击库存查询时,查询分录上当前焦点所在物料的库存(STK_InvSumQuery);
STK_Inventory
4. 查询库存时按组织隔离,只查询当前组织的库存;
5. 当前分录物料F8时,显示所有组织的物料;
6. 暂存时清空单据类型的值;
7. 物料基础资料增加字段有效期至(F_MCY_ExpiryDate);
8. F8时只显示有效期〉今天的物料;
9. 保存判断物料的库存,如果〉100则提示“库存〉100,是否入库?”;
10. 保存后锁定“收料部门”、“收料员”;
11. 保存后自动记录收料日志(MCY_stk_ReceiptLog);
操作步骤:
1. 增加下拉列表,显示单据头的所有字段;
a) 新建(打开)收货单插件工程(MyDev.K3.SCM.Stock.Business.PlugIn);
b) 重载OnInitialize方法,定义List
c) 通过this.View.BusinessInfo.GetFieldList()方法获取所有字段;
d) 通过this.View.GetControl
e) SetComboItems绑定值;
f) 代码如下:
C# |
|
public override void OnInitialize(InitializeEventArgs e) { base.OnInitialize(e); List<EnumItem> list =new List<EnumItem>(); foreach (Field fieldin this.View.BusinessInfo.GetFieldList()) { EnumItem item = new EnumItem(); item.Caption = field.Name; item.EnumId = field.Key; item.Value = field.Key; list.Add(item); } this.View.GetControl<ComboFieldEditor>("FCombo").SetComboItems(list); } |
2. 在分录菜单上增加库存查询(tbQueryInventory)菜单项;
a) 运行IDE,选择单据体-菜单集合,新增菜单:
b) 保存;
3. 点击库存查询时,查询分录上当前焦点所在物料的库存;
a) 打开插件工程,重载方法EntryBarItemClick
b) 判断BarItemKey==库存查询(tbQueryInventory)
c) 取当前分录行
d) 设置ListShowParameter参数,打开表单
这里介绍2种获取当前分录字段数据的方法:
TryGetEntryCurrentRow:获取单据体当前行,返回是否取到值以及行数据和行号;
另外一种方法:
先获取单据体当前行号,再取指定行数据;
2种方法没什么区别。
示例代码如下:
C# |
|
public override void EntryBarItemClick(BarItemClickEventArgs e) { base.EntryBarItemClick(e); if (e.BarItemKey == "tbQueryInventory") { ShowQueryInventory(); } } private void ShowQueryInventory() { DynamicObject row; int rowIndex; // 直接获取当前分录行返回的是分录行对象。 if (this.Model.TryGetEntryCurrentRow("FEntity",out row, out rowIndex)) { ListShowParameter parameter =new ListShowParameter(); parameter.FormId = "STK_Inventory"; // 即时库存的FormId parameter.MultiSelect = false; parameter.ListFilterParameter.Filter = string.Format(" FMaterialId = '{0}' ",Convert.ToString(row["FBase_Id"])); this.View.ShowForm(parameter); } } |
取单据体当前行号,再取指定行的字段数据的方法如下:
C# |
|
private void ShowQueryInventory() { // 获取当前行 int rowIndex = this.Model.GetEntryCurrentRowIndex("FEntity"); if (rowIndex > -1) //判断当前行有数据 { // 取指定行的物料(ide中设置key为FBase)字段数据 DynamicObject materialObj = (DynamicObject)this.Model.GetValue("FBase", rowIndex); ListShowParameter parameter =new ListShowParameter(); parameter.FormId = "STK_Inventory"; parameter.MultiSelect = false; parameter.ListFilterParameter.Filter = string.Format(" FMaterialId = '{0}' ", materialObj["Id"].ToString()); this.View.ShowForm(parameter); } } |
调试状态下,可以屏蔽代码parameter.ListFilterParameter.Filter看看过滤条件的效果。
注意:ListFilterParameter 的Filter属性设置的字段是用IDE中的字段标识。
4. 查询库存时按组织隔离,只查询当前组织的库存:
a) 增加过滤条件,组织=当前组织
b) parameter.ListFilterParameter.Filter= string.Format(" FORGID ={0}",this.Model.Context.CurrentOrganizationInfo.ID)});
5. 当前分录物料F8时,显示所有组织的物料;
a) 重载AuthPermissionBeforeF7Select方法,设置参数IsIsolationOrg = false;
b) 同样,如果需要F8时控制只显示当前组织的物料,该参数设置为true。
注意:
在BOS系统中,默认是按组织隔离的,即非共享基础资料,在F8时都是只显示当前组织的物料。
代码示例如下:
C# |
|
public override void AuthPermissionBeforeF7Select(AuthPermissionBeforeF7SelectEventArgs e) { base.AuthPermissionBeforeF7Select(e); if (e.FieldKey == "FBase") { e.IsIsolationOrg = false; } } |
6. 暂存时清空单据类型的值;
C# |
|
public override void BeforeDoOperation(BeforeDoOperationEventArgs e) { base.BeforeDoOperation(e); if (e.Operation.FormOperation.Operation.Equals("DRAFT",StringComparison.OrdinalIgnoreCase)) { this.Model.SetValue("FBillTypeID",null); } } |
7. F8时只显示审核日期〉2014-03-22的供应商;
a) 重载BeforeF7Select事件;
b) 设置列表过滤参数ListFilterParameter的属性Filter;
C# |
|
public override void BeforeF7Select(BeforeF7SelectEventArgs e) { base.BeforeF7Select(e); if (e.FieldKey == "FSupplierId1") { string filter = " FCreateDate > '2014-03-20' "; if (string.IsNullOrEmpty(e.ListFilterParameter.Filter)) { e.ListFilterParameter.Filter = filter; } else { e.ListFilterParameter.Filter += " AND " + filter; } } } |
8. 保存判断物料的库存,如果〉100则提示“库存〉100,是否入库?”;
a) 新建收货单服务插件工程MyDev.K3.SCM.App.Stock.ServicePlugIn;
b) 定义保存服务类SaveServicePlugIn,继承自AbstractOperationServicePlugIn;
c) 重载OnAddValidators方法;
代码示例如下:
C# |
|
public override void OnAddValidators(AddValidatorsEventArgs e) { base.OnAddValidators(e); SaveValidator saveValidator =new SaveValidator(); saveValidator.EntityKey = "FBillHead"; e.Validators.Add(saveValidator); } |
d) 定义保存校验类SaveValidator,继承自AbstractValidator;
e) 重载方法:Validate:
i. 获取单据体分录数据,取到物料Id;
ii. 查询物料库存;
iii. 检查库存是否〉100;
iv. 构造校验结果信息;
代码示例:
C# |
|
public override void Validate(ExtendedDataEntity[] dataEntities,ValidateContext validateContext, Kingdee.BOS.Context ctx) { if (dataEntities == null || dataEntities.Length == 0) { return; } Dictionary<long,decimal> dictErrMaterialId = new Dictionary<long,decimal>(); //取所有物料 List<long> listMaterialId =new List<long>(); foreach (ExtendedDataEntity entityObjin dataEntities) { DynamicObjectCollection collection = (DynamicObjectCollection)entityObj["FEntity"]; foreach (DynamicObject rowObjin collection) { listMaterialId.Add((long)rowObj["FBase_Id"]); } } string sql = " select a.FMATERIALID, sum(a.FBASEQTY) FQTY from T_STK_INVENTORY a where exists (select 1 from TABLE(fn_StrSplit(@FMATERIALID, ',',1)) t where t.FID = a. FMATERIALID) group by FMATERIALID "; SqlParam param = new SqlParam("@FMATERIALID",KDDbType.udt_inttable, listMaterialId.Distinct().ToArray()); using (IDataReader dr =DBUtils.ExecuteReader(this.Context, sql, param)) { while (dr.Read()) { decimal qty = Convert.ToDecimal(dr["FQTY"]); if (qty > 100) { dictErrMaterialId.Add(Convert.ToInt64(dr["FMATERIALID"]), qty); } } } foreach (ExtendedDataEntity entityObjin dataEntities) { DynamicObjectCollection collection = (DynamicObjectCollection)entityObj["FEntity"]; foreach (DynamicObject rowObjin collection) { if (dictErrMaterialId.ContainsKey((long)rowObj["FBase_Id"])) { ValidationErrorInfo errinfo =new ValidationErrorInfo("FMATERIALID",Convert.ToString(entityObj.DataEntity["Id"]), entityObj.DataEntityIndex,Convert.ToInt32(rowObj["Id"]),"SaveValidator", "库存数量大于100","校验失败",ErrorLevel.Error); validateContext.AddError(entityObj, errinfo); } } } } |
f) 重载方法:Validate:
9. 保存后锁定“收料部门”、“收料员”;
a) 锁定字段的方法:this.View.LockField;
b) 该锁定与事务无关,只要在客户端保存后事件(AfterBarItemClick)处理即可;
c) “收料部门”、“收料员”的key可以在IDE设计器中拷贝;
代码如下:
C# |
|
public override void AfterBarItemClick(AfterBarItemClickEventArgs e) { base.AfterBarItemClick(e); if (e.BarItemKey == "tbSave") { this.View.LockField("FBase1",true); this.View.LockField("FBase2",true); } } |
10. 保存后自动记录收料日志(KDV_stk_ReceiptLog);
根据需求设计收料日志表:
字段 |
名称 |
类型 |
说明 |
KDV_ID |
日志ID |
int |
自增长 |
KDV_UserID |
操作用户 |
Int |
关联用户表ID |
KDV_Date |
操作时间 |
Datetime |
缺省getdate |
KDV_Content |
日志内容 |
Nvarchar(2000) |
保存有2种方法:
方法1:
a) 在IDE中定义收料日志基础资料;
b) 打开收货单服务插件保存服务类SaveServicePlugIn;
c) 根据收料日志基础资料的元数据定义,创建动态实体对象;
d) 设置对象属性值;
e) 调用BusinessDataService服务的保存方法保存动态实体对象;
代码如下:
C# |
|
public override void AfterExecuteOperationTransaction(AfterExecuteOperationTransaction e) { base.AfterExecuteOperationTransaction(e); MetaDataService metaService =new MetaDataService(); FormMetadata formMetaData = (FormMetadata)metaService.Load(this.Context,"1823871d-b9cf-4d8b-93af-39c0c37011a5"); DynamicObjectType dt = formMetaData.BusinessInfo.GetDynamicObjectType(); DynamicObject obj = new DynamicObject(dt); dt.Properties["KDV_UserID_Id"].SetValueFast(obj,this.Context.UserId); dt.Properties["KDV_Content"].SetValueFast(obj,"保存"); ISaveService saveService =ServiceHelper.GetService<ISaveService>(); saveService.Save(this.Context,new DynamicObject[] { obj }); } |
方法2:
a) 自定义收料日志表;
b) 获取日志的自增长(序列)值;
c) 执行insert;
代码如下:
C# |
|
public override void AfterExecuteOperationTransaction(AfterExecuteOperationTransaction e) { base.AfterExecuteOperationTransaction(e); SequenceReader sequence =new SequenceReader(this.Context); int[] ids = (int[])sequence.GetSequence("KDV_stk_ReceiptLog", 1); int id = ids[0]; string sql = " insert into KDV_stk_ReceiptLog(FID, KDV_UserID, KDV_Content) values (@FID, @KDV_UserID, @KDV_Content) "; SqlParam[] sqlParams =new SqlParam[3]; sqlParams[0] = new SqlParam("@FID", KDDbType.Int64, id); sqlParams[1] = new SqlParam("@KDV_UserID",KDDbType.Int64, this.Context.UserId); sqlParams[2] = new SqlParam("@KDV_Content",KDDbType.String, "保存"); DBUtils.Execute(this.Context, sql, sqlParams); } |