ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

U9客户化开发入门:UBF路径、分层规范与发布避坑

U9客户化开发入门:UBF路径、分层规范与发布避坑

简介:用友U9客户化开发入门课程讲解资料,面向用友伙伴及准备进入U9客开领域的初中级开发者,围绕UBF路径配置、解决方案目录层次、命名规范与档案开发需求等基础环节,帮助读者建立从项目组织到组件落地的整体认知。包内为1个docx文档,压缩包约817KB,以课程知识点整理为主,便于按章节查阅并配合视频教学对照使用。内容覆盖解决方案、项目、组件的层次划分,配件与BE表命名规则,科目档案字段设计,以及BE Code与Model对比、组件/类实体/属性关系查询、UI Code与Model对比、UIForm/Part/Page关系查询和同步枚举脚本等实操主题,并附相关SQL查询示例,可辅助理解元数据对应关系与常见开发路径。目前已有3029人学习,适合作为U9客开入门阶段的系统梳理材料,也可供开发前查漏补缺时参考。

1. 一个“加个科目档案”的需求,为什么第一天就卡住

某开发者接了个 U9 客户化的小活:给考核模块加一个科目档案,字段不多,一个编码、一个名称、一个枚举类型、一个状态加备注。他以为在 UBF Studio 里拖几个控件、生成一下代码就能交差,结果第一天就卡在三件事上:UBF 路径没配对,解决方案建成了“一个大项目全塞进去”,生成出来的 DLL 发布后菜单里根本找不到页面。U9 客户化开发入门这件事,难点从来不是写多少 C# 代码,而是先把工程骨架、命名规范和发布链路理顺。这份资料适合刚接触 U9 客开的伙伴,也适合已经能改代码、但总在发布和参照上翻车的人。下面按实际拆项目的顺序,把路径、分层、档案、单据、发布和避坑一次讲透。

2. 工程骨架先立规矩:UBF 路径、解决方案分层与命名规范

U9 客户化开发最怕一上来就写代码。路径没配好,Studio 找不到元数据;解决方案层次乱了,一个模块生成七八个 DLL,后面打补丁包时连自己都说不清哪个文件该覆盖。这一章先把三个基础动作做对:UBF 路径配置、解决方案/项目/组件的分层、命名规范,最后补一条用 SQL 反查组件与类关系的路子,方便你随时核对设计器里看到的东西和数据库里存的东西是不是一回事。

2.1 UBF 路径配置:先让 Studio 认到产品目录

UBF 路径配置是客开的第一步,视频里放在 160 节,不是没有道理。常见做法是打开 UBF Studio 后,先在选项里把产品安装目录、元数据目录和 UILib 目录指到本机实际路径,再新建解决方案。路径配错的典型表现是:设计器能打开,但新建组件时模板列表是空的;或者编译能过,发布时提示找不到U9.VOB.Product.UBF下的某个 dll。

我一般按下面顺序检查:

  1. 确认产品目录指向U9.VOB.Product.UBF的上一级,而不是直接指到UBFStudioBuildupDesigner.exe所在目录。
  2. 元数据路径指向产品自带的 Metadata 目录,客开产生的元数据不要混进去。
  3. UILib 路径单独记录,后面buildup.xml里要填同一个值。
  4. 路径里不要带中文、空格和括号,U9 的工具链对这类字符兼容性一般。

可以用一个简单的批处理确认目录是否存在,避免每次靠肉眼找:

@echo off set UBF_HOME=D:\U9\U9.VOB.Product.UBF if not exist "%UBF_HOME%\UBFStudioBuildupDesigner.exe" ( echo UBF 工具目录不存在,请检查路径 exit /b 1 ) if not exist "%UBF_HOME%\Runtime\environment.xml" ( echo 运行时配置文件缺失 exit /b 1 ) echo UBF 路径检查通过

这段脚本只做两件事:确认装配发布器和运行时配置文件在不在。UBF_HOME换成你自己的产品目录,别照抄盘符。参数上唯一需要注意的是,UBFStudioBuildupDesigner.exe通常不在产品根目录,而是在U9.VOB.Product.UBF下,路径指错一层,后面发布一定报“找不到入口”。

提示:路径配置改完后重启一次 Studio,有些缓存不重启不生效。

2.2 解决方案/项目/组件的分层:一个模块只出几个 DLL

