本章导读:在本系列的第一篇中,我们学习了表单插件的基础架构、生命周期事件、数据获取方法以及KSQL数据库操作。本篇将在此基础上深入讲解表单插件的高级应用场景------F7字段的精细控制、动态表单弹窗交互、操作拦截与事务控制、以及性能优化技巧。这些技能是区分"会用插件"与"精通插件"的关键分水岭。
第1章:F7字段的精细控制
F7字段(基础资料选择字段)是金蝶云星空中最常用的字段类型之一。表单插件提供了丰富的扩展点,让开发者能够精细控制F7的过滤、赋值和联动行为。
1.1 F7字段的数据结构
在深入了解F7控制之前,必须先理解F7字段在数据包中的存储结构。一个基础资料字段在数据库中包含多个关联字段:
| 字段后缀 | 类型 | 存储内容 |
|---|---|---|
{字段Key} |
大整数 | 基础资料内码(FID) |
{字段Key}_Id |
大整数 | 同内码(别名) |
{字段Key}_Name |
字符串 | 基础资料名称 |
{字段Key}_Number |
字符串 | 基础资料编码 |
获取F7字段的三种方式:
csharp
// 方式1:获取内码(推荐)
long materialId = Convert.ToInt64(this.View.Model.GetValue("F_PAEZ_MaterialId"));
// 方式2:获取完整数据包
DynamicObject material = this.View.Model.GetValue("F_PAEZ_MaterialId") as DynamicObject;
if (material != null)
{
long id = Convert.ToInt64(material["Id"]);
string name = material["Name"]?.ToString();
string number = material["Number"]?.ToString();
}
// 方式3:直接获取特定属性(需要字段开启了属性绑定)
string materialName = this.View.Model.GetValue("F_PAEZ_MaterialId_Name")?.ToString();
1.2 BeforeF7Select:F7打开前的动态过滤
BeforeF7Select 在用户点击F7按钮、弹窗打开之前触发,是实现动态F7过滤的核心事件。
csharp
using Kingdee.BOS.Core.DynamicForm.PlugIn.Args;
using Kingdee.BOS.ORM;
/// <summary>
/// F7打开前过滤
/// </summary>
public override void BeforeF7Select(BeforeF7SelectEventArgs e)
{
base.BeforeF7Select(e);
// 判断是哪个F7字段
if (e.FieldKey == "F_PAEZ_MaterialId")
{
// 场景1:只显示已启用的物料
e.Filter = "FUseState = 'A'";
// 场景2:根据当前组织过滤(获取当前用户的组织)
string orgId = this.Context.OrganizationId.ToString();
if (!string.IsNullOrEmpty(orgId))
{
e.Filter = $"FUseState = 'A' AND FOrgId = '{orgId}'";
}
// 场景3:动态过滤------根据单据头的供应商过滤物料(物料启用了供应商关联)
string supplierId = this.View.Model.GetValue("F_PAEZ_SupplierId")?.ToString();
if (!string.IsNullOrEmpty(supplierId))
{
// 假设物料基础资料上有"默认供应商"字段
e.Filter = $"FUseState = 'A' AND FDefaultSupplierId = '{supplierId}'";
}
// 场景4:排除已在当前分录中选中的物料
int currentRow = e.RowIndex; // 当前行索引
if (currentRow >= 0)
{
HashSet<string> selectedMaterials = new HashSet<string>();
int entryCount = this.View.Model.GetEntryRowCount("F_PAEZ_Entry");
for (int i = 0; i < entryCount; i++)
{
if (i == currentRow) continue; // 排除当前行
string materialId = this.View.Model.GetValue("F_PAEZ_MaterialId", i)?.ToString();
if (!string.IsNullOrEmpty(materialId))
{
selectedMaterials.Add(materialId);
}
}
if (selectedMaterials.Count > 0)
{
string excludeFilter = $"FID NOT IN ({string.Join(",", selectedMaterials)})";
e.Filter = string.IsNullOrEmpty(e.Filter) ? excludeFilter : $"{e.Filter} AND {excludeFilter}";
}
}
}
// 供应商F7过滤:根据采购组织过滤
if (e.FieldKey == "F_PAEZ_SupplierId")
{
string purchaseOrg = this.View.Model.GetValue("F_PAEZ_PurchaseOrg")?.ToString();
if (!string.IsNullOrEmpty(purchaseOrg))
{
e.Filter = $"FUseState = 'A' AND FOrgId = '{purchaseOrg}'";
}
}
}
1.3 AfterF7Select:F7选择后的处理
AfterF7Select 在用户从F7弹窗中选择数据并确认后触发,可以实现选择后的自动填充。
csharp
/// <summary>
/// F7选择后处理
/// </summary>
public override void AfterF7Select(AfterF7SelectEventArgs e)
{
base.AfterF7Select(e);
if (e.FieldKey == "F_PAEZ_MaterialId")
{
int rowIndex = e.RowIndex;
if (rowIndex >= 0)
{
// 获取选中的物料数据包
DynamicObject material = e.ReturnData as DynamicObject;
if (material != null)
{
// 自动填充规格、型号、单价等
this.View.Model.SetValue("F_PAEZ_Specification", material["FSpecification"], rowIndex);
this.View.Model.SetValue("F_PAEZ_Model", material["FModel"], rowIndex);
this.View.Model.SetValue("F_PAEZ_Unit", material["FBaseUnit"], rowIndex);
this.View.Model.SetValue("F_PAEZ_Price", material["FPrice"], rowIndex);
this.View.Model.SetValue("F_PAEZ_TaxRate", material["FTaxRate"], rowIndex);
// 触发数量变更事件(重算金额)
this.View.InvokeFieldUpdateService("F_PAEZ_Qty", rowIndex);
// 刷新视图
this.View.UpdateView("F_PAEZ_Entry");
}
}
}
// 供应商选择后自动填充联系人、电话、地址
if (e.FieldKey == "F_PAEZ_SupplierId")
{
DynamicObject supplier = e.ReturnData as DynamicObject;
if (supplier != null)
{
this.View.Model.SetValue("F_PAEZ_Contact", supplier["FContact"]);
this.View.Model.SetValue("F_PAEZ_Phone", supplier["FPhone"]);
this.View.Model.SetValue("F_PAEZ_Address", supplier["FAddress"]);
this.View.UpdateView("F_PAEZ_Header");
}
}
}
1.4 F7字段的赋值与联动
通过插件给F7字段赋值,需要特别注意赋值方式------不能直接传字符串,需要通过构造数据包或使用特定方法。
csharp
/// <summary>
/// 通过代码给F7字段赋值
/// </summary>
public void SetF7FieldValue()
{
// 方式1:使用SetValue传入DynamicObject(推荐)
DynamicObject material = GetMaterialByNumber("MAT001");
this.View.Model.SetValue("F_PAEZ_MaterialId", material);
// 方式2:通过内码赋值(需要字段允许通过ID赋值)
this.View.Model.SetValue("F_PAEZ_MaterialId_Id", 123456789L);
// 方式3:通过编码赋值(需要字段允许通过编码赋值)
this.View.Model.SetValue("F_PAEZ_MaterialId_Number", "MAT001");
}
/// <summary>
/// 根据编码获取物料数据包
/// </summary>
private DynamicObject GetMaterialByNumber(string number)
{
string ksql = @"
SELECT FID, FNUMBER, FNAME, FSPECIFICATION, FPRICE
FROM T_BD_MATERIAL
WHERE FNUMBER = @Number AND FUseState = 'A'
";
DataTable dt = DBServiceHelper.ExecuteDataSet(
this.Context,
ksql,
new SqlParam[] { new SqlParam("@Number", SqlDbType.NVarChar, number) }
).Tables[0];
if (dt.Rows.Count > 0)
{
DataRow row = dt.Rows[0];
DynamicObject obj = new DynamicObject();
obj["Id"] = Convert.ToInt64(row["FID"]);
obj["Number"] = row["FNUMBER"].ToString();
obj["Name"] = row["FNAME"].ToString();
obj["FSpecification"] = row["FSPECIFICATION"].ToString();
obj["FPrice"] = Convert.ToDecimal(row["FPRICE"]);
return obj;
}
return null;
}
1.5 让F7字段支持模糊搜索
通过插件干预,可以让F7字段支持编码+名称的双重模糊搜索:
csharp
public override void BeforeF7Select(BeforeF7SelectEventArgs e)
{
base.BeforeF7Select(e);
if (e.FieldKey == "F_PAEZ_MaterialId")
{
// 获取用户输入的搜索关键字
// 注意:e.SearchText 是用户在F7搜索框中输入的内容
string searchText = e.SearchText;
if (!string.IsNullOrEmpty(searchText))
{
// 构建自定义过滤:编码或名称包含关键字
string filter = $@"
FUseState = 'A' AND (
FNUMBER LIKE '%{searchText}%'
OR FNAME LIKE '%{searchText}%'
)
";
e.Filter = filter;
}
else
{
e.Filter = "FUseState = 'A'";
}
}
}
第2章:动态表单弹窗交互
动态表单是金蝶云星空中轻量级的弹出页面。表单插件通过 ShowForm 方法打开动态表单,并通过 ClosedCallBack 接收返回数据,实现弹窗交互。
2.1 打开动态表单(ShowForm)
csharp
/// <summary>
/// 打开动态表单选择器
/// </summary>
public override void BarItemClick(BarItemClickEventArgs e)
{
base.BarItemClick(e);
if (e.BarItemKey == "F_PAEZ_SelectBOM")
{
// 构造打开参数
OpenParameter parameter = new OpenParameter();
parameter.FormId = "PAEZ_BOMSelector"; // 动态表单标识
parameter.PageId = "F_PAEZ_BOMSelectorPage"; // 页面标识
parameter.Put("SourceBillId", this.View.Model.GetPKValue());
parameter.Put("SourceBillNo", this.View.Model.GetValue("FBILLNO"));
// 方式1:无回调打开
this.View.ShowForm(parameter);
// 方式2:带回调打开(推荐)
this.View.ShowForm(parameter, OnBOMSelected);
}
}
/// <summary>
/// 回调方法:处理BOM选择结果
/// </summary>
private void OnBOMSelected(FormResult result)
{
if (result.IsLookup && result.ReturnData != null)
{
DynamicObject selectedBOM = result.ReturnData as DynamicObject;
if (selectedBOM != null)
{
// 获取选择的数据
string bomId = selectedBOM["FID"]?.ToString();
string bomName = selectedBOM["FNAME"]?.ToString();
// 将数据写入当前单据
this.View.Model.SetValue("F_PAEZ_BOMId", bomId);
this.View.Model.SetValue("F_PAEZ_BOMName", bomName);
// 根据BOM填充物料明细
FillBOMEntry(bomId);
this.View.ShowMessage($"已选择BOM:{bomName}");
}
}
else if (result.IsCancel)
{
this.View.ShowMessage("用户取消了选择");
}
}
2.2 动态表单中的参数传递
在动态表单插件中,通过 OpenParameter 接收父页面传递的参数:
csharp
// 动态表单插件 BOMSelector
public class BOMSelectorPlugIn : AbstractBillPlugIn
{
private string sourceBillId;
public override void Initialize()
{
base.Initialize();
// 接收父页面传递的参数
if (this.View.OpenParameter != null)
{
sourceBillId = this.View.OpenParameter.GetParameter("SourceBillId") as string;
string sourceBillNo = this.View.OpenParameter.GetParameter("SourceBillNo") as string;
// 根据参数加载数据
LoadBOMList(sourceBillId);
}
}
/// <summary>
/// 确认按钮:将选择的数据返回父页面
/// </summary>
public override void BarItemClick(BarItemClickEventArgs e)
{
base.BarItemClick(e);
if (e.BarItemKey == "F_PAEZ_Confirm")
{
string selectedBOMId = this.View.Model.GetValue("F_PAEZ_SelectedBOM")?.ToString();
if (string.IsNullOrEmpty(selectedBOMId))
{
this.View.ShowErrMessage("请先选择一个BOM!");
return;
}
// 构造返回数据
DynamicObject returnData = GetBOMData(selectedBOMId);
// 返回数据并关闭弹窗
this.View.ReturnData = returnData;
this.View.ReturnToParentWindow(true); // true表示确认,false表示取消
this.View.Close();
}
}
}
2.3 动态表单的返回值处理
父页面通过 ClosedCallBack 或回调委托接收返回值:
csharp
/// <summary>
/// 通过ClosedCallBack接收子页面返回值
/// </summary>
public override void ClosedCallBack(ClosedCallBackEventArgs e)
{
base.ClosedCallBack(e);
// e.ReturnData 是子页面返回的数据
if (e.ReturnData != null)
{
DynamicObject returnData = e.ReturnData as DynamicObject;
if (returnData != null)
{
string selectedValue = returnData["FID"]?.ToString();
this.View.Model.SetValue("F_PAEZ_SelectedValue", selectedValue);
this.View.UpdateView("F_PAEZ_Header");
}
}
}
2.4 确认对话框与消息弹窗
csharp
/// <summary>
/// 显示确认对话框
/// </summary>
public void ShowConfirmDialog()
{
// 方式1:使用 ShowConfirmDialog(同步)
bool confirmed = this.View.ShowConfirmDialog("确定要执行此操作吗?");
if (confirmed)
{
// 执行操作
this.View.ShowMessage("操作已执行");
}
// 方式2:使用 ShowConfirmDialog(带回调)
this.View.ShowConfirmDialog("确定要删除此记录吗?", (result) =>
{
if (result == MessageBoxResult.OK)
{
// 执行删除
}
else
{
this.View.ShowMessage("已取消删除");
}
});
}
/// <summary>
/// 显示消息弹窗
/// </summary>
public void ShowMessages()
{
// 普通消息
this.View.ShowMessage("操作成功完成!");
// 信息提示
this.View.ShowInformation("请先选择物料");
// 错误提示
this.View.ShowErrMessage("保存失败:数量不能为负数!");
// 警告提示
this.View.ShowWarnningMessage("金额超过预算,请确认!");
}
第3章:操作拦截与事务控制
表单插件提供了多层次的操作拦截能力,从操作前校验到操作后处理,从事务内到事务外,覆盖了业务操作的完整生命周期。
3.1 操作拦截的层次
| 事件方法 | 触发时机 | 事务状态 | 用途 |
|---|---|---|---|
BeforeDoOperation |
操作执行前 | 事务外 | 参数校验、权限检查,可取消 |
OnPrepareOperationServiceOption |
准备服务参数时 | 事务外 | 修改操作选项 |
BeforeExecuteOperationTransaction |
事务开始前 | 事务外 | 前置业务校验 |
AfterExecuteOperationTransaction |
事务提交后 | 事务外 | 后置处理(日志、消息) |
AfterDoOperation |
操作完成后 | 事务外 | 获取操作结果 |
3.2 BeforeDoOperation 与 AfterDoOperation
csharp
/// <summary>
/// 操作执行前(可拦截)
/// </summary>
public override void BeforeDoOperation(BeforeDoOperationEventArgs e)
{
base.BeforeDoOperation(e);
string operationId = e.Operation.FormOperation.OperationId;
// 拦截保存操作
if (operationId == "Save")
{
// 校验:单据体不能为空
int entryCount = this.View.Model.GetEntryRowCount("F_PAEZ_Entry");
if (entryCount == 0)
{
this.View.ShowErrMessage("请至少录入一行明细!");
e.Cancel = true;
return;
}
// 校验:金额不能为负数
for (int i = 0; i < entryCount; i++)
{
decimal amount = Convert.ToDecimal(this.View.Model.GetValue("F_PAEZ_Amount", i) ?? 0);
if (amount < 0)
{
this.View.ShowErrMessage($"第{i + 1}行金额不能为负数!");
e.Cancel = true;
return;
}
}
}
// 拦截提交操作
if (operationId == "Submit")
{
string status = this.View.Model.GetValue("F_PAEZ_Status")?.ToString();
if (status != "SAVED" && status != "DRAFT")
{
this.View.ShowErrMessage("只有草稿或已保存状态的单据才能提交!");
e.Cancel = true;
}
}
// 拦截审核操作
if (operationId == "Audit")
{
decimal totalAmount = Convert.ToDecimal(this.View.Model.GetValue("F_PAEZ_TotalAmount") ?? 0);
if (totalAmount > 1000000)
{
bool confirmed = this.View.ShowConfirmDialog("金额超过100万,确定要审核吗?");
if (!confirmed)
{
e.Cancel = true;
}
}
}
}
/// <summary>
/// 操作执行后
/// </summary>
public override void AfterDoOperation(AfterDoOperationEventArgs e)
{
base.AfterDoOperation(e);
string operationId = e.Operation.FormOperation.OperationId;
if (operationId == "Save" && e.OperationResult.IsSuccess)
{
// 保存成功:记录日志
string billNo = this.View.Model.GetValue("FBILLNO")?.ToString();
WriteOperationLog("SAVE", billNo, "保存成功");
// 更新状态
this.View.Model.SetValue("F_PAEZ_Status", "SAVED");
this.View.UpdateView("F_PAEZ_Header");
}
if (operationId == "Audit" && e.OperationResult.IsSuccess)
{
// 审核成功:发送消息通知
string billNo = this.View.Model.GetValue("FBILLNO")?.ToString();
SendNotification(billNo, "审核通过");
}
}
3.3 事务内操作:OnPrepareOperationServiceOption
OnPrepareOperationServiceOption 在操作服务选项被构建时触发,可以修改操作行为 ,如设置批量操作模式 、忽略某些校验等。
csharp
using Kingdee.BOS.App.Data;
/// <summary>
/// 准备操作服务选项(事务内)
/// </summary>
public override void OnPrepareOperationServiceOption(PrepareOperationServiceOptionArgs e)
{
base.OnPrepareOperationServiceOption(e);
// 获取操作选项
BOSOption option = e.Option;
// 场景1:批量操作模式(忽略部分校验)
if (e.Operation.FormOperation.OperationId == "Save")
{
// 启用批量保存模式,忽略部分字段校验
option.SetOption("BATCH_SAVE_MODE", true);
}
// 场景2:忽略必录校验(谨慎使用)
if (e.Operation.FormOperation.OperationId == "Submit")
{
// 忽略某些字段的必录校验
option.SetIgnoreRequiredCheckFields(new List<string> { "F_PAEZ_Remarks" });
}
// 场景3:设置事务超时时间
option.SetOption("TransactionTimeout", 300); // 300秒
}
3.4 操作选项的使用场景
csharp
/// <summary>
/// 通过操作选项控制操作行为
/// </summary>
public void CustomOperationOptions()
{
// 创建操作选项
BOSOption option = new BOSOption();
// 1. 启用批量模式(保存时忽略某些校验)
option.SetOption("BATCH_SAVE_MODE", true);
// 2. 忽略必录校验
option.SetIgnoreRequiredCheckFields(new List<string> { "F_PAEZ_Remarks" });
// 3. 设置忽略哪些字段的校验(值更新)
option.SetIgnoreFieldUpdateServices(new List<string> { "F_PAEZ_Qty" });
// 4. 设置事务超时
option.SetOption("TransactionTimeout", 300);
// 5. 设置操作是否记录日志
option.SetOption("IsLog", true);
// 6. 执行操作时传入选项
this.View.InvokeFormOperation("Save", option);
}
3.5 在事务中执行自定义操作
当需要在保存操作的事务中执行自定义数据库操作时,应使用 BeforeExecuteOperationTransaction 事件:
csharp
/// <summary>
/// 事务执行前(在事务内执行)
/// </summary>
public override void BeforeExecuteOperationTransaction(BeforeExecuteOperationTransactionArgs e)
{
base.BeforeExecuteOperationTransaction(e);
if (e.Operation.FormOperation.OperationId == "Save")
{
// 在事务内执行自定义更新
// 注意:此操作会与保存操作在同一事务中,如果失败,整个事务回滚
string ksql = @"
UPDATE T_BD_MATERIAL
SET F_LAST_USE_DATE = @UseDate
WHERE FID = @MaterialId
";
// 获取当前单据的物料列表
int entryCount = this.View.Model.GetEntryRowCount("F_PAEZ_Entry");
for (int i = 0; i < entryCount; i++)
{
string materialId = this.View.Model.GetValue("F_PAEZ_MaterialId", i)?.ToString();
if (!string.IsNullOrEmpty(materialId))
{
List<SqlParam> parameters = new List<SqlParam>
{
new SqlParam("@UseDate", SqlDbType.DateTime, DateTime.Now),
new SqlParam("@MaterialId", SqlDbType.BigInt, materialId)
};
DBServiceHelper.ExecuteNonQuery(this.Context, ksql, parameters.ToArray());
}
}
}
}
⚠️ 重要提醒 :在
BeforeExecuteOperationTransaction中执行的操作与当前单据的保存处于同一事务 中。如果这部分逻辑失败,整个操作(包括单据保存)都会回滚。适合需要强一致性的业务场景。
第4章:性能优化技巧
表单插件虽然功能强大,但如果不注意性能,很容易成为系统的瓶颈。本章将介绍表单插件开发中常见的性能优化技巧。
4.1 减少不必要的数据库查询
csharp
/// <summary>
/// 错误示例:在循环中查询数据库(N+1问题)
/// </summary>
public void BadExample_QueryInLoop()
{
int entryCount = this.View.Model.GetEntryRowCount("F_PAEZ_Entry");
for (int i = 0; i < entryCount; i++)
{
string materialId = this.View.Model.GetValue("F_PAEZ_MaterialId", i)?.ToString();
// ❌ 每次循环都查询数据库
DynamicObject material = QueryServiceHelper.QueryOne("T_BD_MATERIAL", "*",
new QFilter[] { new QFilter("FID", QFilter.Operator.Equals, materialId) });
// 处理物料数据...
}
}
/// <summary>
/// 正确示例:批量查询(1次查询)
/// </summary>
public void GoodExample_BatchQuery()
{
int entryCount = this.View.Model.GetEntryRowCount("F_PAEZ_Entry");
// 1. 收集所有物料ID
List<string> materialIds = new List<string>();
for (int i = 0; i < entryCount; i++)
{
string id = this.View.Model.GetValue("F_PAEZ_MaterialId", i)?.ToString();
if (!string.IsNullOrEmpty(id)) materialIds.Add(id);
}
if (materialIds.Count == 0) return;
// 2. 一次查询所有物料
string ksql = $@"
SELECT FID, FNUMBER, FNAME, FSPECIFICATION, FPRICE
FROM T_BD_MATERIAL
WHERE FID IN ({string.Join(",", materialIds)})
";
DataTable dt = DBServiceHelper.ExecuteDataSet(this.Context, ksql).Tables[0];
// 3. 构建字典便于快速查找
Dictionary<string, DataRow> materialDict = new Dictionary<string, DataRow>();
foreach (DataRow row in dt.Rows)
{
materialDict[row["FID"].ToString()] = row;
}
// 4. 使用字典数据
for (int i = 0; i < entryCount; i++)
{
string materialId = this.View.Model.GetValue("F_PAEZ_MaterialId", i)?.ToString();
if (materialDict.TryGetValue(materialId, out DataRow materialRow))
{
this.View.Model.SetValue("F_PAEZ_Price", Convert.ToDecimal(materialRow["FPRICE"]), i);
}
}
}
4.2 合理使用缓存
对于频繁访问且不常变化的数据(如基础资料),可以使用缓存:
csharp
/// <summary>
/// 使用缓存提高查询效率
/// </summary>
private DynamicObject GetCachedMaterial(string materialId)
{
if (string.IsNullOrEmpty(materialId)) return null;
// 构建缓存Key
string cacheKey = $"MaterialCache_{materialId}";
// 尝试从缓存获取
DynamicObject cached = CacheManager.GetCacheObject<DynamicObject>(cacheKey);
if (cached != null)
{
return cached;
}
// 缓存未命中,查询数据库
DynamicObject material = QueryServiceHelper.QueryOne(
"T_BD_MATERIAL",
"FID, FNUMBER, FNAME, FSPECIFICATION, FPRICE",
new QFilter[] { new QFilter("FID", QFilter.Operator.Equals, materialId) }
);
if (material != null)
{
// 写入缓存(有效期10分钟)
CacheManager.SetCacheObject(cacheKey, material, 10 * 60);
}
return material;
}
4.3 延迟加载与按需加载
csharp
/// <summary>
/// 按需加载:只有特定条件下才加载数据
/// </summary>
public override void AfterBindData(EventArgs e)
{
base.AfterBindData(e);
// 只在编辑模式下加载扩展数据
if (this.View.IsCardView && !this.View.IsNew)
{
string status = this.View.Model.GetValue("F_PAEZ_Status")?.ToString();
// 只有已审核的单据才加载审批历史
if (status == "AUDITED")
{
LoadApprovalHistory();
}
}
}
private void LoadApprovalHistory()
{
// 异步或延迟加载审批历史
// 避免阻塞主界面渲染
System.Threading.Tasks.Task.Run(() =>
{
string billId = this.View.Model.GetPKValue().ToString();
string ksql = @"
SELECT F_OPERATOR, F_OPERATE_TIME, F_ACTION
FROM T_PAEZ_APPROVAL_LOG
WHERE F_BILL_ID = @BillId
ORDER BY F_OPERATE_TIME DESC
";
// 执行查询并更新界面...
});
}
4.4 避免在AfterBindData中大量修改数据
AfterBindData 每次数据绑定都会触发,如果在此事件中大量修改数据,会导致性能问题。
csharp
/// <summary>
/// 错误示例:在AfterBindData中大量修改数据
/// </summary>
public override void AfterBindData(EventArgs e)
{
base.AfterBindData(e);
// ❌ 每次绑定都重新计算大量数据
RecalculateAllAmounts();
}
/// <summary>
/// 正确示例:使用标志避免重复计算
/// </summary>
private bool _isCalculated = false;
public override void AfterBindData(EventArgs e)
{
base.AfterBindData(e);
// 只在第一次绑定时计算
if (!_isCalculated)
{
RecalculateAllAmounts();
_isCalculated = true;
}
}
第5章:综合实战示例
5.1 场景描述
开发一个"采购申请单"的高级扩展插件,包含以下功能:
- F7物料选择时,根据供应商动态过滤;
- 物料选择后自动填充规格、单价、税率;
- 点击"选择BOM"按钮弹出动态表单选择BOM,自动填充物料明细;
- 保存前校验:物料不重复、金额非负、供应商与物料匹配;
- 审核前校验:金额超过50万需要二次确认;
- 审核通过后自动更新物料最后使用日期;
- 所有操作记录审计日志。
5.2 完整代码实现
csharp
using Kingdee.BOS;
using Kingdee.BOS.App.Data;
using Kingdee.BOS.Core.Bill.PlugIn;
using Kingdee.BOS.Core.DynamicForm.PlugIn.Args;
using Kingdee.BOS.Orm.DataEntity;
using Kingdee.BOS.ServiceHelper;
using Kingdee.BOS.Util;
using System;
using System.Collections.Generic;
using System.Data;
namespace Demo.K3.SCM.Purchase.Business.PlugIn
{
[Description("采购申请单高级扩展插件")]
[HotUpdate]
public class PurchaseReqAdvancedPlugIn : AbstractBillPlugIn
{
#region 常量定义
private const string KEY_ENTRY = "F_PAEZ_Entry";
private const string KEY_MATERIAL = "F_PAEZ_MaterialId";
private const string KEY_SUPPLIER = "F_PAEZ_SupplierId";
private const string KEY_QTY = "F_PAEZ_Qty";
private const string KEY_PRICE = "F_PAEZ_Price";
private const string KEY_AMOUNT = "F_PAEZ_Amount";
private const string KEY_TAX_RATE = "F_PAEZ_TaxRate";
private const string KEY_TAXED_AMOUNT = "F_PAEZ_TaxedAmount";
private const string KEY_TOTAL_AMOUNT = "F_PAEZ_TotalAmount";
private const string KEY_SPECIFICATION = "F_PAEZ_Specification";
private const string KEY_STATUS = "F_PAEZ_Status";
#endregion
#region F7控制
/// <summary>
/// F7打开前:根据供应商动态过滤物料
/// </summary>
public override void BeforeF7Select(BeforeF7SelectEventArgs e)
{
base.BeforeF7Select(e);
if (e.FieldKey == KEY_MATERIAL)
{
string supplierId = this.View.Model.GetValue(KEY_SUPPLIER)?.ToString();
if (!string.IsNullOrEmpty(supplierId))
{
// 只显示该供应商可提供的物料
e.Filter = $"FUseState = 'A' AND FDefaultSupplierId = '{supplierId}'";
}
else
{
e.Filter = "FUseState = 'A'";
}
}
}
/// <summary>
/// F7选择后:自动填充物料信息
/// </summary>
public override void AfterF7Select(AfterF7SelectEventArgs e)
{
base.AfterF7Select(e);
if (e.FieldKey == KEY_MATERIAL && e.RowIndex >= 0)
{
DynamicObject material = e.ReturnData as DynamicObject;
if (material != null)
{
int row = e.RowIndex;
this.View.Model.SetValue(KEY_SPECIFICATION, material["FSpecification"], row);
this.View.Model.SetValue(KEY_PRICE, material["FPrice"], row);
this.View.Model.SetValue(KEY_TAX_RATE, material["FTaxRate"] ?? 0.13m, row);
// 重算金额
RecalculateRowAmount(row);
}
}
}
#endregion
#region 字段联动
/// <summary>
/// 字段值变更
/// </summary>
public override void DataChanged(DataChangedEventArgs e)
{
base.DataChanged(e);
if ((e.Field.Key == KEY_QTY || e.Field.Key == KEY_PRICE || e.Field.Key == KEY_TAX_RATE)
&& e.RowIndex >= 0)
{
RecalculateRowAmount(e.RowIndex);
}
}
/// <summary>
/// 重新计算行金额
/// </summary>
private void RecalculateRowAmount(int rowIndex)
{
decimal qty = Convert.ToDecimal(this.View.Model.GetValue(KEY_QTY, rowIndex) ?? 0);
decimal price = Convert.ToDecimal(this.View.Model.GetValue(KEY_PRICE, rowIndex) ?? 0);
decimal taxRate = Convert.ToDecimal(this.View.Model.GetValue(KEY_TAX_RATE, rowIndex) ?? 0.13m);
decimal amount = qty * price;
decimal taxedAmount = amount * (1 + taxRate);
this.View.Model.SetValue(KEY_AMOUNT, amount, rowIndex);
this.View.Model.SetValue(KEY_TAXED_AMOUNT, taxedAmount, rowIndex);
// 更新总金额
UpdateTotalAmount();
this.View.UpdateView(KEY_ENTRY);
}
/// <summary>
/// 更新单据头总金额
/// </summary>
private void UpdateTotalAmount()
{
decimal total = 0;
int count = this.View.Model.GetEntryRowCount(KEY_ENTRY);
for (int i = 0; i < count; i++)
{
decimal amount = Convert.ToDecimal(this.View.Model.GetValue(KEY_TAXED_AMOUNT, i) ?? 0);
total += amount;
}
this.View.Model.SetValue(KEY_TOTAL_AMOUNT, total);
}
#endregion
#region BOM选择
/// <summary>
/// 自定义按钮:选择BOM
/// </summary>
public override void BarItemClick(BarItemClickEventArgs e)
{
base.BarItemClick(e);
if (e.BarItemKey == "F_PAEZ_SelectBOM")
{
// 检查供应商是否已选
string supplierId = this.View.Model.GetValue(KEY_SUPPLIER)?.ToString();
if (string.IsNullOrEmpty(supplierId))
{
this.View.ShowErrMessage("请先选择供应商!");
return;
}
OpenParameter parameter = new OpenParameter();
parameter.FormId = "PAEZ_BOMSelector";
parameter.PageId = "F_PAEZ_BOMSelectorPage";
parameter.Put("SupplierId", supplierId);
parameter.Put("SourceBillId", this.View.Model.GetPKValue());
this.View.ShowForm(parameter, OnBOMSelected);
}
}
/// <summary>
/// BOM选择回调
/// </summary>
private void OnBOMSelected(FormResult result)
{
if (result.IsLookup && result.ReturnData != null)
{
DynamicObject selectedBOM = result.ReturnData as DynamicObject;
if (selectedBOM != null)
{
FillBOMEntry(selectedBOM);
this.View.ShowMessage($"已选择BOM:{selectedBOM["FNAME"]}");
}
}
}
/// <summary>
/// 根据BOM填充物料明细
/// </summary>
private void FillBOMEntry(DynamicObject bom)
{
string bomId = bom["FID"]?.ToString();
if (string.IsNullOrEmpty(bomId)) return;
// 查询BOM明细
string ksql = @"
SELECT F_MATERIAL_ID, F_QTY, F_PRICE
FROM T_PAEZ_BOM_DETAIL
WHERE F_BOM_ID = @BomId
";
DataTable dt = DBServiceHelper.ExecuteDataSet(
this.Context,
ksql,
new SqlParam[] { new SqlParam("@BomId", SqlDbType.NVarChar, bomId) }
).Tables[0];
// 清空现有明细
int currentCount = this.View.Model.GetEntryRowCount(KEY_ENTRY);
for (int i = currentCount - 1; i >= 0; i--)
{
this.View.Model.DeleteEntryRow(KEY_ENTRY, i);
}
// 插入BOM明细
foreach (DataRow row in dt.Rows)
{
int newRow = this.View.Model.CreateNewEntryRow(KEY_ENTRY);
this.View.Model.SetValue(KEY_MATERIAL, row["F_MATERIAL_ID"].ToString(), newRow);
this.View.Model.SetValue(KEY_QTY, Convert.ToDecimal(row["F_QTY"]), newRow);
this.View.Model.SetValue(KEY_PRICE, Convert.ToDecimal(row["F_PRICE"]), newRow);
RecalculateRowAmount(newRow);
}
this.View.UpdateView(KEY_ENTRY);
}
#endregion
#region 操作拦截与事务控制
/// <summary>
/// 保存前校验
/// </summary>
public override void BeforeSave(BeforeSaveEventArgs e)
{
base.BeforeSave(e);
int entryCount = this.View.Model.GetEntryRowCount(KEY_ENTRY);
// 1. 校验:明细不能为空
if (entryCount == 0)
{
this.View.ShowErrMessage("请至少录入一行物料明细!");
e.Cancel = true;
return;
}
// 2. 校验:物料不重复
HashSet<string> materialSet = new HashSet<string>();
for (int i = 0; i < entryCount; i++)
{
string materialId = this.View.Model.GetValue(KEY_MATERIAL, i)?.ToString();
if (!string.IsNullOrEmpty(materialId))
{
if (materialSet.Contains(materialId))
{
this.View.ShowErrMessage($"第{i + 1}行物料重复!");
e.Cancel = true;
return;
}
materialSet.Add(materialId);
}
}
// 3. 校验:供应商与物料匹配
string supplierId = this.View.Model.GetValue(KEY_SUPPLIER)?.ToString();
if (!string.IsNullOrEmpty(supplierId))
{
for (int i = 0; i < entryCount; i++)
{
string materialId = this.View.Model.GetValue(KEY_MATERIAL, i)?.ToString();
if (!string.IsNullOrEmpty(materialId) && !IsMaterialMatchSupplier(materialId, supplierId))
{
this.View.ShowErrMessage($"第{i + 1}行物料与该供应商不匹配!");
e.Cancel = true;
return;
}
}
}
}
/// <summary>
/// 检查物料与供应商是否匹配
/// </summary>
private bool IsMaterialMatchSupplier(string materialId, string supplierId)
{
string ksql = @"
SELECT COUNT(1)
FROM T_BD_MATERIAL
WHERE FID = @MaterialId AND FDefaultSupplierId = @SupplierId
";
DataTable dt = DBServiceHelper.ExecuteDataSet(
this.Context,
ksql,
new SqlParam[] {
new SqlParam("@MaterialId", SqlDbType.BigInt, materialId),
new SqlParam("@SupplierId", SqlDbType.BigInt, supplierId)
}
).Tables[0];
return Convert.ToInt32(dt.Rows[0][0]) > 0;
}
/// <summary>
/// 事务内操作:更新物料最后使用日期
/// </summary>
public override void BeforeExecuteOperationTransaction(
BeforeExecuteOperationTransactionArgs e)
{
base.BeforeExecuteOperationTransaction(e);
if (e.Operation.FormOperation.OperationId == "Audit")
{
// 审核通过后,在事务内更新物料最后使用日期
int entryCount = this.View.Model.GetEntryRowCount(KEY_ENTRY);
for (int i = 0; i < entryCount; i++)
{
string materialId = this.View.Model.GetValue(KEY_MATERIAL, i)?.ToString();
if (!string.IsNullOrEmpty(materialId))
{
string ksql = @"
UPDATE T_BD_MATERIAL
SET F_LAST_USE_DATE = @UseDate
WHERE FID = @MaterialId
";
List<SqlParam> parameters = new List<SqlParam>
{
new SqlParam("@UseDate", SqlDbType.DateTime, DateTime.Now),
new SqlParam("@MaterialId", SqlDbType.BigInt, materialId)
};
DBServiceHelper.ExecuteNonQuery(this.Context, ksql, parameters.ToArray());
}
}
}
}
#endregion
#region 审计日志
/// <summary>
/// 操作后:记录审计日志
/// </summary>
public override void AfterDoOperation(AfterDoOperationEventArgs e)
{
base.AfterDoOperation(e);
if (e.OperationResult.IsSuccess)
{
string billNo = this.View.Model.GetValue("FBILLNO")?.ToString();
string operationId = e.Operation.FormOperation.OperationId;
string action = operationId switch
{
"Save" => "保存",
"Submit" => "提交",
"Audit" => "审核",
"UnAudit" => "反审核",
"Delete" => "删除",
_ => operationId
};
WriteAuditLog(billNo, action);
}
}
/// <summary>
/// 写入审计日志
/// </summary>
private void WriteAuditLog(string billNo, string action)
{
string ksql = @"
INSERT INTO T_PAEZ_AUDIT_LOG
(F_BILL_NO, F_ACTION, F_OPERATOR, F_OPERATE_TIME, F_REMARK)
VALUES
(@BillNo, @Action, @Operator, @OperateTime, @Remark)
";
List<SqlParam> parameters = new List<SqlParam>
{
new SqlParam("@BillNo", SqlDbType.NVarChar, billNo),
new SqlParam("@Action", SqlDbType.NVarChar, action),
new SqlParam("@Operator", SqlDbType.NVarChar, this.Context.UserName),
new SqlParam("@OperateTime", SqlDbType.DateTime, DateTime.Now),
new SqlParam("@Remark", SqlDbType.NVarChar, $"操作人:{this.Context.UserName}")
};
DBServiceHelper.ExecuteNonQuery(this.Context, ksql, parameters.ToArray());
}
#endregion
#region 确认对话框
/// <summary>
/// 审核前二次确认
/// </summary>
public override void BeforeDoOperation(BeforeDoOperationEventArgs e)
{
base.BeforeDoOperation(e);
if (e.Operation.FormOperation.OperationId == "Audit")
{
decimal totalAmount = Convert.ToDecimal(
this.View.Model.GetValue(KEY_TOTAL_AMOUNT) ?? 0
);
if (totalAmount > 500000)
{
bool confirmed = this.View.ShowConfirmDialog(
$"金额 {totalAmount:N2} 超过50万,确定要审核吗?",
MessageBoxType.Question
);
if (!confirmed)
{
e.Cancel = true;
}
}
}
}
#endregion
}
}
本篇小结
本篇完成了金蝶云星空企业版表单插件开发的高级应用讲解:
| 章节 | 核心收获 |
|---|---|
| 第1章 | 掌握F7字段的三层数据结构、BeforeF7Select动态过滤(供应商/组织/排除已选)、AfterF7Select自动填充、F7字段赋值方法 |
| 第2章 | 掌握动态表单的打开(OpenParameter)、参数传递、回调处理(ClosedCallBack)、确认对话框与消息弹窗 |
| 第3章 | 掌握操作拦截的多层架构(BeforeDoOperation/OnPrepareOperationServiceOption/BeforeExecuteOperationTransaction/AfterDoOperation)、事务内自定义操作 |
| 第4章 | 掌握性能优化技巧:批量查询替代循环查询、缓存使用、延迟加载、避免AfterBindData中大量修改数据 |
| 第5章 | 综合实战:F7过滤+自动填充+BOM选择+多重校验+事务操作+审计日志的完整实现 |
下一篇预告 :第三篇将深入讲解表单插件的数据转换与下推------从目标单据数据获取、转换规则插件开发,到反写规则的完整实现,并结合WebAPI调用、外部系统集成等企业级场景,打造一个完整的跨单据业务流程。