文為金蝶云星空自定義校驗方法的新手教程。內容為技術類文章。做星空系統二次開發的新手朋友,可以收藏作為參考。非技術職業的朋友,可以直接劃走,以免耽誤您的寶貴時間。
閱讀對象:星空系統二次開發新手
需求場景:在插件開發過程中,實現BOS不容易配置的單據數據校驗。實現校驗一個單據編號字段必須是11個字符。
開發語言:C#
開發工具:Visual Studio 2019
星空版本:7.6.0
說明:本文前提是開發機已經安裝好金蝶云星空系統和金蝶BOS IDE。
金蝶云社區課程詳情-課程詳情-金蝶云社區官網
本文是承接“金蝶云星空插件實戰開發-服務插件”這篇教程。是在這篇教程的基礎上做的擴展。
如果想要測試本文的代碼,必須順利完成服務插件的教程。
打開服務插件的代碼項目,在Visual Studio右側的“解決方案資源管理器”中郵件點擊服務插件工程,新建一個類,類名:PurchaseValidator.cs,如下圖所示:
既然創建的一個類文件,那下面就直接敲代碼咯,代碼如下:
using System;
using Kingdee.BOS;
using Kingdee.BOS.Core;
using Kingdee.BOS.Core.Validation;
namespace Test.K3Cloud.SCM.MyAppPlugin
{
public class PurchaseValidator : AbstractValidator
{
/// <summary>
/// 初始化
/// </summary>
/// <param name="validateContext"></param>
/// <param name="ctx"></param>
public override void InitializeConfiguration(ValidateContext validateContext, Context ctx)
{
base.InitializeConfiguration(validateContext, ctx);
if (validateContext.BusinessInfo !=null)
{
EntityKey=validateContext.BusinessInfo.GetEntity(0).Key;
}
}
/// <summary>
/// 自定義校驗
/// </summary>
/// <param name="dataEntities"></param>
/// <param name="validateContext"></param>
/// <param name="ctx"></param>
public override void Validate(ExtendedDataEntity[] dataEntities, ValidateContext validateContext, Context ctx)
{
if (validateContext.IgnoreWarning)
{
return; //警告已經被用戶忽略,就不需要再次執行了
}
if (dataEntities==null || dataEntities.Length <=0)
{
return;
}
// 循環校驗每一個數據包(一個數據包對應一張單據)
foreach (var et in dataEntities)
{
// 訂單編號在單據數據包中的字段名為:BillNo
string billNo=Convert.ToString(et.DataEntity["BillNo"]);
if (billNo.Length !=11)
{
validateContext.AddError(et, new ValidationErrorInfo(
"BillNo", // 出錯的字段Key,可以空
Convert.ToString(et.DataEntity[0]), // 數據包內碼,必填,后續操作會據此內碼避開此數據包
et.DataEntityIndex, // 出錯的數據包在全部數據包中的順序
et.RowIndex, // 出錯的數據行在全部數據行中的順序,如果校驗基于單據頭,此為0
"Error 0", // 錯誤編碼,可以任意設定一個字符,主要用于追查錯誤來源
"數據校驗未通過,單據編號必須是11個字符", // 錯誤的詳細提示信息
"單據合法性檢查", // 錯誤的簡明提示信息
ErrorLevel.FatalError // 錯誤級別
));
}
}
}
}
}
自定義校驗器類需要繼承 AbstractValidator 類。
在自定義校驗器類中,dataEntities 為客戶端傳遞過來的單據的數據。
可以多個單據同時操作,所以這里,單據數據是一個數組。
上面代碼中 et.DataEntity 是某個單據的數據包。
接下來我們在服務插件中注冊自定義校驗器。
為了方便大家順利閱讀代碼,下面將服務插件的完整代碼貼出來。
如下面代碼所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Kingdee.BOS;
using Kingdee.BOS.Core.DynamicForm;
using Kingdee.BOS.Core.DynamicForm.PlugIn;
using Kingdee.BOS.Core.DynamicForm.PlugIn.Args;
using Kingdee.BOS.Core.Interaction;
using Newtonsoft.Json.Linq;
namespace Test.K3Cloud.SCM.MyAppPlugin
{
public class Class1: AbstractOperationServicePlugIn
{
// SpensorKey
private const string SpensorKey="DefaultSpensorKey";
/// <summary>
/// 單據校驗。
/// </summary>
/// <param name="e"></param>
public override void OnAddValidators(AddValidatorsEventArgs e)
{
base.OnAddValidators(e);
var purchaseValidator=new PurchaseValidator();
e.Validators.Add(purchaseValidator); // 注冊自定義校驗器
}
public override void EndOperationTransaction(EndOperationTransactionArgs e)
{
base.EndOperationTransaction(e);
// 當保存訂單時
if (FormOperation.OperationName=="保存")
{
/// 構造錯誤信息
JArray errMsg=new JArray {
new JValue("01"),
new JValue("這是一條提示信息")
};
bool ignore=false; // 窗口顯示狀態,默認不顯示
Option.TryGetVariableValue(SpensorKey, out ignore);
if (!ignore && !Option.HasInteractionFlag(SpensorKey))
{
KDInteractionException ie=ShowErrorMsg(Context, SpensorKey, ignore, errMsg);
throw ie;
}
}
}
/// <summary>
/// 信息提示窗口
/// </summary>
/// <param name="context">上下文對象</param>
/// <param name="spensorKey">窗口標識</param>
/// <param name="ignore">狀態</param>
/// <param name="errorMsg">錯誤信息</param>
/// <returns></returns>
public static KDInteractionException ShowErrorMsg(Context context, string spensorKey, bool ignore, JArray errorMsg)
{
if (errorMsg.Count() !=2)
{
return null;
}
string titleMsg="代碼~|~信息";
string errMsg="{0}~|~{1}";
K3DisplayerModel model=K3DisplayerModel.Create(context, titleMsg);
model.AddMessage(string.Format(errMsg, errorMsg[0].ToString(), errorMsg[1].ToString()));
model.Option.SetVariableValue(K3DisplayerModel.CST_FormTitle, "單據操作有以下信息出錯,需要繼續嗎?");
model.OKButton.Caption=new LocaleValue("是");
model.CancelButton.Visible=model.OKButton.Visible=true;
model.CancelButton.Caption=new LocaleValue("否");
KDInteractionException ie=new KDInteractionException(spensorKey);
ie.InteractionContext.InteractionFormId="BOS_K3Displayer";
ie.InteractionContext.K3DisplayerModel=model;
ie.InteractionContext.IsInteractive=true;
return ie;
}
}
}
我們在 OnAddValidators 方法中注冊自定義校驗器。
到此,自定義校驗器最基本的代碼就完成了。
當然了,本文的校驗只是做演示用,大家根據實際需求,做具體的校驗。
敲完代碼,我們編譯插件。編譯插件的方法,金蝶云星空插件實戰開發-新手入門教程-服務插件這篇教程里有描述,這里就不做重復說明了。
插件編譯完成之后,重啟IIS服務。然后測試效果,如下圖所示:
如果一次沒有成功,沒關系,萬事開頭難,大家跟著做多做幾次,希望本教程能夠幫助到大家!加油!!
本教程源碼:
https://gitee.com/hsg4ok_admin/kingdee_documents/tree/master/%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81/Test.K3Cloud.SCM.MyAppPlugin
更多精彩內容發布于公眾號:代碼乾坤 (CoderLand)
個Hook讓你體驗極致舒適的Dialog使用方式!
為啥是地獄?
因為凡是有Dialog出現的頁面,其代碼絕對優雅不起來!因為一旦你在也個組件中引入Dialog,就最少需要額外維護一個visible變量。如果只是額外維護一個變量這也不是不能接受,可是當同樣的Dialog組件,即需要在父組件控制它的展示與隱藏,又需要在子組件中控制。
為了演示我們先實現一個MyDialog組件,代碼來自ElementPlus的Dialog示例
html復制代碼<script setup lang="ts">
import { computed } from 'vue';
import { ElDialog } from 'element-plus';
const props=defineProps<{
visible: boolean;
title?: string;
}>();
const emits=defineEmits<{
(event: 'update:visible', visible: boolean): void;
(event: 'close'): void;
}>();
const dialogVisible=computed<boolean>({
get() {
return props.visible;
},
set(visible) {
emits('update:visible', visible);
if (!visible) {
emits('close');
}
},
});
</script>
<template>
<ElDialog v-model="dialogVisible" :title="title" width="30%">
<span>This is a message</span>
<template #footer>
<span>
<el-button @click="dialogVisible=false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible=false"> Confirm </el-button>
</span>
</template>
</ElDialog>
</template>
就像下面這樣:
示例代碼如下:
html復制代碼<script setup lang="ts">
import { ref } from 'vue';
import { ElButton } from 'element-plus';
import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';
const dialogVisible=ref<boolean>(false);
const dialogTitle=ref<string>('');
const handleOpenDialog=()=> {
dialogVisible.value=true;
dialogTitle.value='父組件彈窗';
};
const handleComp1Dialog=()=> {
dialogVisible.value=true;
dialogTitle.value='子組件1彈窗';
};
const handleComp2Dialog=()=> {
dialogVisible.value=true;
dialogTitle.value='子組件2彈窗';
};
</script>
<template>
<div>
<ElButton @click="handleOpenDialog"> 打開彈窗 </ElButton>
<Comp text="子組件1" @submit="handleComp1Dialog"></Comp>
<Comp text="子組件2" @submit="handleComp2Dialog"></Comp>
<MyDialog v-model:visible="dialogVisible" :title="dialogTitle"></MyDialog>
</div>
</template>
這里的MyDialog會被父組件和兩個Comp組件都會觸發,如果父組件并不關心子組件的onSubmit事件,那么這里的submit在父組件里唯一的作用就是處理Dialog的展示?。。∵@樣真的好嗎?不好!
來分析一下,到底哪里不好!
MyDialog本來是submit動作的后續動作,所以理論上應該將MyDialog寫在Comp組件中。但是這里為了管理方便,將MyDialog掛在父組件上,子組件通過事件來控制MyDialog。
再者,這里的handleComp1Dialog和handleComp2Dialog函數除了處理MyDialog外,對于父組件完全沒有意義卻寫在父組件里。
如果這里的Dialog多的情況下,簡直就是Dialog地獄?。?/p>
理想的父組件代碼應該是這樣:
html復制代碼<script setup lang="ts">
import { ElButton } from 'element-plus';
import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';
const handleOpenDialog=()=> {
// 處理 MyDialog
};
</script>
<template>
<div>
<ElButton @click="handleOpenDialog"> 打開彈窗 </ElButton>
<Comp text="子組件1"></Comp>
<Comp text="子組件2"></Comp>
</div>
</template>
在函數中處理彈窗的相關邏輯才更合理。
朕觀之,是書之文或不雅,致使人之心有所厭,何得無妙方可解決?
依史記之辭曰:“天下苦Dialog久矣,苦楚深深,望有解脫之道?!庇谑?,諸位賢哲紛紛舉起討伐Dialog之旌旗,終“命令式Dialog”逐漸突破困境之境地。
沒錯現在網上對于Dialog的困境,給出的解決方案基本上就“命令式Dialog”看起來比較優雅!這里給出幾個網上現有的命令式Dialog實現。
吐槽一下~,這種是能在函數中處理彈窗邏輯,但是缺點是MyDialog組件與showMyDialog是兩個文件,增加了維護的成本。
基于第一種實現的問題,不就是想讓MyDialog.vue和.js文件合體嗎?于是諸位賢者想到了JSX。于是進一步的實現是這樣:
嗯,這下完美了!
完美?還是要吐槽一下~
首先承認一點命令式的封裝的確可以解決問題,但是現在的封裝都存一定的槽點。
如果有一種方式,即保持原來對話框的編寫方式不變,又不需要關心JSX和template的問題,還保存了命令式封裝的特點。這樣是不是就完美了?
那真的可以同時做到這些嗎?
如果存在一個這樣的Hook可以將狀態驅動的Dialog,轉換為命令式的Dialog嗎,那不就行了?
父組件這樣寫:
html復制代碼<script setup lang="ts">
import { ElButton } from 'element-plus';
import { useCommandComponent } from '../../hooks/useCommandComponent';
import Comp from './components/Comp.vue';
import MyDialog from './components/MyDialog.vue';
const myDialog=useCommandComponent(MyDialog);
</script>
<template>
<div>
<ElButton @click="myDialog({ title: '父組件彈窗' })"> 打開彈窗 </ElButton>
<Comp text="子組件1"></Comp>
<Comp text="子組件2"></Comp>
</div>
</template>
Comp組件這樣寫:
html復制代碼<script setup lang="ts">
import { ElButton } from 'element-plus';
import { useCommandComponent } from '../../../hooks/useCommandComponent';
import MyDialog from './MyDialog.vue';
const myDialog=useCommandComponent(MyDialog);
const props=defineProps<{
text: string;
}>();
</script>
<template>
<div>
<span>{{ props.text }}</span>
<ElButton @click="myDialog({ title: props.text })">提交(需確認)</ElButton>
</div>
</template>
對于MyDialog無需任何改變,保持原來的樣子就可以了!
useCommandComponent真的做到了,即保持原來組件的編寫方式,又可以實現命令式調用!
使用效果:
是不是感受到了莫名的舒適?
不過別急,要想體驗這種極致的舒適,你的Dialog還需要遵循兩個約定!
如果想要極致舒適的使用useCommandComponent,那么彈窗組件的編寫就需要遵循一些約定(其實這些約定應該是彈窗組件的最佳實踐)。
約定如下:
如果你的彈窗組件滿足上面兩個約定,那么就可以通過useCommandComponent極致舒適的使用了!!
這兩項約定雖然不是強制的,但是這確實是最佳實踐!不信你去翻所有的UI框看看他們的實現。我一直認為學習和生產中多學習優秀框架的實現思路很重要!
這時候有的同學可能會說:哎嘿,我就不遵循這兩項約定呢?我的彈窗就是要標新立異的不用visible屬性來控制打開和關閉,我起名為dialogVisible呢?我的彈窗就是沒有close事件呢?我的事件是具有業務意義的submit、cancel呢?...
得得得,如果真的沒有遵循上面的兩個約定,依然可以舒適的使用useCommandComponent,只不過在我看來沒那么極致舒適!雖然不是極致舒適,但也要比其他方案舒適的多!
如果你的彈窗真的沒有遵循“兩個約定”,那么你可以試試這樣做:
html復制代碼<script setup lang="ts">
// ...
const myDialog=useCommandComponent(MyDialog);
const handleDialog=()=> {
myDialog({
title: '父組件彈窗',
dialogVisible: true,
onSubmit: ()=> myDialog.close(),
onCancel: ()=> myDialog.close(),
});
};
</script>
<template>
<div>
<ElButton @click="handleDialog"> 打開彈窗 </ElButton>
<!--...-->
</div>
</template>
如上,只需要在調用myDialog函數時在props中將驅動彈窗的狀態設置為true,在需要關閉彈窗的事件中調用myDialog.close()即可!
這樣是不是看著雖然沒有上面的極致舒適,但是也還是挺舒適的?
對于useCommandComponent的實現思路,依然是命令式封裝。相比于上面的那兩個實現方式,useCommandComponent是將組件作為參數傳入,這樣保持組件的編寫習慣不變。并且useCommandComponent遵循單一職責原則,只做好組件的掛載和卸載工作,提供足夠的兼容性。
其實useCommandComponent有點像React中的高階組件的概念
源碼不長,也很好理解!在實現useCommandComponent的時候參考了ElementPlus的MessageBox。
源碼如下:
ts復制代碼import { AppContext, Component, ComponentPublicInstance, createVNode, getCurrentInstance, render, VNode } from 'vue';
export interface Options {
visible?: boolean;
onClose?: ()=> void;
appendTo?: HTMLElement | string;
[key: string]: unknown;
}
export interface CommandComponent {
(options: Options): VNode;
close: ()=> void;
}
const getAppendToElement=(props: Options): HTMLElement=> {
let appendTo: HTMLElement | null=document.body;
if (props.appendTo) {
if (typeof props.appendTo==='string') {
appendTo=document.querySelector<HTMLElement>(props.appendTo);
}
if (props.appendTo instanceof HTMLElement) {
appendTo=props.appendTo;
}
if (!(appendTo instanceof HTMLElement)) {
appendTo=document.body;
}
}
return appendTo;
};
const initInstance=<T extends Component>(
Component: T,
props: Options,
container: HTMLElement,
appContext: AppContext | null=null
)=> {
const vNode=createVNode(Component, props);
vNode.appContext=appContext;
render(vNode, container);
getAppendToElement(props).appendChild(container);
return vNode;
};
export const useCommandComponent=<T extends Component>(Component: T): CommandComponent=> {
const appContext=getCurrentInstance()?.appContext;
const container=document.createElement('div');
const close=()=> {
render(null, container);
container.parentNode?.removeChild(container);
};
const CommandComponent=(options: Options): VNode=> {
if (!Reflect.has(options, 'visible')) {
options.visible=true;
}
if (typeof options.onClose !=='function') {
options.onClose=close;
} else {
const originOnClose=options.onClose;
options.onClose=()=> {
originOnClose();
close();
};
}
const vNode=initInstance<T>(Component, options, container, appContext);
const vm=vNode.component?.proxy as ComponentPublicInstance<Options>;
for (const prop in options) {
if (Reflect.has(options, prop) && !Reflect.has(vm.$props, prop)) {
vm[prop as keyof ComponentPublicInstance]=options[prop];
}
}
return vNode;
};
CommandComponent.close=close;
return CommandComponent;
};
export default useCommandComponent;
除了命令式的封裝外,我加入了const appContext=getCurrentInstance()?.appContext;。這樣做的目的是,傳入的組件在這里其實已經獨立于應用的Vue上下文了。為了讓組件依然保持和調用方相同的Vue上下文,我這里加入了獲取上下文的操作!
基于這個情況,在使用useCommandComponent時需要保證它在setup中被調用,而不是在某個點擊事件的處理函數中哦~
如果你覺得useCommandComponent對你在開發中有所幫助,麻煩多點贊評論收藏
如果useCommandComponent對你實現某些業務有所啟發,麻煩多點贊評論收藏
如果...,麻煩多點贊評論收藏
如果大家有其他彈窗方案,歡迎留言交流哦!
屬性:
Text:向用戶展示的信息。
1、界面
2、代碼
namespace WindowsFormsApplication2
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
//喜歡的單擊事件
private void btnok_Click(object sender, EventArgs e)
{
MessageBox.Show("太好了,我也喜歡你?。。?#34;);
}
//不喜歡的鼠標移動事件
private void btnNG_MouseMove(object sender, MouseEventArgs e)
{
Random size_X=new Random(); //定義一個隨機數對象
int x=size_X.Next(0, this.ClientSize.Width - btnNG.Size.Width);
int y=size_X.Next(0, this.ClientSize.Height - btnNG.Size.Height);
Point point=new Point(x, y);
btnNG.Location=point;
}
}
}
3、效果:
永遠點不到不喜歡按鈕。
(1)屬性:
FormBoarderStyle:窗體的邊緣樣式。
Icon:窗體左上角的圖標。
MaximizeBox:最大化按鈕是否可用。
Minimizebox:最小化按鈕是否可用。
Opacity:透明度 0-1。
ShowInTaskbar:是否在任務欄上顯示。
StartPosition:啟動程序時,主窗體顯示的位置。
Text:標題欄中的文本。
TopMost:保持在最上面,有可能擋住下面的窗體。
WindowState:指示窗體處于是最大化還是最小化還是正常。
ClientSize:指窗體工作區域的大小。
Load(默認):窗體在顯示之前最后一個被觸發的事件,所以我們一般在這里對窗體上的控件進行賦值初始化。
Activated:窗體獲得焦點時觸發。
Deactivate:窗體失去焦點時觸發。
注意:窗體的Enter和Leave事件被取消,請不要使用。
FormClosing:窗體進入關閉前觸發的事件。(即執行完FormClosing事件里面的代碼后,窗體關閉)
FormClosed:窗體關閉后觸發的事件。(即執行完窗體關閉后,再執行FormClosed事件里面的代碼)
Close(); 關閉事件。
Show(); 以非模態形式顯示。(顯示的窗體可以有多個相同的窗體,程序直接往下執行)
ShowDialog(); 以模態的形式顯示。(顯示的窗體具有唯一性,只有關閉當前窗體,程序才往下執行)
this.Hide(); //本類隱藏,相當于Visible賦值false。
this.Show(); //本類顯示,相當于Visible賦值true。
btnButton.hide(); //名字叫btnButton的按鈕隱藏,相當于Visible賦值false。
btnButton.Show(); //名字叫btnButton的按鈕顯示,相當于Visible賦值true。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。