解决方案、项目、组件这三层关系,资料里给了很明确的规则,核心就一句话:同一模块,按“BE 一个项目、BP/SV 一个项目、UI 按档案/单据/列表/参照分开”来建。很多人图省事,一个解决方案里塞十几个项目,每个实体一个项目,最后编译出来一堆 DLL,发布时漏一个就报类型加载失败。

我把规则整理成一张表,建项目时直接对着看:

内容放在哪个项目生成物说明
同一模块所有 BE一个 BE 项目一个 BE 的 DLL枚举也放在这个 BE 组件内
同一模块所有 BP、SV一个 BP/SV 项目一个 BP/SV 的 DLL不要按单据拆项目
一个档案或单据的 UI 及其查找参照 UI一个 UI 项目一个 UI 的 DLL一个档案/单据对应一个 UI DLL
同一模块所有列表 UI一个列表 UI 项目一个列表 DLL列表和卡片分开
同一模块所有参照 UI一个参照 UI 项目一个参照 DLL参照统一放一起

目录层次上,我习惯按“解决方案 >> 模块 >> 项目 >> 组件”落盘,模块名用两位缩写,比如考核模块用EX。下面是一个可以照着建的目录结构:

CustSolution/ ├── EX/ │ ├── EXBE/ # 所有 BE、枚举 │ │ ├── Entity/ │ │ └── Enum/ │ ├── EXBP/ # 所有 BP、SV │ ├── EXUI/ # 档案、单据的卡片 UI │ ├── EXUIList/ # 列表 UI │ └── EXUIRef/ # 参照 UI └── Common/ └── CommonBE/ # 跨模块公共实体

这样分层的好处很实际:打补丁包时,一个模块的 BE 只覆盖一个 DLL,UI 按页面覆盖,出问题回退也快。反过来,如果每个实体一个项目,光记 DLL 名字就能把人逼疯。

2.3 命名规范:配件名和表名一次定死

命名规范不是形式主义,U9 的元数据、DLL、数据库表之间靠名字关联,名字起错,后面用 SQL 反查时对不上,发布也容易冲突。资料里给的格式是:

  • 配件名:UFIDA.U9.Cust.XM(地区).HXPP(公司).EX(模块)
  • BE 表名:Cust_XM(地区)_HXPP(公司)_EX(模块)_XXXX

我一般把地区、公司、模块做成固定段,后面再跟实体名。比如考核模块的科目表,写成Cust_XM_HXPP_EX_Subject;配件名写成UFIDA.U9.Cust.XM.HXPP.EX。枚举放在 BE 组件里,枚举类型名跟实体走,不要另起一套前缀。

有几个细节容易忽略:

  1. 配件名里的公司段不要用中文拼音全拼,用 4 位缩写,后面查 DLL 时好认。
  2. 表名总长别超过数据库标识符限制,实体名太长就缩写,但缩写规则要在模块内统一。
  3. 同一模块的 BE 项目名、BP 项目名、UI 项目名保持同一前缀,发布时按前缀筛选。
  4. 不要用DocType给实体字段命名,U9 单据基类里有同名成员,后面重载时容易撞名。

2.4 用 SQL 反查组件、类、属性关系

设计器里看到的是模型,数据库里存的是元数据,两边对不上时,最直接的办法是查UBF_MD_Component、UBF_MD_Class、UBF_MD_Attribute三张主表和对应的翻译表。资料里给的这条 SQL 可以直接用,我把它整理成带注释的版本:

SELECT a.Name AS ComponentName, at.DisplayName AS ComponentName_CN, a.AssemblyName, a.Kind, b.Name AS ClassName, bt.DisplayName AS ClassName_CN, b.FullName, b.Discriminator, c.Name AS AttributeName, ct.DisplayName AS AttributeName_CN FROM UBF_MD_Component a LEFT OUTER JOIN UBF_MD_Component_Trl at ON a.Local_ID = at.Local_ID AND at.SysMLFlag = 'zh-cn' LEFT OUTER JOIN UBF_MD_Class b ON a.ID = b.MD_Component_ID LEFT OUTER JOIN UBF_MD_Class_Trl bt ON b.Local_ID = bt.Local_ID AND bt.SysMLFlag = 'zh-cn' LEFT OUTER JOIN UBF_MD_Attribute c ON b.ID = c.MD_Class_ID LEFT OUTER JOIN UBF_MD_Attribute_Trl ct ON c.Local_ID = ct.Local_ID AND ct.SysMLFlag = 'zh-cn' WHERE bt.DisplayName = '科目';

