文为金蝶云星空自定义校验方法的新手教程。内容为技术类文章。做星空系统二次开发的新手朋友,可以收藏作为参考。非技术职业的朋友,可以直接划走,以免耽误您的宝贵时间。
阅读对象:星空系统二次开发新手
需求场景:在插件开发过程中,实现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地狱啊!
理想的父组件代码应该是这样:
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("太好了,我也喜欢你!!!");
}
//不喜欢的鼠标移动事件
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小时内与您取得联系。