開發工具
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 | |
動態表單數據綁定器抽象類 | ||
動態表單插件抽象基類 | ||
動態表單頁面元數據構建插件 | ||
操作服務插件抽象類 |
主要接口:
Interface | Description | |
動態表單Model層插件控制接口;實現本接口的插件,可以接收Model層的事件 | ||
動態表單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 | |
菜單單擊事件完成後處理擴展接口 | ||
綁定數據後事件處理後擴展接口 | ||
按鈕單擊之後調用 | ||
分錄行拷貝後調用 | ||
操作完成後調用 | ||
分錄菜單單擊事件處理擴展接口 | ||
基礎資料選擇返回後調用 | ||
工具欄單擊事件處理擴展接口 | ||
主菜單單擊事件處理擴展接口 | ||
綁定數據前事件處理後擴展接口,主要用於加載數據到界面前對控件狀態進行設置 | ||
頁面準備關閉 | ||
操作開始前調用 | ||
基礎資料界面調出之前拋出 | ||
按鈕單擊時調用 | ||
分錄行單擊事件 | ||
分錄行雙擊事件 | ||
分錄菜單單擊事件處理擴展接口 | ||
表格按鈕單擊時調用 | ||
字段標題單擊事件 | ||
單據體列全選事件 | ||
列表控件單擊事件 | ||
頁面初始化 | ||
頁籤控件的頁籤選中事件 | ||
工具欄單擊事件處理擴展接口 | ||
KDTree 拖拽事件 | ||
TreeView 節點單擊之後調用 | ||
TreeView 節點雙擊之後調用 |
模型訪問接口
接口名:IdynamicFormModelPlugIn
動態表單Model層插件控制接口;實現本接口的插件,可以接收Model層的事件。主要包括:
Name | Description | |
業務對象創建後的擴展接口 | ||
新增、插入、多行輸入後調用 | ||
值改變更新前的擴展接口 | ||
創建新業務對象擴展接口,插件可以更加需要自己創建對象 | ||
字段值改變後擴展接口 |
加載機制
動態表單元模型包括外觀模型和表單邏輯模型,第一次訪問時會先加載元數據,初始化視圖和模型對象,初始化頁面,然後創建數據包並綁定數據。
對於二次開發提供了一系列插件允許二次開發在加載表單時對視圖、模型、數據包及界面進行控制,插件在加載過程中的執行順序如下:
OnInitialize 頁面初始化
CreateNewData 動態表單數據包創建
AfterCreateNewEntryRow 創建分錄行後
AfterCreateNewData 動態表單數據包創建後
BeforeBindData 綁定數據前事件
AfterBindData 綁定數據及控件狀態
BeforeClosed 頁面關閉前
初始化方法
OnInitialize
該插件負責動態表單實例初始化,包括單據Global參數(當然有些參數僅僅在使用時候才獲取),動態初始化控件數據源等。
比如,批量修改界面初始化時將允許修改的字段加入到下拉列表。
C# | |
///<summary> ///界面初始化 ///</summary> ///<param name="e"></param> 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# | |
///<summary> ///創建數據包事件處理;由插件處理數據包的創建過程,界面僅展示 ///</summary> ///<param name="e"></param> 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# | |
///<summary> ///創建新的分錄行事件 ///</summary> ///<param name="e"></param> 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) // 插入行不賦值 { // 設置工序號=上取整((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# | |
///<summary> ///模型數據包創建完畢,顯示訂單界面 ///</summary> ///<param name="e"></param> 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<SplitContainer>("FSpliter").HideFirstPanel(true); 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# | |
///<summary> ///界面關閉前事件:判斷用戶是否修改了數據,提示保存 ///</summary> ///<param name="e"></param> 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 | |
執行操作事務後的邏輯處理,後續事情不影響當前操作事務的可以放在此處理 | ||
執行操作事務前事件,通知插件對要處理的數據進行排序等預處理 | ||
調用操作事務前觸發 | ||
調用操作事務成功後觸發 | ||
操作成功後觸發 | ||
通過此事件,通知插件進行添加自定義數據校驗器 | ||
通過此事件,通知插件進行選項設置 | ||
準備操作對象實體屬性事件,在此事件中可以將校驗過程需要的屬性對應的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 | |
本次操作事務處理成功的數據實體集合 | ||
當前操作校驗通過的所有行對象 |
(參數命名空間: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<DynamicObjectCollection>(ENTITY_ORM_Entity).Where(w => w.GetDynamicObjectItemValue<string>( ORM_SrcBillType_Id) == SCMFormIdConst.SpInStockBill).Distinct().Select(w => w.GetDynamicObjectItemValue<long>( ORM_SrcEnteryId)); // 根據分錄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<EnumItem>用於存儲下拉列表枚舉值;
c) 通過this.View.BusinessInfo.GetFieldList()方法獲取所有字段;
d) 通過this.View.GetControl<ComboFieldEditor>方法獲取界面上的下拉列表控件;
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); } |