这条 SQL 的入口是bt.DisplayName,也就是类的中文显示名。你把自己的实体显示名替换掉“科目”,就能查到它属于哪个组件、程序集名是什么、下面挂了哪些属性。a.Kind用来区分组件类型,b.Discriminator在继承场景下很有用,能看出这个类是实体、单据还是单据类型。实际排查时,如果发布后报“找不到类型”,先跑这条 SQL 看AssemblyName和你实际部署的 DLL 名是否一致,比翻日志快。

3. 档案开发:以科目为例把 BE 和 UI 串起来

档案开发是 U9 客开里最典型的闭环:建实体、定字段、生成 BE、做 UI、发布。科目这个例子不大,但把业务主键、枚举、扩展字段、参照和发布都串了一遍。下面按字段设计、BE 与 Model 对照、UIForm/Part/Page 关系、枚举同步四块来讲,每一步都落到能直接抄的 SQL 或代码。

3.1 科目档案的字段设计与业务主键

科目档案的字段不多,但业务主键和枚举值要一次定清楚。资料里给的结构是:组织、编码、名称、科目类型、状态、备注、扩展字段。我把它们整理成表:

字段名显示名称类型约束说明
Org组织组织机构业务主键,不可空默认取当前登录组织
Code编码字符串业务主键,不可空,长 50组织内唯一
Name科目名称字符串不可空,长 200显示用
SubjectType科目类型枚举枚举值见下辅料、主要材料、产值、费用、对账差异、营销可比利润、调节项、其他收入、税后利润、考核税前利润
Effective状态状态枚举可空启用/停用
Memo备注字符串长 500可空
DescFlexField扩展字段实体扩展字段集合可空描述性弹性域

业务主键选Org + Code,意味着同一个组织下编码不能重复,不同组织可以重复。这个设计在 U9 里很常见,但要注意:BE 上赋默认值时,Org必须从上下文取,不能让用户手填,否则业务主键校验会很难受。SubjectType的枚举值不要直接写死在 UI 下拉框里,走 BE 组件里的枚举定义,后面同步脚本和参照都依赖它。

3.2 BE Code 与 Model 对照:生成物和手写扩展分开

BE 项目生成后,Entity目录下是实体代码,Model目录下是设计器生成的模型代码。常见误区是直接改生成文件,下次重新生成就被覆盖。正确做法是在实体对应的Extend分部类里写扩展逻辑。比如给科目赋组织默认值,在 BE 的OnSetDefaultValue里加:

