

昨天那一层的产物是物理数据表,改一次要搬数据,还得先算要不要停机。今天回到界面这一侧,性质正好相反:界面要改,改的是一份定义,不必更版;而且改一次,所有前端一起变。
案例订单那张表单就是这样:桌面、浏览器、iOS、Android 四个端读同一份 FormLayout,各自长出自己的样子,没有一张界面是手写的。
本篇说明:
FormLayout的词汇为何刻意简化- 从一个字段到一个原生控件
- 各端各有一个渲染层
- 这一层之所以要独立,是因为它会过期
- 窄屏变形为何不写进定义
- 界面只有自动排版与手动排版两种,接上数据那一段两种都一样
一、FormLayout 的词汇刻意简化
一份 FormLayout 分成两种区块:
- 主表区块:可以有好几个,全部共用同一个列数
- 明细区块:可以有好几个,也可以一个都没有,一律整宽排在主表下面
主表区块里放字段,而布局对一个字段只交代这几样:属于哪一群、排在谁后面、要不要占宽一点、用哪一种控件、显不显示、唯不只读、必不必填。明细区块里放的是列,能交代的差不多同一组,多一个宽度。
布局不讲位置。整份定义找不到一个坐标,也没有「放在第几行第几列」这种话。位置是渲染层照顺序流出来的:字段一个接一个填,一行填满就换下一行,标了跨两列的就占两格。连那个列数都不是承诺,是上限,窄屏上会被压下来。
案例订单那份布局节选,完整文件是 Order.FormLayout.xml:
<LayoutSection Name="Main" Caption="Order">
<Fields>
<LayoutField FieldName="order_date" Caption="Order Date" />
<LayoutField FieldName="customer_rowid" Caption="Customer" ControlType="ButtonEdit"
DisplayFields="ref_customer_id,ref_customer_name" />
<LayoutField FieldName="total_amount" Caption="Total Amount" ReadOnly="true" />
</Fields>
</LayoutSection>
一份布局只回答四个问题:这张表单有哪些字段、什么顺序、分成哪几群、每个字段用哪一种控件。显不显示、唯不只读、必不必填是挂在字段上的状态,不改变形状。
词汇少是这一层的规格,不是它还没长大。布局不是最终要画出来的那一份,是中间那一份:它的下游不是某一种界面,是各端各自的排版机制,而那些机制彼此没有共同点(一边是桌面的排版控件,一边是浏览器的网格,下一代前端用什么现在还不知道)。要喂给这些下游的中介,词汇必须少到每一种都翻得动。
所以不在里面的东西:
- 摆放的事(对齐、边界、间距)
- 样式、模板与动画(那些是「画成什么样子」,归渲染层)
- 区块里再放区块(一份布局摊开来是平的,可以整批扫、可以程序化校验)
少掉的东西比留下的重要:一个被钉在某个像素位置的字段,换一个屏幕尺寸就坏了,而定义里若写得下这种话,坏掉的是上千份定义。
几百张界面长得一样
ERP、HRM、CRM 的用户几乎都是公司内部成员。他们不是挑一张界面来用,是被指派一整个模块,上线前要学会其中几十张、几百张表单。
界面若一张张手写,每张都可以长得不一样:这张明细在右边、那张在下面;这张挑客户是点放大镜开窗、那张是下拉菜单;这张的添加在表格上方、那张在右下角。每处差异单独看都不算什么,凑起来就是用户要记的东西。
布局的词汇少,这些差异就没有地方发生。用户要学的不是几百张界面,是一张界面加上几百组字段。上线训练的成本因此不跟表单数量成正比,而教育训练在导入预算里从来不是小数目,换一批人员、加一个部门还要再付一次。
代价:换到一致,就等于每一张界面都不会为它自己优化。某张使用频率特别高的表单,就算自己去调那份布局,也只能在这组词汇里排。划不划算取决于张数:十张界面各自优化是好事,几百张各自优化就是几百种操作习惯。
二、字段对应控件
字段若没有特别指定控件,控件种类就由 FormSchema 上的类型决定:
FormSchema 声明的类型 |
默认的控件种类 | 桌面端 | 网页端 |
|---|---|---|---|
| 布尔 | CheckEdit |
复选框控件 | 复选框输入元素 |
| 日期、日期时间 | DateEdit |
日期输入控件 | 日期输入元素 |
| 长文字 | MemoEdit |
多行文字控件 | 多行文字元素 |
| 短整数、整数、长整数、小数、金额 | NumericEdit |
数值输入控件 | 文字输入元素 |
| 其余 | TextEdit |
文字输入控件 | 文字输入元素 |
控件种类翻成各端的原生控件。上表右边两列就是,两边除了名字之外没有共同点。
挑控件还有一条规则比类型优先:字段若声明了跨表关联而没有特别指定控件,默认就是开窗选取的编辑器。Day 5 说同一段关联声明在界面这一侧也在做事,落点就是这里。这个控件一次绑住好几个字段:值落在关联键上,界面显示的是它带出来的那几个字段,选一次全部一起写回,所以那些显示列不会再各自占一格。
控件绑上去时回头读 FormSchema。文字控件去拿最大长度,下拉控件去拿选项清单,开窗选取的编辑器去拿它该开哪一支程序。布局因此只要记「用哪一种控件」,不必把字段语义再说一次。
跟 UI 框架有关的只有翻成原生控件那一段,而它整个落在渲染层里面。前后两段只跟定义打交道,换一个端一个字都不必动。LanguageResource 管文字,FormLayout 管排法,ViewModel 处理数据绑定。
三、同一份 FormLayout,各端各有一个渲染层
布局这一层不知道有 UI 框架这回事,这句话可以验证,方式跟 Day 4 那次一样:定义层的引用清单极短,任何一家 UI 框架都不在里面。
所以「翻成什么」全部发生在各端自己的渲染层里:
- 列数:布局上就是一个数字。桌面把它翻成排版控件上等宽的几列,网页把它翻成浏览器网格排版的一段样式声明。
- 清单:同一份清单布局,桌面交给表格控件去画,网页直接铺成一张网页表格。两边读的字段、顺序、宽度是同一份。
案例的四个前端就是这样组起来的:桌面、浏览器、iOS、Android 共用同一个 UI 项目,各平台入口只剩启动与平台外壳那几个文件。
「写一个渲染层」听起来很虚,拆开来是四件事:
- 把两种区块排出来
- 把控件种类对到这一家的原生控件
- 把控件接上 ViewModel
- 决定窄屏上要怎么变形
都不简单,但都是有限的,而且做完之后这一端就能跑所有表单。
清单上没有「怎么跟后端沟通」,那一段不必重写:每一个前端都经过同一层调用端程序,多一种前端框架,后端一行都不必跟着动(调用端那一层是 Day 17 的题目)。
四个端共用的那个渲染层就是 Bee.UI.Avalonia,连浏览器那一端也是同一套程序编译过去的。多支持一个端要付的是一个渲染层,不是零。
四、这一层之所以要独立,是因为它会过期
Day 1 那张对照表有一行是「换一代前端要付什么」,今天兑现它。
ERP/HRM 套装的产品周期十年、二十年起跳,这段时间里系统得跟着前端技术的世代更替长出不同样子。桌面程序、Web、移动 App,一代来的时候问题都是同一句:那几百张界面,要不要整批重做?
界面若一张张手写,答案只有重做,而且量跟表单数量成正比,跟业务有没有变一点关系都没有。一家公司的订单十年来只多了两个字段,界面却已经被重写过三次。
分层要解的就是这一题:结构稳定,跟着业务走;布局易变,跟着前端世代走。
两者的节奏差在支出的形状:
| 变动节奏 | 支出形状 | |
|---|---|---|
| 结构 | 连续 | 小额:一年多几个字段、改几条规则,每次动一两张表单 |
| 布局 | 间歇 | 大额:十年没事,然后一次全换 |
手写的界面把两种节奏绑在同一份文件里,那笔十年一次的大额支出因此每次都乘上表单总数;分开之后付的是一次固定成本。
这不是免费的。渲染层要有人写、要写对,而且它是一个收敛点:手写界面一次错一张,渲染层一次错全部。划不划算取决于表单有几张,上千张的系统差距大到不用算,十张的系统自己写一写还比较快。
排好的布局文件会不会也过期?会。差别在它是数据:改一份数据,跟改一份界面程序再重新编译、打包、部署,不是同一件事。
五、窄屏自己变形,不必另外排一份
多数做法是为手机另外排一份,于是同一张表单有两份布局,而两份会各自演进。
框架的做法是让渲染层自己变形:
- 单笔界面:主表列数压成一列;明细区块从就地编辑改成点一行、开一张行编辑表单
- 清单:宽的时候是一行一行的表格,窄的时候是一行一张卡片,每个字段在卡片上占一行,左标题右值
| iOS | Android |
|---|---|
![]() |
![]() |
卡片上那几个字段不是另外列的一份清单。宽版表格与窄版卡片读的是同一份清单布局,同一组列、同一个顺序,差别只在拿它去画成什么。
没有两份定义在互相同步,只有一份定义配两种画法,而这正是它最实际的价值:多一份就多一次对不起来的机会,而清单少一列这种事通常要等用户问「为什么手机上看不到金额」才会被发现。
这些变形的决定,一个字都不写在布局定义里。布局说的是有哪些字段、分成几列;「多窄算窄」「窄的时候明细要怎么编」是渲染层自己的判断。框架源代码在这里留了一句注释:明细怎么编是 UI 层的事,共用的布局定义维持中立。
界线画在这里有理由:布局定义一旦开始描述「手机上要怎样」,它就把一组关于屏幕的假设写进了定义。现在的假设是手机与桌面两种宽度,下一代可能是别的形状,而写进定义的假设要改就是改上千份。
代价:同一份布局在不同渲染层上不会长得完全一样。这是刻意的。要求各端像素级一致,等于要求各端都放弃自己的平台惯例,而用户对平台惯例的期待比对一致性的期待强得多。
六、标准订完了,弹性在哪里
Day 3 那个疑虑在这一层更尖锐,因为界面是用户天天看的东西。界面只有自动排版与手动排版两种,而接上数据那一段两种都一样。
| 方式 | 界面怎么来 | 适用 |
|---|---|---|
| 自动排版 | 走 FormLayout:布局在设计阶段先产出一份,之后照需要调整:分几个区块、几列、哪个字段跨两列、哪些不显示、哪一列只读、明细显示哪几列。它是数据,改完不必重新编译 |
制式表单,也就是绝大多数 |
| 手动排版 | 控件自己摆、位置自己决定,不经过布局那一层 | 布局那组词汇排不出来的形状 |
手动排版不是把框架关在门外。控件摆好之后填上数据表名与字段名,值的读写、变更通知、最大长度与选项清单这些就照样接得上。两种方式的差别只在界面是排出来的还是人手配置的,绑定那一段完全一样。
布局那组词汇不打算描述所有形状。它不能,也不该:仪表板、图表、拖拽式调度、地图这些走的是手动排版。布局是「一套什么都能做的框架等于什么决定都没替你做」这句话最容易被要求破例的地方:只要多加一种区块、多开一个样式属性,就能多盖住一种界面,而每加一个,每一个渲染层要对付的东西就多一样。
弹性的位置在颗粒度,不在能力。从自动排版到手动排版,中间没有断层,因为接数据那一段两边是同一套。
七、案例:订单那份布局说了什么、没说什么
案例的定义目录里有八份布局,一张表单一份,都是设计阶段产出之后再调整出来的。前面节选过的订单那一份说了这些:
- 主表一个区块、两列、一个明细区块
- 主表三个关联列与明细那一列,用的都是开窗选取的编辑器,带出来的显示列不再各自占一格
- 金额那一列在
FormSchema上标了只读,产出布局时一起带过来,界面上就是不给人填 - 四个端读同一份,长出四个平台各自的样子
订单那份布局没有说的事比说了的更多:没说每个字段最后落在界面什么位置、没说开窗选取的编辑器长什么样、没说手机上怎么排、没说标题要翻成哪一种语言、没说明细在窄屏上怎么编。正因为都不是它的职责,它才翻得到四个端上去。
那一天要重写的东西
一份布局定义长出四个前端的界面,这句话真正的内容不是「界面也能用定义描述」,能描述只是入场条件。它的内容是三件事的总和:
- 这一层的词汇很少,少到每一端都翻得动,也少到几百张表单长得一样
- 它不知道自己会被翻成什么,所以多一个端就是多一个渲染层,而不是回头改定义
- 它是整套定义里最容易过期的一份,而过期的时候被换掉的是渲染层,不是结构
十年后会出现什么样的前端我不知道,二十年前也没有人猜得到现在会长成这样。能先安排的只有一件事:让那一天到来的时候,要重写的东西是有限的,而且跟表单有几张无关。
明天谈算出来的字段与保存前的规则,能声明到什么程度。
本文为 2026 iThome 铁人赛参赛文章,原文为繁体中文。
系列完整目录:ERP 架构师笔记:定义驱动的框架设计
文章摘自:https://www.cnblogs.com/jeff-tsai/p/23518335/day-07-one-layout-many-uis


