简介:面向用友U9客户化开发初学者的一份课程讲解文档,适合用友伙伴在入门阶段系统梳理客开流程与关键概念。内容围绕UBF路径配置、解决方案与项目组件的目录层次、命名规范、档案开发需求等基础环节展开,并延伸到BE Code与Model对比、组件/类实体/属性关系查询、UI Code与Model对比、UIForm/Part/Page关系查询及同步枚举脚本等主题。文档中配有SQL查询示例和科目档案字段设计说明,便于读者理解U9元数据组织方式、对照实际开发对象查找组件与界面关系,也为后续单据与档案客开提供可复用的排查思路。压缩包共1个docx文件,约817KB,结构集中,适合按知识点逐节查阅。目前已有3029人学习,可作为U9客开入门阶段的参考资料。
1. U9 客户化开发入门的第一张地图:四层结构,缺一层都得返工
U9 客户化开发入门这件事,最容易被误读成「在 Webpart 里写几个按钮事件」。真跑一遍会发现,敲 C# 的时间大概只占三成,剩下七成都在跟元数据打交道:UBF 里挂路径、解决方案怎么切项目、BE 建完模型再对 UI、发布时装配能不能过。整张地图其实就四层——工程层(路径、解决方案、命名)、元数据层(BE、枚举、类与属性)、界面层(UIForm、Part、Page 与事件顺序)、发布层(装配、连接串、页面与菜单)。四层里任何一层塌了,后面全是白干。
这份梳理适合已经能打开 UBF Studio、但每次新建模块都要回头翻视频的人;也适合复习阶段想把零散命令串成一条流水线的人。下面从最容易踩坑的工程结构开始,一路走到最后的自检清单。
2. 工程结构与命名:UBF 路径、解决方案分层与配件名的三处硬约束
工程层是整条链路的起点,也是最不像「写代码」的部分。很多人一上来就新建项目,写完编译通过,发布时才发现 DLL 打架、菜单点不开、连接串连的是别人的库。这些问题在当前代码里找,一个都找不到答案,全部要回到工程结构上看。这一章把 UBF 路径、解决方案切分、命名规范三件事拆开,每一件都落到可以照着做的粒度。
2.1 UBF 路径配置:开工前要确认清楚的三个位置
UBF 路径配置是清单上的第一项,也是唯一一项「一旦配错,后面所有报错都指向别处」的工作。课程里把它放在 160 号视频,说明它是前置动作而不是可选项。真正常用到的位置其实只有三个:
第一是 UBF 产品安装根目录,常见形态是\UBFV28\U9.VOB.Product.UBF\,装配设计器UBFStudioBuildupDesigner.exe就躺在这个目录里,它负责把编译产物装进运行环境。第二是这个设计器读取的buildup.xml,里面写的UILib路径决定了界面库从哪里取,路径不改,发布出去的界面就是旧版本。第三是\Runtime\environment.xml,连接串就写在里面,它决定你发布的元数据落到哪个库。
这三个位置的顺序是死的:先确认产品根目录存在且版本对得上,再改buildup.xml的UILib,最后核对environment.xml的库名。顺序颠倒的典型现象是:装配器能打开,点了发布也显示成功,登录进系统却发现新单据不在菜单里——因为元数据全灌到了另一个库。
提示:路径里不要留中文和空格,UBF 的部分组件对路径做字符串拼接,中文目录在装配阶段报的错往往跟真实原因毫无关系。
2.2 解决方案—项目—组件的分层:一条 DLL 就是一个部署边界
课程里给的规则看起来零碎,其实背后只有一个判断标准:一条 DLL 就是一个部署边界。拆项目的粒度,等价于「以后打补丁时,哪些东西必须一起换」。规则原样整理成下表,方便对照执行。
| 项目类型 | 拆分粒度 | 产出 |
|---|---|---|
| BE 项目 | 同一模块所有 BE 放在同一个解决方案内的一个项目 | 一个 BE 的 DLL |
| 枚举 | 同一模块所有枚举放在同一个 BE 组件内 | 无独立 DLL,随 BE 走 |
| BP、SV 项目 | 同一模块所有 BP、SV 只用一个项目 | 一个 BP、SV 的 DLL |
| 档案或单据 UI | 一个档案/单据 + 其相关查找参照 UI,用同一个项目 | 一个 UI 的 DLL |
| 列表 UI | 同一模块所有列表 UI 用一个项目 | 一个列表 DLL |
| 参照 UI | 同一模块所有参照 UI 用一个项目 | 一个参照 DLL |
层次关系从大到小是:解决方案 >> 项目 >> 组件。同一个模块的所有 BE、BP、SV 项目都放在同一个解决方案内,方便一起编译、一起发布;但项目本身按上表的粒度合并。
为什么 BL 层要合成一个项目?因为 BE 之间会互相引用,拆成多个项目后,一个实体改动就可能引发两个 DLL 的循环依赖,发布顺序也变得不可控。为什么 UI 层反而要拆细?因为界面是并行开发最密集的地方,一个档案一个 DLL,两个顾问同时改不同单据时不会互相覆盖,上线后出问题也只替换那一个 DLL。这套「后端合并、前端拆细」的思路,在 U9 客开里几乎是默认答案。
2.3 命名规范:配件名与 BE 表名的公式化写法
命名规范在课程里只给了两个公式,但这两个公式的每一段都有含义,写错一段,后面查元数据时就要多花半小时。
配件名格式:UFIDA.U9.Cust.XM.HXPP.EX。逐段拆开是:UFIDA.U9是产品固定命名空间,照抄不改;Cust表示客户化扩展,用来和标准产品区分;XM是地区代号;HXPP是公司代号;EX是模块代号。这里的地区、公司、模块都是占位,实际项目里换成自己单位的编码即可。
BE 表名格式:Cust_XM_HXPP_EX_XXXX。前三段与配件名一一对应,只是分隔符从点换成了下划线,XXXX是具体实体名。这样命名的好处是,在数据库里看到一张表就能反推它属于哪个模块、哪个实体,执行 3.4 节那几条元数据 SQL 时,where条件不用猜。
| 段位 | 配件名 | 表名 | 说明 |
|---|---|---|---|
| 产品前缀 | UFIDA.U9 | 无 | 固定,不改 |
| 客户化标识 | Cust | Cust | 固定,标识这是客开对象 |
| 地区 | XM | XM | 占位,按实际地区缩写 |
| 公司 | HXPP | HXPP | 占位,按实际单位缩写 |
| 模块 | EX | EX | 占位,按模块英文缩写 |
| 实体 | 无 | XXXX | 实体名,如 Subject |
注意:配件名一旦发布出去就不要再改。UBF 的元数据表里保存的是全名,改名会导致旧元数据和新 DLL 对不上,症状是页面能打开但字段全是空白。
2.4 装配发布:buildup.xml、environment.xml 与发布三件套
装配发布对应 190 号视频,动作本身只有三步:改buildup.xml的UILib路径、核对environment.xml的连接串、跑UBFStudioBuildupDesigner.exe。连接串是最容易被复制粘贴带错的,尤其是数据库带实例名的场景:
<!-- Runtime\environment.xml,注意 Data Source 的实例名写法 --> <system> <connectionString> User Id=sa;Password=***; Data Source=主机名\实例名; <!-- 带实例名时必须写成 主机名\实例名,只写主机名会连到默认实例 --> Initial Catalog=HXPP0326; <!-- 目标库,别连成测试库 --> packet size=4096; Max Pool size=1500; <!-- 并发高时连不上,先看这里 --> persist security info=True </connectionString> </system>Data Source写成主机名\实例名还是只写主机名,取决于目标库是不是默认实例,这一项错了报的是「登录失败」或「找不到数据库」,但代码一行没错。Max Pool size在多人同时登录的测试环境里经常是瓶颈,默认值偏小,报错信息又是一句含糊的超时。
装配完成不等于发布完成,发布是三个动作:发布组件、发布页面、发布菜单。少做任何一个,现象都不同——只发组件,页面打不开;发了组件和页面,菜单里找不到入口;三个都发了但没给角色授权,登录后依然看不见。这条链路在后面第 5 章还会再提一次,因为它是联调阶段最高频的翻车点。
3. BE 建模与元数据:档案字段、单据三层与 SQL 反查
元数据层是 U9 客开真正的重心。界面上的一个字段,背后依次是:类(Class)上的属性(Attribute)、属性对应的组件(Component)、组件编译出的 DLL、DLL 里的实体类。这四层任何一层对不上,界面就显示空值。这一章从两个最典型的建模场景入手——档案和单据——把字段设计、继承重载和元数据反查串起来。
3.1 档案开发需求:科目 Subject 的字段怎么落
课程用「考核模块(EX)」下的「科目(Subject)」当档案范例,对应 170、180 号视频。字段清单原样整理如下,这张表基本就是建 BE 时的录入顺序。
| 名称 | 显示名称 | 类型 | 约束 |
|---|---|---|---|
| Org | 组织 | 组织机构 | 业务主键,不可空 |
| Code | 编码 | 字符串 | 业务主键,不可空,长 50 |
| Name | 科目名称 | 字符串 | 不可空,长 200 |
| SubjectType | 科目类型 | 枚举 | 见下方枚举值 |
| Effective | 状态 | 状态 | 启用/停用 |
| Memo | 备注 | 字符串 | 长 500 |
| DescFlexField | 扩展字段 | 实体扩展字段集合 | 集合类型,不是单个字段 |
科目类型的枚举值是:辅料、主要材料、产值、费用、对账差异、营销可比利润、调节项、其他收入、税后利润、考核税前利润。十个值,一次性建全,别等到开发中途再补——补枚举意味着要重跑 3.5 节的同步脚本。
这里有两个设计点值得停下来想。第一,业务主键是Org + Code的组合,不是单独的Code。多组织环境下,两个组织可以有同编码的科目,主键不加组织会在保存时报唯一性冲突,而且报错位置在保存按钮上,查起来很绕。第二,DescFlexField的类型是「实体扩展字段集合」,它是弹性域的载体,界面上要用专门的控件承接,4.4 节会给出挂载代码。
3.2 单据三层:单据类型、单据、单据行的分工
190 号之后的内容切到单据,范例是费用明细单,一共三个实体:FDDocType(单据类型)、FeeDetail(单据)、FeeDetailLine(单据行)。这三层的字段分工是 U9 单据建模的通用套路,理解了这一个,其他单据照抄结构即可。
| 实体 | 继承 | 关键字段 | 备注 |
|---|---|---|---|
| FDDocType | 单据类型基类 | 编码、名称、IsPreset、状态、上游单据、编号方式、确认方式、审批方式、自动生成编号、是否可改 | 大部分字段来自基类,不要重造 |
| FeeDetail | 单据基类 | 组织、单据类型、单号、日期、FeeType、Status、BadDebtAdd、BadDebtReduce、FundOccupancy、Memo、扩展字段 | 单号与日期继承自基类 |
| FeeDetailLine | 单据行基类 | RowNo、Subject、Fee、Memo、DescFlexField | Subject 引用 3.1 节的科目档案 |
FeeType是枚举,两个值:暂估、准确。Status是单据状态枚举,三个值:开立、审核中、已审核。三个金额字段BadDebtAdd、BadDebtReduce、FundOccupancy都要求十进制精度 2,Memo长 500,和档案保持一致的写法。
真正容易栽跟头的是单据类型字段的命名。课程明确写了:费用明细单上的单据类型字段不要叫DocType,会与基类的重载方法重名。字段名用FDDocType,然后在 BE 的 Extend 部分补一个重载,否则编译直接报错:
// FeeDetail 实体的 Extend 部分 // 字段名用 FDDocType 避开基类的 DocType 成员,这里把基类属性指回去 public override UFIDA.U9.Base.Doc.DocType DocType { get { return this.FDDocType; } }不写这段,编译器报的是「不会实现继承的抽象成员 UFIDA.U9.Base.Doc.Doc.DocType.get」,错误信息指向生成的FeeDetail.cs某一行,看起来像生成器出了问题,实际上是字段命名撞了基类成员。这类错误的排查成本很高,因为报错文件是自动生成的,改它没用,改完重新生成又回去了——根因在建模时的字段名。
另外,单据的组织默认值要在实体的 Extend 里、OnSetDefaultValue中赋值。单据类型、单据、单据行三层如果都带组织,三处都要给默认值,漏一处就会出现「表头有组织、表体组织为空」的脏数据。
3.3 BE Code 与 Model 的对照:同一个字段在两层的不同写法
BE Code 与 Model 的对比是课程单列的一节,因为初学者最常问的就是「为什么 BE 里叫Org,UI 里变成FieldOrg」。两者的关系可以概括成一句话:BE 是服务端的实体定义,Model 是 UI 侧对实体的强类型包装,包装时会给字段加前缀并派生编码、名称两个影子字段。
| 维度 | BE Code | UI Model |
|---|---|---|
| 所在层 | 服务端 | 客户端 |
| 访问方式 | this.Org | this.Model.Subject或this.Subject |
| 字段名 | Org、Code | FieldOrg、FieldOrg_Code、FieldOrg_Name |
| 默认值位置 | OnSetDefaultValue | AfterInitModel |
| 取值来源 | Context.LoginOrg | PDContext.Current.OrgRef |
一个组织字段在 Model 层会裂成三个:FieldOrg存 ID,FieldOrg_Code存编码,FieldOrg_Name存名称。界面上显示名称、后台存 ID,靠的就是这三者同步赋值。只赋FieldOrg不赋另外两个,界面会显示一串数字 ID 或者干脆空白,这是新手最常见的「默认值不生效」现象。
还有一个隐含约束:BE 改完字段必须重新生成,UI 侧的 Model 才会出现对应的Field属性。改了 BE 没重新生成,编译报「Model 不包含 FieldXXX 的定义」,这时候不要怀疑拼写,先去看生成步骤。
3.4 用 SQL 把组件、类、属性串起来
界面上看到一个中文字段名,想知道它背后是哪个组件、哪个类、哪个属性,最快的办法是直接查元数据表。课程给的第一条 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 (1 = 1) and bt.DisplayName = '科目'; -- 换成要查的中文实体名几个参数说明:所有_Trl结尾的是翻译表,SysMLFlag = 'zh-cn'用来取中文显示名;where (1 = 1)是留的扩展位,追加条件时直接写and,不用管前面有没有条件;Discriminator用来区分继承层次,同一个类名下有多层实体时靠它分辨;AssemblyName告诉你这条元数据来自哪个 DLL,排查「改了没生效」时非常有用。
同理,枚举值也能反查。枚举在数据库里是三张表的关系:类型表、类型翻译表、值表。查某个枚举类型下所有值的语句如下:
-- 按枚举类型编码列出所有枚举值 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是枚举类型的完整命名空间,不是中文名,这一点和实体查询不同。查出来的EValue是落库值,NAME是显示值,两者对不上时界面就会显示成数字。
3.5 枚举同步:什么时候必须跑脚本,什么时候跑了白跑
枚举同步脚本是整份资料里最容易被误用的一段。课程给了一句非常关键的限定:以补丁包方式更新或安装,不需要跑下面的脚本,因为 BE 的 bulk 里会处理。反过来说,只有手工往库里灌元数据时才需要跑。
脚本的结构是四删四插:先清空ubf_sys_extenumtype、ubf_sys_extenumtype_trl、ubf_sys_extenumvalue、ubf_sys_extenumvalue_trl四张表,再从ubf_md_class(classtype = 3)和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 -- 类型表:从 classtype = 3 的类生成,后两列是审计用的账号与时间戳 insert into ubf_sys_extenumtype select local_id as id, '20060612', 'u9cust', '20060612', 'u9cust', 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 classtype = 3; -- 值表:从属性表生成,ExtEnumType 指向所属类型 insert into ubf_sys_extenumvalue select a.local_id as id, '20060612', 'u9cust', '20060612', 'u9cust', 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 as a inner join ubf_md_class as b on a.md_class_id = b.id where classtype = 3; -- 值翻译表 insert into ubf_sys_extenumvalue_trl select a.local_id as id, 'zh-CN', c.[displayname] as [name] from ubf_md_attribute as a inner join ubf_md_class as b on a.md_class_id = b.id left join ubf_RES_resourcevalue c on cast(a.[ID] as nvarchar(255)) = c.resourcename where classtype = 3; GO脚本里的'20060612'和'u9cust'是审计列,分别对应时间戳与操作账号,实际执行时换成当天日期和自己的账号即可,这两处不改不影响功能,但会影响后续排查是谁灌的数据。
最容易忽略的是翻译表。只插类型表和值表,枚举在界面上会显示成编码而不是中文,或者在多语言环境下直接空白。四张表是成对的:类型对类型翻译,值对值翻译,少一对就会出现「有的枚举值有中文、有的没有」的半残状态。
4. UI 层:事件顺序、控件挂载点与参照导航
界面层是代码量最大的部分,也是最讲究「放对位置」的部分。同一段逻辑放错事件,可能表现为「刷新时有值、点保存就丢」,也可能表现为「第一次打开正常、回发后失效」。这一章按 UIForm/Part/Page 的关系、事件执行顺序、常用挂载点、查找与导航四个角度铺开。
4.1 UI Code 与 Model 的对照:改哪一层,生成哪一层
UI 侧的分层比 BE 侧更容易混,因为界面上同时存在「界面单元」和「数据模型」两套对象。课程把 UI Code 与 Model 对比单列一节,就是为了先把这两层分开。
| 概念 | 代码里的写法 | 说明 |
|---|---|---|
| UIForm | this | 界面单元,事件都挂在它上面 |
| Model | this.Model.Subject | 数据模型,Webpart 里访问用 |
| Model 内字段 | this.Subject.FieldOrg | Model 层内部访问用 |
| Part | this.CurrentPart、wpFindID.ClientID | 页面区块,导航与弹窗要用 |
| 字段控件 | this.Status178、this.ApprovedBy101 | 自动生成的控件名,带数字后缀 |
两层的差别不只是写法。Model 层改字段要重新生成 UI 代码,控件名会跟着变;UIForm 层写的事件方法名是固定的Xxx_Click_Extend、Xxx_Extend这类约定名,改名框架就找不到。所以改 Model 要重新生成,改事件要守命名约定,两条规则不能混。
4.2 UIForm、Part、Page 的关系:三个 UID 各管一段
一条界面在元数据里是三个对象串起来的:UIForm 是界面的定义,Part 是装配单元,Page 是运行时页面,Page 再挂到某个应用(Application)下。搞不清这三者的关系,最直接的表现是:想做一个查找弹窗,找不到该传哪个 ID。
课程给的查询语句把这条链一次拉通:
-- UIForm → Part → Page → Application 的完整链路 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 (1 = 1) and at.DisplayName = '科目'; -- 换成目标界面的中文名查出来的UID(FormID)就是 4.5 节写查找弹窗时要传的那个 GUID,PartID(UIFormURI)是导航时用的 URI 片段。为什么不能靠记忆?因为这些 UID 是系统生成的,同一个界面的 FormID 在不同环境(开发库、测试库)里通常一致,但在重建元数据后可能变化,写死之前先查一遍最稳。
4.3 事件执行顺序:刷新与回发的分水岭
这一节是整份资料里信息密度最高的部分之一,课程直接给了一串事件名和它们的执行顺序,但没画图。按触发先后整理成表:
| 顺序 | 事件 | 触发时机 | 典型用途 |
|---|---|---|---|
| 1 | AfterEventBind | 事件绑定之后 | 绑定自定义事件 |
| 2 | AfterCreateChildControls | 控件创建之后 | 弹性域、个性化、删除提示、查找弹窗绑定 |
| 3 | OnLoadData_Extend | 刷新时执行,一般 PostBack 不执行 | 加载数据后的加工 |
| 4 | AfterOnLoad | 页面加载完成 | 依赖控件树已就绪的逻辑 |
| 5 | OnDataCollect_Extend | 刷新时不执行,PostBack 执行 | 数据收集、参数过滤 |
| 6 | BeforeUIModelBinding | 值绑定到控件之前 | 给参照加过滤条件 |
| 7 | AfterUIModelBinding | 值绑定到控件之后 | 按钮状态控制、只读控制 |
关键在于第 3 和第 5 条这一对:OnLoadData_Extend走刷新、不走回发,OnDataCollect_Extend反过来。默认值代码如果写进了OnDataCollect_Extend,第一次打开界面看起来正常(因为走的是刷新,取了别处的值),一按保存就变成空——这种「时好时坏」的问题,本质是事件挂错了位置,不是值没赋上。
课程里明确把「参数过滤代码」放在OnLoadData_Extend,就是因为这段代码只在首次加载时需要运行,回发时再跑一遍反而会覆盖用户的选择。反之,按钮状态、只读控制放在AfterUIModelBinding,因为要等数据绑定完才知道当前记录是什么状态。
4.4 常用挂载点:默认值、弹性域、复制与按钮状态
这一组代码是日常开发里复用率最高的,课程在 200 到 220 号视频里集中给了。按挂载位置归类,避免记混。
BE 层给组织赋默认值,写在OnSetDefaultValue:
// BE 实体 Extend -> OnSetDefaultValue // 新建时把当前登录组织带出来,避免表头组织为空 if (this.Org == null) { this.Org = Context.LoginOrg; }UI 层给组织赋默认值,写在 Model 的AfterInitModel。注意 Model 层一个组织要赋三个字段:
// Model -> AfterInitModel // 组织在 UI 侧是 ID / 编码 / 名称 三个字段,必须同步赋值 this.Subject.FieldOrg.DefaultValue = PDContext.Current.OrgRef.ID; this.Subject.FieldOrg_Code.DefaultValue = PDContext.Current.OrgRef.CodeColumn; this.Subject.FieldOrg_Name.DefaultValue = PDContext.Current.OrgRef.NameColumn;弹性域有两个挂载点,卡片和表格写法不同:
// Webpart -> AfterCreateChildControls() // 1) 卡片上的描述性弹性域:第一个参数是弹性域控件,第二个是它所在的 View FlexFieldHelper.SetDescFlexField(this.FlexFieldPicker0, this.Model.Subject); // 2) 表格(DataGrid)上的弹性域:用列索引 + 字段名 FlexFieldHelper.SetDescFlexField(this.DataGrid5, this.DataGrid5.Columns.Count - 1, "DescFlexField");SetDescFlexField的第二个参数在卡片场景下是 View 对象,把this.Model.Subject换成自己的 View 即可;在表格场景下换成了列索引和字段名,因为表格的弹性域是挂在最后一行上。这两种写法参数形式不同,照抄时最容易只改一半。
复制按钮的逻辑比较特别:在 Webpart 的按钮事件里写代码,框架会自动完成复制,只需要补上「清除不需要复制的值」。以编码为例,复制出来的单据编码必须清空,否则保存时报唯一性冲突:
// 复制按钮:清掉不该跟着复制的编号 this.Model.Subject.FocusedRecord.Code = string.Empty;个性化、删除提示、按钮状态、附件这几项也都是一两行的事,集中列一下:
// 开启个性化:AfterCreateChildControls UFIDA.U9.UI.PDHelper.PersonalizationHelper.SetPersonalizationEnable(this, true); // 删除按钮绑定确认提示:AfterCreateChildControls UFIDA.U9.UI.PDHelper.PDFormMessage.ShowDelConfirmDialog( this.Page, UFIDA.U9.UI.PDHelper.PDResource.GetDeleteConfirmInfo(), "", this.BtnDelete); // 弹出窗口 / 非弹出窗口的按钮控制:AfterUIModelBinding 中调用 private void SetControlStauts() <p> <a href="https://download.csdn.net/download/qq46128558/8204637" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>