protected override void OnSetDefaultValue() { base.OnSetDefaultValue(); // 新建时组织为空,默认取当前登录组织 if (this.Org == null) { this.Org = Context.LoginOrg; } }

Context.LoginOrg是当前登录组织对象,不是组织 ID,赋值时不要混。OnSetDefaultValue只在新建默认值时触发,修改历史单据不会重跑,所以不要在这里做“每次保存都修正组织”的逻辑。BE Code 和 Model 的对照关系,我一般用两个办法核对:一是看Model里属性的DisplayName和数据库翻译表是否一致;二是用 2.4 节那条 SQL,按类显示名反查属性列表,确认手写代码里引用的属性名没有拼错。

3.3 UIForm、Part、Page 的关系查询

UI 这一层最容易迷糊的是 UIForm、Part、Page 三个概念。简单说,UIForm 是页面表单,Part 是页面里的区域装配,Page 是页面定义,三者通过 UID 和 URI 关联。发布页面、配导航、查 FormID 时,都要先定位到具体 UIForm。资料里给的查询 SQL 可以改成按显示名过滤:

SELECT at.DisplayName AS UIFormDisplayName, a.name AS UIFormName, c.name AS PageName, a.uid AS [UID(FormID)], b.PageCode, a.uri AS [PartID(UIFormURI)], c.uri AS PageURI, a.AssemblyName, a.ClassName, a.Width, a.Height, dt.name AS ApplicationName FROM ubf_md_uiform a LEFT OUTER JOIN ubf_assemble_part b ON a.uid = b.component LEFT OUTER JOIN ubf_assemble_page c ON b.page = c.id LEFT OUTER JOIN base_application_trl dt ON c.application = dt.id AND dt.sysmlflag = 'zh-cn' LEFT OUTER JOIN ubf_md_uiform_trl at ON a.id = at.id AND at.sysmlflag = 'zh-cn' WHERE at.DisplayName = '科目';

at.DisplayName换成你的页面显示名,查出来的UID(FormID)就是查找代码里要填的 FormID,PartID(UIFormURI)和PageURI在配导航时会用到。ApplicationName用来确认页面挂在哪个应用下,发布菜单时如果找不到,多半是应用名对不上。实际使用时,我习惯把这条 SQL 和 3.4 节的枚举查询放在同一个查询窗口,改完 UI 先跑一遍,确认元数据已经进库,再去做发布。

3.4 枚举同步脚本的适用边界

枚举同步脚本是客开里容易用错的东西。资料里说得很清楚:以补丁包方式更新/安装,不需要跑下面的脚本,因为 BE 的 bulk 会处理;只有在手工维护元数据、或者补丁包没覆盖到时才用。脚本的逻辑是先清空四张扩展枚举表,再从UBF_MD_Class和UBF_MD_Attribute里重建。我把它整理成可读版本,账号和日期常量换成占位:

-- 仅在非补丁包方式更新时手工执行 DELETE FROM ubf_sys_extenumtype; DELETE FROM ubf_sys_extenumtype_trl; DELETE FROM ubf_sys_extenumvalue; DELETE FROM ubf_sys_extenumvalue_trl; GO INSERT INTO ubf_sys_extenumtype SELECT local_id AS id, '20240101', 'u9demo', '20240101', 'u9demo', 0, fullname AS code, isextend, id AS uid, 0 AS IsRefView FROM ubf_md_class WHERE classtype = 3; INSERT INTO ubf_sys_extenumtype_trl SELECT a.local_id AS id, 'zh-CN', b.displayname AS name, b.Description FROM ubf_md_class a LEFT JOIN ubf_RES_resourcevalue b ON CAST(a.ID AS nvarchar(255)) = b.resourcename WHERE a.classtype = 3; INSERT INTO ubf_sys_extenumvalue SELECT a.local_id AS id, '20240101', 'u9demo', '20240101', 'u9demo', 0, a.Local_Class_ID AS ExtEnumType, a.name AS code, a.defaultvalue AS evalue, issystem, md_class_id AS ExtEnumTypeUID FROM ubf_md_attribute a INNER JOIN ubf_md_class b ON a.md_class_id = b.id WHERE b.classtype = 3; INSERT INTO ubf_sys_extenumvalue_trl SELECT a.local_id AS id, 'zh-CN', c.displayname AS name FROM ubf_md_attribute a INNER JOIN ubf_md_class b ON a.md_class_id = b.id LEFT JOIN ubf_RES_resourcevalue c ON CAST(a.ID AS nvarchar(255)) = c.resourcename WHERE b.classtype = 3; GO

这段脚本的参数点在于classtype = 3,表示只处理枚举类;'20240101'和'u9demo'是创建人/修改人占位,实际执行时按项目规范替换。ubf_sys_extenumtype里的code取的是类的fullname,所以枚举类的全名必须符合命名规范,否则同步出来的编码会很乱。跑完脚本后,用下面这条 SQL 验证枚举值和类型是否挂上:

SELECT a.Code, at.NAME, a.EValue, c.UID AS EnumTypeUID FROM UBF_Sys_ExtEnumValue a LEFT OUTER JOIN UBF_Sys_ExtEnumValue_Trl at ON a.id = at.id AND at.SysMLFlag = 'zh-cn' LEFT OUTER JOIN UBF_Sys_ExtEnumType c ON a.ExtEnumType = c.id WHERE c.Code = 'UFIDA.U9.Base.Doc.ConfirmTypeEnum';

把c.Code换成你自己的枚举全名,能查到值就说明同步成功。注意这个脚本是“先删后插”,重复执行会覆盖手工加过的枚举值,生产环境不要随手跑。

4. 单据开发:费用明细单的继承链与 UI 发布

档案是单实体,单据是继承链。费用明细单这个例子把单据类型、单据头、单据行三层都涉及到了,还带出一个经典编译错误:DocType.get抽象成员没实现。这一章按继承关系、三张实体的字段安排、UI 发布和行号设置来讲,最后给一个发布后的检查点。

4.1 单据类型与单据基类的继承链

费用明细单的单据类型实体FDDocType继承单据类型基类,单据头FeeDetail继承单据基类,单据行FeeDetailLine挂在单据头下面。资料里特别提醒:单据类型字段不要命名为DocType,会与基类的重载方法重名。编译时如果报“不会实现继承的抽象成员UFIDA.U9.Base.Doc.Doc.DocType.get”,原因就是基类要求子类重写DocType属性,而你还没有重写。

解决办法是在FeeDetail的Extend里补上:

// 重载 DocType.get,返回本单据的单据类型实体 public override UFIDA.U9.Base.Doc.DocType DocType { get { return this.FDDocType; } }

这里的FDDocType是单据头上的单据类型字段,类型是FDDocType实体。重写之后,基类的审批、编号、状态流转才能识别到具体单据类型。这个错误看起来吓人,其实就是继承链没闭合,补上重写就能过。另一个容易忽略的点是:单据类型实体要继承单据类型基类,单据头要继承单据基类,两层不要继承反了,否则发布后单据打不开。

4.2 费用明细单三张实体的字段安排

三张实体的字段我整理成三张表,建实体时直接对照。先是单据类型FDDocType:

字段名显示名称类型说明
Code编码基类继承单据类型基类
Name名称基类继承单据类型基类
Description描述基类继承单据类型基类
IsPreset系统预制布尔UI 不显示

然后是单据头FeeDetail:

字段名显示名称类型说明
Org组织基类取当前登录组织
FDDocType单据类型FDDocType不要命名为 DocType
DocNo单号基类按编号规则生成
BusinessDate日期基类默认当前日期
FeeType费用类型枚举暂估、准确
Status状态单据状态枚举开立、审核中、已审核
BadDebtAdd坏账增加十进制精度 2金额
BadDebtReduce坏账减少十进制精度 2金额
FundOccupancy资金占用额十进制精度 2金额
Memo备注字符串长 500可空
DescFlexField扩展字段实体扩展字段集合描述性弹性域

单据行FeeDetailLine:

字段名显示名称类型说明
RowNo行号整数按行号规则生成
Subject科目科目参照科目档案
Fee费用十进制精度 2金额
Memo备注字符串长 500可空
DescFlexField扩展字段实体扩展字段集合描述性弹性域

字段安排上,金额字段统一用十进制精度 2,不要用浮点;状态字段用单据状态枚举,不要自己建普通枚举,否则审批流按钮控制会对不上。扩展字段在单据头和单据行都放一份,UI 上分别绑不同的 FlexFieldPicker。

4.3 单据 UI:编号规则参照、默认日期与行号设置

单据 UI 开发里,编号规则的参照有两个关键位置:一个是参照定义在UFIDA.U9.CBO.Ref.BaseReferenceUI.MetaData,另一个是 Form 引用在UFIDA.U9.Base.Sequence.SequenceUI.WebPart。发布组件、发布页面、发布菜单这三步按顺序做,少一步页面就出不来。默认日期在Model的AfterInitModel里赋:

// 单据 UI 的 Model 初始化后,默认业务日期为当前日期 this.FeeDetail.FieldBusinessDate.DefaultValue = DateTime.Now;

FieldBusinessDate是字段绑定名,不是实体属性名,写错不会报编译错,但运行时默认值不生效。行号设置稍微麻烦一点,要读系统参数SysLineNo,再设给 Grid:

// 读取行号规则,设置 Grid 起始行号 UFIDA.U9.Base.Profile.Proxy.GetProfileValueProxy p1 = new UFIDA.U9.Base.Profile.Proxy.GetProfileValueProxy(); p1.ProfileCode = "SysLineNo"; p1.ProfileOrg = long.Parse(PDContext.Current.OrgID); int sequenceStart = 10; UFIDA.U9.Base.Profile.PVDTOData data1 = p1.Do(); if (data1 != null && !string.IsNullOrEmpty(data1.ProfileValue)) { int.TryParse(data1.ProfileValue, out sequenceStart); } UFSoft.UBF.UI.ControlModel.IAutoRowNo r1 = (UFSoft.UBF.UI.ControlModel.IAutoRowNo)this.DataGrid5; r1.StartNo = sequenceStart;

ProfileCode固定为SysLineNo,ProfileOrg传组织 ID 字符串,StartNo是 Grid 的起始行号。如果读不到参数,就给一个兜底值 10,不要让行号从 0 开始。Grid 上的扩展字段用FlexFieldHelper.SetDescFlexField绑定,列索引取最后一列:

// Grid 最后一列绑定描述性弹性域 FlexFieldHelper.SetDescFlexField( this.DataGrid5, this.DataGrid5.Columns.Count - 1, "DescFlexField");

三个参数分别是 Grid 控件、列索引、扩展字段属性名。列索引写死容易在加列后错位,建议用列名反查,或者至少在加列后回归一次。

4.4 发布后的检查点

发布组件、发布页面、发布菜单之后,不要急着打开页面。先做三个检查:一是查 3.3 节那条 UIForm SQL,确认AssemblyName和实际部署的 DLL 名一致;二是查ubf_assemble_part里 Part 是否挂到了正确的 Page 上;三是打开页面看事件日志里AfterCreateChildControls有没有抛异常。菜单找不到页面,多半是应用名或页面 URI 填错,回到 SQL 里对一遍 URI 就能定位。

5. 避坑:UI 事件顺序、枚举与参照的五个翻车点

这一章集中写我见过最多的五类问题。每一条都按“现象 → 原因 → 解决”来,能对上号就直接改,对不上号也可以当排查清单用。U9 客开的很多“玄学”问题,拆开看都是顺序、命名或缓存的事。

5.1 事件顺序写反,默认值被覆盖

现象:在AfterInitModel里给组织赋了默认值,页面打开却是空的;或者保存后默认值又变回空。

原因:UI 事件的执行顺序是AfterEventBind→AfterCreateChildControls→OnLoadData_Extend(刷新时执行)→AfterOnLoad→OnDataCollect_Extend(PostBack 执行)→BeforeUIModelBinding→AfterUIModelBinding。默认值写在AfterCreateChildControls里,可能被后面的数据绑定覆盖;写在BeforeUIModelBinding里,又可能拿不到控件。

解决:新建默认值优先写在 BE 的OnSetDefaultValue,UI 层只做展示相关的默认值。必须在 UI 层写时,放在AfterInitModel,并且用DefaultValue而不是直接给控件赋值。涉及数据收集的逻辑放OnDataCollect_Extend,涉及控件状态的放AfterUIModelBinding。

5.2 枚举同步脚本重复执行,枚举值翻倍

现象:科目类型的下拉框里出现重复值,或者枚举值编码变成ClassFullName.Value这种长串。

原因:枚举同步脚本是先DELETE再INSERT,但只清四张扩展枚举表;如果补丁包方式已经写过枚举,再手工跑一遍,会把补丁包写入的值覆盖成脚本重建的值。更常见的是同一条脚本在多个环境重复执行,ubf_sys_extenumvalue里出现多份相同ExtEnumType的记录。

解决:补丁包方式更新/安装时不要跑这段脚本,BE 的 bulk 会处理。手工执行前先备份四张表,执行后只验证不重复跑。发现重复值时,按ExtEnumTypeUID和code去重,保留最新一条,不要直接全表删。

5.3 参照过滤不生效,CustomInParams拼错命名空间

现象:给参照加了过滤条件,页面打开还是全量数据;或者过滤条件只在第一次打开生效,之后失效。

原因:CustomInParams的字符串是强类型的,命名空间、类名、属性名任何一段拼错都不会报编译错,只会静默不生效。另一个原因是写在AfterCreateChildControls里,此时参照列还没绑定数据。

解决:CustomInParams放在BeforeUIModelBinding里设,字符串按UFIDA::U9::Cust::...::实体::属性的格式拼,先用简单条件验证,再叠加复杂子查询。下面是一个可用的写法:

// 在 BeforeUIModelBinding 中给参照列加自定义过滤 IUFFldReferenceColumn c1 = (IUFFldReferenceColumn)this.DataGrid5.Columns["WaterMarkMO"]; c1.CustomInParams = BaseAction.Symbol_AddCustomFilter + "=UFIDA::U9::Cust::HXPP::MO::WaterMarkMOBE::WaterMarkMO.DocNo in (" + "select MO.DocNo from UFIDA::U9::MO::MO::MO as MO " + "where MO.Org=UFIDA::U9::Cust::HXPP::MO::WaterMarkMOBE::WaterMarkMO.Org " + "and MO.DocState in (2,3))";

命名空间段要和实际 BE 程序集一致,Symbol_AddCustomFilter是固定前缀,不要手写。改完先开页面看参照弹窗的 SQL 日志,确认过滤条件拼进去了。

5.4 参照参数过滤拿不到当前行

现象:在OnLoadData_Extend里读CurrentFilter.Parameters[0],有时为 null,有时拿到的是上一行的值。

原因:OnLoadData_Extend在刷新时执行,但参照弹窗打开时参数还没完全初始化;Parameters[0]的索引依赖参数顺序,换一个参照就错位。

解决:优先用Parameters["@ID"]按名取,取不到再退回索引;取值前判空,并且把过滤逻辑放在if (curID > 0)里面。树形参照可以用FLDcsUI.Get.TreeChildID拿子节点,再拼OPath:

if (this.Model.cRef.CurrentFilter.Parameters["@ID"] != null) { long curID = long.Parse( this.Model.cRef.CurrentFilter.Parameters["@ID"].DataValue.ToString()); if (curID > 0) { string childID = FLDcsUI.Get.TreeChildID( "CustUT_GC_ParameterConfig", "Parent", curID); if (!string.IsNullOrEmpty(childID)) { this.Model.cRef.CurrentFilter.OPath += " and UFIDA::U9::Cust::UT::GC::ParameterConfigBE::ParameterConfig.ID not in (" + childID + ")"; } } }

TreeChildID的三个参数分别是表名、父字段名、当前 ID,返回的是逗号分隔的子节点 ID 串。OPath拼接时注意前后空格,少一个空格就可能变成IDnot in。

5.5 发布后 DLL 没更新,页面还是旧逻辑

现象:代码改了、编译过了,页面行为没变;或者报“找不到类型/方法”。

原因:装配发布器的buildup.xml里 UILib 路径指错,DLL 发布到了旧目录;或者environment.xml的连接串指向了另一个库,元数据和代码对不上。

解决:发布前先确认buildup.xml的 UILib 路径和 Studio 里配的一致,发布后看目标目录 DLL 的时间戳。连接串带实例名时写成Data Source=AppServer\SQL2012,库名用当前环境,不要照抄示例:

<system> <connectionString> User Id=sa;Password=YourPassword;Data Source=AppServer\SQL2012; Initial Catalog=CustDB;packet size=4096;Max Pool size=100; Connection Timeout=900;persist security info=True; MultipleActiveResultSets=true; </connectionString> </system>

Data Source里的实例名用反斜杠,Initial Catalog换成你的客开库。改完连接串重启相关服务,再打开页面验证。

6. 发布验证与一个固定习惯:别让“看起来生效”骗了你

发布之后最危险的状态是“页面能打开、按钮能点,但逻辑没生效”。我一般会做三层验证。第一层查元数据:用 3.3 节的 UIForm SQL 确认AssemblyName、PartID、PageURI和当前部署一致;用 2.4 节的组件 SQL 确认实体属性的DisplayName已经进库。第二层查 DLL:到 UILib 目标目录看文件时间戳,确认发布的是刚编译的版本,不是缓存。第三层查运行时:打开页面,先在AfterCreateChildControls里临时打一行日志,确认事件走到了;再看参照弹窗的 SQL 日志,确认过滤条件拼进去了。

有一个小技巧可以省很多时间:把常用的验证 SQL 存成一个脚本,每次发布后按顺序跑一遍,输出三列——组件名、程序集名、页面 URI。哪一列对不上,问题就定位到哪一层。比如页面打开报“找不到类型”,先看程序集名;参照弹窗数据不对,先看OPath有没有拼进去;菜单里找不到页面,先看ApplicationName和页面 URI。

我自己的习惯是:每次发布前先把buildup.xml的 UILib 路径和environment.xml的连接串各看一遍,发布后强制跑一次元数据查询,确认AssemblyName和 DLL 名一致才打开页面。这个动作看起来多余,但省下的排查时间远比几分钟多。从那以后我每次改完 UI 都强制走一遍“查元数据、看时间戳、开页面看日志”这三步,翻车次数明显少了。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进