简介:本资源是面向用友生态伙伴及U9初学者的客户化开发系统入门指南,聚焦U9平台二次开发核心实践,解决从环境配置到业务模块落地的关键技术盲区。文档为单个817KB的Word(.docx)文件,内容涵盖UBF路径配置、解决方案与项目目录规范、BE/UI命名规则、档案建模要点、BE Code与Model对比逻辑、组件-类-属性关系SQL查询语句、UIForm/Part/Page三层结构解析及枚举同步脚本等九大实操模块,所有知识点均结合视频教学编号(如视频160/170)标注,便于对照学习。其中目录结构设计强调模块内BE/BP/SV/UI项目的归并原则,SQL查询示例可直接复用于开发环境验证,同步脚本提供补丁包外的手动枚举更新方案。目前已有3033人学习下载,是U9客开新人快速建立工程化认知、规避常见命名与架构错误的高实用性参考资料。
1. 用友U9客户化开发入门:不是写代码,是搭积木式配置驱动的业务建模实战
你刚接手一个U9客户化项目,打开Visual Studio,发现解决方案里没有“WebApplication1”,只有几十个以UFIDA.U9.Cust.XM.HXPP.EX开头的项目;双击一个.cs文件,满屏是this.Model.Subject.FieldOrg.DefaultValue = PDContext.Current.OrgRef.ID;——这不是传统.NET开发,这是在U9的UBF(Universal Business Framework)黑匣子上做精准插拔。这套机制不靠手写SQL增删改查,而是通过BE(Business Entity)定义业务实体、UIForm绑定视图、Part承载交互逻辑、Page组织导航路径,再用一套强约束的目录结构和命名规范把所有模块拧成一股绳。它解决的不是“怎么实现功能”,而是“如何让业务变化时,80%的代码不用动,只改配置、补枚举、调参数”。适合两类人:一是从ERP实施转岗的顾问,需要快速理解U9底层如何映射业务规则;二是有C#基础但没接触过元数据驱动框架的开发者,得先破除“写后台=写Service”的惯性。它不教你怎么造轮子,而是告诉你:轮子在哪、怎么换、换完怎么验。
2. UBF路径配置与解决方案目录结构:为什么必须把BE、BP、SV塞进同一个解决方案?
UBF不是插件,是U9的骨骼系统。它的路径配置直接决定编译器能否识别你的客户化组件,而目录结构则是运行时加载顺序的宪法。错一步,轻则UI打不开,重则整个模块在发布对象浏览器里消失。这不是约定俗成,是UBF元数据注册机制的硬性要求。
2.1 UBF路径配置:三处关键位置,一处错全盘崩
UBF路径配置不是配一个环境变量就完事。它分散在三个物理位置,且必须严格指向同一套UBF源码:
UBFStudioBuildupDesigner.exe所在路径
这是U9官方提供的装配工具,路径形如D:\UFIDA\UBFV28\U9.VOB.Product.UBF\UBFStudioBuildupDesigner.exe。你必须确认这个路径下存在UBFStudioBuildupDesigner.exe,且其版本号(如V28)与你当前U9服务器版本匹配。常见翻车点:用U9 V6.5的UBFStudio去编译V6.0的客户化包,会报Assembly version mismatch。UBF源码根目录(UBFStudioBuildupDesigner.exe同级)
UBFStudioBuildupDesigner.exe所在文件夹内必须包含完整的UBF源码结构,尤其是UBF_MD_*系列数据库表定义、UBF_SYS_*系统表脚本、以及UBFBaseBE等基类项目。若缺失UBFBaseBE,后续BE继承DocTypeBE时必然编译失败(如视频250中提到的error CS0534: 'FeeDetail' does not implement inherited abstract member 'Doc.DocType.get')。Visual Studio项目引用路径
在VS中,所有客户化项目(BE/BP/SV/UI)的引用必须指向本地UBF源码中的DLL,而非GAC或U9安装目录下的DLL。例如:<Reference Include="UFIDA.U9.Base.BE"> <HintPath>D:\UFIDA\UBFV28\U9.VOB.Product.UBF\UBFBaseBE\bin\Debug\UFIDA.U9.Base.BE.dll</HintPath> </Reference>提示:
HintPath必须是绝对路径,且指向bin\Debug(非Release),因为UBFStudioBuildupDesigner默认读取Debug输出。若指向Release,装配时会提示Cannot find assembly。
2.2 解决方案目录结构:五条铁律,一条都不能破
U9客户化不是按功能建项目,而是按“模块+角色”建层次。以下结构是UBF元数据扫描器的解析规则,违反即导致组件无法注册:
| 层级 | 规则 | 为什么必须这样? | 翻车后果 |
|---|---|---|---|
| 解决方案(Solution) | 同一业务模块(如EX考核模块)的所有项目必须放在同一个.sln文件下 | UBFStudioBuildupDesigner以解决方案为单位扫描项目,跨解决方案的组件不会被识别为同一模块 | BE和UI在发布对象浏览器中显示为孤立节点,无法建立关联 |
| 项目(Project) | 同一模块的BE逻辑必须全部放入单个BE项目(如EXBE),生成唯一EXBE.dll | UBF要求每个业务实体(Class)的AssemblyName全局唯一,多BE项目会导致同名Class注册冲突 | 数据库表UBF_MD_Class中出现重复FullName,BE属性丢失 |
| 项目(Project) | 同一模块的BP(业务过程)、SV(服务)必须放入单个BP项目(如EXBP),生成唯一EXBP.dll | BP方法需通过UBF_MD_UIRComponent表注册为可调用服务,多项目导致服务URI重复 | UI调用BPProxy时抛出Service not found异常 |
| 项目(Project) | 同一模块的UIForm、Part、Page必须放入单个UI项目(如EXUI),生成唯一EXUI.dll | UBF的UI路由引擎(NavigateManager)依赖AssemblyName定位Page,多UI项目导致NavigatePage("URI")找不到目标 | 页面跳转白屏,控制台报Page not registered |
| 组件(Component) | 同一模块的枚举(Enum)必须全部定义在BE项目内,且MD_Component_ID指向该BE组件 | 枚举值存储在UBF_SYS_ExtEnumType表,其ExtEnumTypeUID字段必须关联到BE组件ID | 枚举在UI下拉框中显示为空,或保存时报Invalid enum value |
2.3 实操:从零搭建EX考核模块解决方案
我们以摘要中“考核模块(EX)>>科目(Subject)”为例,演示标准建法:
创建空解决方案
VS中新建解决方案,命名为U9.Cust.XM.HXPP.EX.Solution.sln,路径建议:D:\UFIDA\VSProject\Cust\XM\HXPP\EX\添加BE项目(强制单项目)
- 右键解决方案 → “添加” → “新建项目” → 选择“类库(.NET Framework)”
- 名称:
EXBE - 路径:
D:\UFIDA\VSProject\Cust\XM\HXPP\EX\EXBE\ - 引用UBF基类:添加对
UFIDA.U9.Base.BE.dll(来自UBF源码UBFBaseBE\bin\Debug\)的引用
添加BP项目(强制单项目)
- 新建类库项目,名称:
EXBP - 路径:
D:\UFIDA\VSProject\Cust\XM\HXPP\EX\EXBP\ - 引用:
EXBE.dll+UFIDA.U9.Base.BP.dll
- 新建类库项目,名称:
添加UI项目(强制单项目)
- 新建“ASP.NET Web Forms应用程序”,名称:
EXUI - 路径:
D:\UFIDA\VSProject\Cust\XM\HXPP\EX\EXUI\ - 引用:
EXBE.dll+EXBP.dll+UFIDA.U9.UI.PDHelper.dll
- 新建“ASP.NET Web Forms应用程序”,名称:
验证目录结构
最终文件树必须为:D:\UFIDA\VSProject\Cust\XM\HXPP\EX\ ├── U9.Cust.XM.HXPP.EX.Solution.sln ├── EXBE\ │ └── Cust_XM_HXPP_EX_Subject.cs // 科目BE实体 ├── EXBP\ │ └── SubjectBP.cs // 科目业务过程 └── EXUI\ └── SubjectUI\ ├── SubjectForm.aspx // UIForm └── SubjectPart.ascx // Part
注意:
Cust_XM_HXPP_EX_Subject.cs的命名必须严格遵循摘要第3条“命名规范”——配件名UFIDA.U9.Cust.XM.HXPP.EX,BE表名Cust_XM_HXPP_EX_Subject。这是UBF元数据扫描器识别实体的唯一依据,大小写、下划线、顺序缺一不可。
3. BE与UI模型深度绑定:从档案需求到可运行代码的完整映射链
U9客户化最反直觉的点在于:你写的C#代码,90%不是在操作数据库,而是在告诉UBF框架“这个业务字段应该长什么样、默认值是什么、跟谁联动”。BE定义数据契约,UI定义呈现契约,两者通过Model对象桥接。这一章拆解“科目档案”从需求文档到可运行页面的全链路,重点讲清BE属性与UI控件的隐式绑定逻辑。
3.1 档案需求→BE实体:字段类型、主键、枚举的硬编码规则
摘要第4条给出的“科目”需求表,不是数据库设计文档,而是BE类的字段声明说明书。每行对应Cust_XM_HXPP_EX_Subject.cs中的一个属性,但需转换为UBF特定语法:
// BE实体:Cust_XM_HXPP_EX_Subject.cs public partial class Cust_XM_HXPP_EX_Subject : UFIDA.U9.Base.BE.BusinessEntity { // Org组织:业务主键,不可空 → 映射为EntityRef类型,且Required=true [EntityRef(typeof(UFIDA.U9.Base.Org.Org), Required = true)] public virtual Org Org { get; set; } // Code编码:字符串,业务主键,长50 → 映射为String类型,MaxLength=50,且Key=true [Key] [StringLength(50)] public virtual string Code { get; set; } // Name科目名称:字符串,不可空,长200 → 映射为String类型,Required=true,MaxLength=200 [Required] [StringLength(200)] public virtual string Name { get; set; } // SubjectType科目类型:枚举 → 必须继承UBF内置枚举基类,并指定枚举值集合 [EnumType(typeof(SubjectTypeEnum))] public virtual SubjectTypeEnum SubjectType { get; set; } // Effective状态:布尔型 → 直接用bool,UBF自动映射为tinyint(1) public virtual bool Effective { get; set; } // Memo备注:字符串,长500 → StringLength(500) [StringLength(500)] public virtual string Memo { get; set; } // DescFlexField扩展字段:UBF专用类型,无需手动声明属性 // UBF会自动为该实体生成DescFlexField集合,UI端用FlexFieldHelper.SetDescFlexField()绑定 }关键参数说明:
[Key]:标记业务主键,UBF生成数据库表时设为PRIMARY KEY,且Code字段在UI中自动设为只读(因主键不可修改)。[EntityRef]:声明外键关系,typeof(Org)指向UBF标准组织实体,UBF自动生成Org_ID外键列及关联查询。[EnumType]:SubjectTypeEnum必须是public enum,且枚举值名称(如AuxMaterial)需与摘要中“辅料”等中文显示名通过资源文件(UBF_RES_resourcevalue)映射,否则UI下拉框显示英文。DescFlexField:UBF预留的弹性字段容器,BE中不声明具体属性,UI中通过FlexFieldHelper动态绑定,避免为每个客户化字段建DB列。
3.2 BE→UI Model:OnSetDefaultValue与AfterInitModel的执行时机差异
BE赋默认值和UI赋默认值看似一样,实则天壤之别。前者在数据层生效,后者在展现层生效,选错时机直接导致数据错乱。
BE层默认值:OnSetDefaultValue(数据持久化前触发)
此方法在Save()提交到数据库前执行,影响最终入库值。适用于必须保证数据一致性的场景,如组织默认值:// BE实体中重写OnSetDefaultValue protected override void OnSetDefaultValue() { base.OnSetDefaultValue(); // 若未设置Org,则取当前登录组织 → 确保每条记录都有Org,避免空外键 if (this.Org == null) { this.Org = Context.LoginOrg; // Context.LoginOrg来自UBF上下文,非Session } }逻辑说明:
Context.LoginOrg是UBF从当前用户会话中提取的组织对象,this.Org = Context.LoginOrg将组织ID写入BE实体内存,后续Save()时自动插入Org_ID列。若此处写this.Org_ID = Context.LoginOrg.ID,则绕过UBF的EntityRef校验,可能引发外键约束失败。UI层默认值:Model.AfterInitModel(UI初始化后触发)
此方法在UIForm加载完毕、Model对象已实例化但尚未绑定到控件时执行,仅影响前端显示。适用于UI交互优化,如默认填充当前组织编码:// UIForm的Model类中(如SubjectModel.cs) protected override void AfterInitModel() { base.AfterInitModel(); // 填充UI控件的默认值,不影响BE实体本身 this.Subject.FieldOrg_Code.DefaultValue = PDContext.Current.OrgRef.CodeColumn; this.Subject.FieldOrg_Name.DefaultValue = PDContext.Current.OrgRef.NameColumn; // 注意:FieldOrg(组织对象)不在此处赋值,因UI中组织选择器需用户手动选择 }参数说明:
FieldOrg_Code是UBF为Org外键自动生成的Code字段绑定器,PDContext.Current.OrgRef是UI上下文中的组织引用对象,CodeColumn为其编码列。此赋值仅让UI文本框显示默认编码,用户仍可修改;若用户修改了编码,UBF会自动根据编码查出对应Org对象并赋给FieldOrg。
3.3 扩展字段(DescFlexField):UI端零代码绑定的弹性方案
摘要第4条“DescFlexField扩展字段”是U9应对客户化字段爆炸的核心机制。它不新增数据库列,而是在UBF_SYS_DescFlexField表中动态注册字段,再通过FlexFieldHelper绑定到UI控件。全程无需写SQL建表、无需改BE代码。
UI端绑定代码(WebPart中)
// 在SubjectPart.ascx.cs的AfterCreateChildControls中 protected override void AfterCreateChildControls() { base.AfterCreateChildControls(); // 将扩展字段控件FlexFieldPicker0,绑定到Subject实体的DescFlexField集合 FlexFieldHelper.SetDescFlexField(this.FlexFieldPicker0, this.Model.Subject); // this.Model.Subject即BE实体,UBF自动提取其DescFlexField属性 }逻辑说明:
FlexFieldPicker0是U9标准扩展字段控件(类型UFIDA.U9.UI.Controls.FlexFieldPicker),SetDescFlexField方法会:- 查询
UBF_SYS_DescFlexField表中所有EntityType = 'Cust_XM_HXPP_EX_Subject'的字段定义; - 根据字段类型(文本/数字/日期/参照)动态渲染对应UI控件;
- 将用户输入值序列化为JSON存入
UBF_SYS_DescFlexFieldValue表。
全程无硬编码字段名,新增扩展字段只需在U9管理后台配置,UI自动识别。
- 查询
BE端扩展字段数据获取
若需在BP中读取扩展字段值,不通过Subject.DescFlexField["FieldName"],而用UBF标准API:// 在EXBP项目中 public static string GetFlexFieldValue(Cust_XM_HXPP_EX_Subject subject, string fieldName) { var flexValue = subject.DescFlexField.FirstOrDefault(x => x.Name == fieldName); return flexValue?.Value ?? string.Empty; }
4. 避坑:客户化开发中五个血泪经验总结的高频翻车点
U9客户化不是写完代码就能跑,UBF框架的元数据注册、上下文传递、UI生命周期管理处处是坑。以下是我在多个模拟项目X中踩过的真坑,按现象→原因→解决三步法整理,每一条都附带可复现的错误日志特征。
4.1 现象:UIForm打开白屏,浏览器控制台报Uncaught ReferenceError: $ is not defined
原因:UI项目未正确引用jQuery。U9的PDHelper严重依赖jQuery 1.12.4,但VS新建Web Forms项目默认引用jQuery 3.x,且ScriptManager未启用EnablePartialRendering。
解决:
- 删除
<script>标签中对jQuery 3.x的引用; - 在
Site.Master或SubjectForm.aspx顶部添加:<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="true" /> <script src="~/Scripts/jquery-1.12.4.min.js"></script> - 确认
jquery-1.12.4.min.js文件存在于Scripts文件夹,且版本号精确匹配(U9 V6.5要求1.12.4,V6.0要求1.10.2)。
4.2 现象:BE编译通过,但发布后在U9客户端看不到“科目”菜单,或点击报Object reference not set to an instance of an object
原因:BE项目未正确设置AssemblyName,或AssemblyName与UBF元数据注册的ComponentName不一致。UBF要求AssemblyName必须为UFIDA.U9.Cust.XM.HXPP.EX(不含.BE后缀),而VS默认项目名为EXBE,导致AssemblyName为EXBE。
解决:
- 右键
EXBE项目 → “属性” → “应用程序”选项卡 → 修改程序集名称为UFIDA.U9.Cust.XM.HXPP.EX; - 重新编译,检查
EXBE.dll的AssemblyName:用ILSpy打开dll,查看AssemblyTitle是否为UFIDA.U9.Cust.XM.HXPP.EX; - 确认
UBF_MD_Component表中AssemblyName字段值与之完全一致(区分大小写)。
4.3 现象:UI中枚举下拉框显示为空,或保存时报The value 'AuxMaterial' is not valid for EnumType 'SubjectTypeEnum'
原因:枚举值未在UBF_RES_resourcevalue表中注册中文显示名,或UBF_SYS_ExtEnumType表中Code字段值与BE中EnumType属性值不匹配。
解决:
- 检查BE中枚举定义:
public enum SubjectTypeEnum { AuxMaterial, // 必须与UBF_SYS_ExtEnumType.Code值完全一致 MainMaterial, // ... } - 执行摘要第9条同步脚本前,先确认
UBF_SYS_ExtEnumType中Code='AuxMaterial'的记录存在; - 在
UBF_RES_resourcevalue表中插入资源记录:INSERT INTO UBF_RES_resourcevalue (ResourceName, Language, Name, Description) VALUES ('AuxMaterial', 'zh-CN', '辅料', '辅料');注意:
ResourceName必须等于枚举值名称(AuxMaterial),Language为zh-CN,否则UI无法加载中文。
4.4 现象:列表页双击行跳转到卡片页时,URL中ID参数为空,或跳转后显示“未找到记录”
原因:OnGridRowDbClick_Extend中获取FocusedRecord["ID"]时,FocusedRecord为null。U9列表控件的FocusedRecord需在OnLoadData_Extend之后才可用,而OnGridRowDbClick_Extend在OnLoadData_Extend之前触发。
解决:
- 改用
Grid.SelectedKeys获取选中行ID:protected void OnGridRowDbClick_Extend(object sender, EventArgs e) { if (this.DataGrid5.SelectedKeys.Count > 0) { string dataid = this.DataGrid5.SelectedKeys[0].ToString(); // 获取第一个选中行ID if (!string.IsNullOrEmpty(dataid)) { UFIDA.U9.UI.PDHelper.NaviteParamter p = new NaviteParamter(); p.NameValues.Add("PDPageStatus", "browser"); p.NameValues.Add("ID", dataid); UFIDA.U9.UI.PDHelper.NavigateManager.NavigatePage(this.CurrentPart, "UFIDA.U9.Cust.XM.HXPP.EX.SubjectURI", p); } } } - 确保列表页
DataGrid5的DataKeyNames属性设为"ID",否则SelectedKeys无法提取ID。
4.5 现象:费用明细单(FeeDetail)BE编译报错error CS0534: 'FeeDetail' does not implement inherited abstract member 'Doc.DocType.get'
原因:FeeDetail继承自UFIDA.U9.Base.Doc.Doc,但未重写抽象属性DocType。U9要求所有单据类必须明确指定单据类型(FDDocType),且DocType属性返回值必须是UFIDA.U9.Base.Doc.DocType类型。
解决:
- 在
FeeDetail.cs中添加重写:// 重载 DocType.get public override UFIDA.U9.Base.Doc.DocType DocType { get { return this.FDDocType; } // FDDocType是FeeDetailBE中定义的DocType属性 } - 确保
FeeDetailBE类中已声明FDDocType属性:[EntityRef(typeof(FDDocTypeBE), Required = true)] public virtual FDDocTypeBE FDDocType { get; set; }注意:
FDDocTypeBE必须是独立BE实体,且其AssemblyName为UFIDA.U9.Base.Doc(来自UBFBaseBE),不能自己新建。
5. UI事件生命周期与按钮状态机:从点击到数据落地的七步执行流
U9的UI事件不是简单的Button_Click,而是一套严格的生命周期钩子链。搞不清AfterCreateChildControls和AfterUIModelBinding的区别,就会写出“UI显示了默认值,但保存时还是空”的玄学bug。这一章用“费用明细单”的提交按钮为例,拆解从用户点击到数据落库的完整七步流,并给出状态机控制模板。
5.1 UI事件执行顺序:七个钩子,每个都有不可替代的使命
摘要第12条列出的事件顺序是U9的黄金法则。我们以费用明细单(FeeDetail)的“提交”按钮为例,追踪一次完整点击:
| 步骤 | 事件名 | 触发时机 | 典型用途 | 本例中做什么 |
|---|---|---|---|---|
| 1 | AfterEventBind | 所有事件委托绑定完成后 | 初始化事件监听器 | 绑定BtnSubmit.Click += BtnSubmit_Click_Extend |
| 2 | AfterCreateChildControls | UI控件树创建完毕,但未绑定数据 | 设置控件初始状态、注册扩展字段 | 调用SetPersonalizationEnable开启个性化,ShowDelConfirmDialog绑定删除提示 |
| 3 | OnLoadData_Extend | 列表页首次加载数据时(非PostBack) | 加载主数据、设置过滤条件 | 列表页中执行CustomFilterOpath_Extend,追加and Org={CurrentOrgID} |
| 4 | AfterOnLoad | OnLoadData_Extend执行后 | 数据加载后微调 | 无 |
| 5 | BeforeUIModelBinding | UIModel数据即将绑定到控件前 | 动态修改Model值、预处理 | 无 |
| 6 | AfterUIModelBinding | UIModel数据已绑定到控件后 | 设置控件可见性、启用/禁用 | 调用SetControlStauts(),根据单据状态Status控制BtnSubmit是否启用 |
| 7 | BtnSubmit_Click_Extend | 用户点击按钮后 | 业务逻辑处理、数据保存 | 执行BtnSave_Click_Extend两次,中间修改Status=1 |
关键洞察:
AfterUIModelBinding是设置按钮状态的唯一正确时机。若在AfterCreateChildControls中设置BtnSubmit.Enabled = false,则用户首次打开页面时按钮禁用,但当用户修改单据状态后,AfterUIModelBinding会再次触发,此时SetControlStauts()才能根据最新Status值刷新按钮状态。这就是为什么摘要第14条强调“在AfterUIModelBinding中调用SetControlStauts”。
5.2 不走审批流的状态机:三状态按钮控制模板
费用明细单不走U9标准审批流,需自行管理Status(0开立/1审核中/2已审核)。按钮状态必须与Status严格同步,且禁用非法操作(如已审核后还能提交)。摘要第14条的SetControlStauts是核心,但需补充两点实战细节:
- 状态变更必须伴随
BtnSave_Click_Extend调用:U9的Status字段是普通属性,修改后不自动保存,必须显式调用保存方法。摘要中BtnSubmit_Click_Extend内两次调用BtnSave_Click_Extend,第一次保存原始数据,第二次保存更新Status后的数据。 - 只读控制要覆盖所有字段:摘要中
Card4.ReadOnly = true仅控制一个区域,实际需遍历所有字段控件:private void SetReadOnlyForStatus2() { // 卡片区域只读 Card4.ReadOnly = true; // 所有IUIFieldBindingDataBindControl控件只读 foreach (Control c in this.Controls) { if (c is IUIFieldBindingDataBindControl field) { field.ReadOnly = true; } } // 特殊字段单独控制(如审批人、审批时间) ((IUIFieldBindingDataBindControl)this.ApprovedBy101).ReadOnly = true; ((IUIFieldBindingDataBindControl)this.ApprovedOn311).ReadOnly = true; }
5.3 页面导航:URI字符串的构成与调试技巧
U9的NavigatePage("URI", param)不是简单跳转,URI是UBF注册的全局唯一标识符。摘要第19条给出的UFIDA.U9.Cust.XM.HXPP.EX.SubjectURI,其构成规则如下:
UFIDA.U9.Cust.XM.HXPP.EX.SubjectURI │ │ │ │ │ │ │ │ │ │ │ │ │ └─ Page名称(必须与UBF_MD_Page表中Name字段一致) │ │ │ │ │ └──────── UIForm名称(必须与UBF_MD_UIForm表中Name字段一致) │ │ │ │ └─────────── 模块编码(EX) │ │ │ └────────────── 公司编码(HXPP) │ │ └───────────────── 地区编码(XM) │ └────────────────────── U9标准命名空间前缀 └─────────────────────────────── 固定前缀调试URI是否注册成功:
在U9客户端,按Ctrl+Shift+U打开“发布对象浏览器”,搜索SubjectURI,确认其Page、UIForm、AssemblyName均存在且状态为“已发布”。若不存在,说明EXUI项目未正确装配。动态URI拼接防错:
摘要中this.NavigatePage("UFIDA.U9.Cust.XM.HXPP.EX.SubjectListURI", null)硬编码URI易出错。更安全的做法是定义常量:public static class URIs { public const string SubjectList = "UFIDA.U9.Cust.XM.HXPP.EX.SubjectListURI"; public const string SubjectCard = "UFIDA.U9.Cust.XM.HXPP.EX.SubjectURI"; } // 使用时 this.NavigatePage(URIs.SubjectList, null);
6. 发布与装配:从VS编译到U9客户端可用的四步验证法
客户化开发最后一步不是“编译成功”,而是“在U9客户端点开菜单能用”。UBF的装配(Buildup)和发布(Publish)是两道独立关卡,漏检任何一步都会导致前功尽弃。我给自己定了一套四步验证法,每次发布必走,十年零漏发。
6.1 第一步:UBFStudioBuildupDesigner装配验证(检查DLL依赖)
UBFStudioBuildupDesigner不是IDE,是UBF的装配校验器。它不编译代码,而是扫描项目引用、检查UBF元数据一致性。启动它前,必须确保:
buildup.xml中UILib路径指向EXUI\bin\Debug\EXUI.dll(非Release);environment.xml中ConnectionString的Initial Catalog数据库名与当前U9测试库完全一致(如HXPP0326);- 所有项目(EXBE/EXBP/EXUI)已用VS编译成功,且
bin\Debug下存在对应DLL。
执行UBFStudioBuildupDesigner.exe后,观察输出窗口:
- ✅ 正确日志:
Buildup completed successfully. Total components: 12(组件数应与你BE/UI中定义的Class数匹配); - ❌ 错误日志:
Error: Cannot resolve assembly 'UFIDA.U9.Cust.XM.HXPP.EX'→ 检查EXBE项目AssemblyName是否为UFIDA.U9.Cust.XM.HXPP.EX; - ❌ 错误日志:
Warning: Component 'Subject' has no UIForm binding→ 检查UBF_MD_UIForm表中是否存在Name='Subject'的记录。
6.2 第二步:U9管理后台发布验证(检查元数据注册)
装配成功后,登录U9管理后台(http://your-server/u9/),进入“系统管理” → “发布对象浏览器”:
- 搜索
Cust_XM_HXPP_EX_Subject,确认其Component状态为“已发布”,AssemblyName为UFIDA.U9.Cust.XM.HXPP.EX; - 展开该Component,检查
Classes下是否有Cust_XM_HXPP_EX_Subject,且Kind为Entity; - 搜索
SubjectURI,确认其Page状态为“已发布”,UIForm指向正确的SubjectForm.aspx。
提示:若搜索不到,点击右上角“刷新元数据缓存”,U9会重新扫描
UBF_MD_*表。
6.3 第三步:U9客户端菜单验证(检查导航可达性)
登录U9客户端,进入“系统” → “自定义” → “菜单管理”:
- 找到你发布的菜单项(如“考核管理” → “科目维护”),确认其
URI字段值为UFIDA.U9.Cust.XM.HXPP.EX.SubjectURI; - 点击菜单,观察地址栏URL是否变为
.../u9/Default.aspx?PageURI=UFIDA.U9.Cust.XM.HXPP.EX.SubjectURI; - 若URL中
PageURI参数为空,说明菜单未正确绑定URI,需在菜单管理中重新选择URI。
6.4 第四步:运行时行为验证(检查上下文与数据流)
菜单打开后,进行三类必测操作:
上下文验证:在UIForm代码中临时添加:
protected override void AfterCreateChildControls() { base.AfterCreateChildControls(); // 打印当前组织ID,确认上下文注入正常 System.Diagnostics.Debug.WriteLine($"Current OrgID: {PDContext.Current.OrgID}"); }查看VS“输出”窗口,确认打印出数字ID(非0或null)。
数据绑定验证:在列表页
OnLoadData_Extend中添加:protected override void OnLoadData_Extend() { base.OnLoadData_Extend(); // 打印查询到的记录数 System.Diagnostics.Debug.WriteLine($"Loaded records: {this.DataGrid5.DataSource.Count}"); }确认数量与数据库
Cust_XM_HXPP_EX_Subject表中数据一致。保存验证:点击“保存”后,在SQL Server Profiler中捕获U9执行的SQL,确认包含:
INSERT INTO Cust_XM_HXPP_EX_Subject (Org_ID, Code, Name, SubjectType, ...) VALUES (...)而非
INSERT INTO Subject (...)(说明UBF正确映射了表名)。
从那以后我每次发布客户化包,都强制走一遍这四步:UBFStudio看装配日志、发布对象浏览器查
本文还有配套的精品资源,点击获取