Day 7:FormLayout 与多前端界面生成


昨天那一层的产物是物理数据表,改一次要搬数据,还得先算要不要停机。今天回到界面这一侧,性质正好相反:界面要改,改的是一份定义,不必更版;而且改一次,所有前端一起变。

案例订单那张表单就是这样:桌面、浏览器、iOS、Android 四个端读同一份 FormLayout,各自长出自己的样子,没有一张界面是手写的。

本篇说明:

  1. FormLayout 的词汇为何刻意简化
  2. 从一个字段到一个原生控件
  3. 各端各有一个渲染层
  4. 这一层之所以要独立,是因为它会过期
  5. 窄屏变形为何不写进定义
  6. 界面只有自动排版与手动排版两种,接上数据那一段两种都一样

一、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 项目,各平台入口只剩启动与平台外壳那几个文件。

「写一个渲染层」听起来很虚,拆开来是四件事:

  1. 把两种区块排出来
  2. 把控件种类对到这一家的原生控件
  3. 把控件接上 ViewModel
  4. 决定窄屏上要怎么变形

都不简单,但都是有限的,而且做完之后这一端就能跑所有表单。

清单上没有「怎么跟后端沟通」,那一段不必重写:每一个前端都经过同一层调用端程序,多一种前端框架,后端一行都不必跟着动(调用端那一层是 Day 17 的题目)。

四个端共用的那个渲染层就是 Bee.UI.Avalonia,连浏览器那一端也是同一套程序编译过去的。多支持一个端要付的是一个渲染层,不是零。


四、这一层之所以要独立,是因为它会过期

Day 1 那张对照表有一行是「换一代前端要付什么」,今天兑现它。

ERP/HRM 套装的产品周期十年、二十年起跳,这段时间里系统得跟着前端技术的世代更替长出不同样子。桌面程序、Web、移动 App,一代来的时候问题都是同一句:那几百张界面,要不要整批重做?

界面若一张张手写,答案只有重做,而且量跟表单数量成正比,跟业务有没有变一点关系都没有。一家公司的订单十年来只多了两个字段,界面却已经被重写过三次。

分层要解的就是这一题:结构稳定,跟着业务走;布局易变,跟着前端世代走。

两者的节奏差在支出的形状:

变动节奏 支出形状
结构 连续 小额:一年多几个字段、改几条规则,每次动一两张表单
布局 间歇 大额:十年没事,然后一次全换

手写的界面把两种节奏绑在同一份文件里,那笔十年一次的大额支出因此每次都乘上表单总数;分开之后付的是一次固定成本。

这不是免费的。渲染层要有人写、要写对,而且它是一个收敛点:手写界面一次错一张,渲染层一次错全部。划不划算取决于表单有几张,上千张的系统差距大到不用算,十张的系统自己写一写还比较快。

排好的布局文件会不会也过期?会。差别在它是数据:改一份数据,跟改一份界面程序再重新编译、打包、部署,不是同一件事。


五、窄屏自己变形,不必另外排一份

多数做法是为手机另外排一份,于是同一张表单有两份布局,而两份会各自演进。

框架的做法是让渲染层自己变形:

  • 单笔界面:主表列数压成一列;明细区块从就地编辑改成点一行、开一张行编辑表单
  • 清单:宽的时候是一行一行的表格,窄的时候是一行一张卡片,每个字段在卡片上占一行,左标题右值
iOS Android

卡片上那几个字段不是另外列的一份清单。宽版表格与窄版卡片读的是同一份清单布局,同一组列、同一个顺序,差别只在拿它去画成什么。

没有两份定义在互相同步,只有一份定义配两种画法,而这正是它最实际的价值:多一份就多一次对不起来的机会,而清单少一列这种事通常要等用户问「为什么手机上看不到金额」才会被发现。

这些变形的决定,一个字都不写在布局定义里。布局说的是有哪些字段、分成几列;「多窄算窄」「窄的时候明细要怎么编」是渲染层自己的判断。框架源代码在这里留了一句注释:明细怎么编是 UI 层的事,共用的布局定义维持中立。

界线画在这里有理由:布局定义一旦开始描述「手机上要怎样」,它就把一组关于屏幕的假设写进了定义。现在的假设是手机与桌面两种宽度,下一代可能是别的形状,而写进定义的假设要改就是改上千份。

代价:同一份布局在不同渲染层上不会长得完全一样。这是刻意的。要求各端像素级一致,等于要求各端都放弃自己的平台惯例,而用户对平台惯例的期待比对一致性的期待强得多。


六、标准订完了,弹性在哪里

Day 3 那个疑虑在这一层更尖锐,因为界面是用户天天看的东西。界面只有自动排版与手动排版两种,而接上数据那一段两种都一样。

方式 界面怎么来 适用
自动排版 走 FormLayout:布局在设计阶段先产出一份,之后照需要调整:分几个区块、几列、哪个字段跨两列、哪些不显示、哪一列只读、明细显示哪几列。它是数据,改完不必重新编译 制式表单,也就是绝大多数
手动排版 控件自己摆、位置自己决定,不经过布局那一层 布局那组词汇排不出来的形状

手动排版不是把框架关在门外。控件摆好之后填上数据表名与字段名,值的读写、变更通知、最大长度与选项清单这些就照样接得上。两种方式的差别只在界面是排出来的还是人手配置的,绑定那一段完全一样。

布局那组词汇不打算描述所有形状。它不能,也不该:仪表板、图表、拖拽式调度、地图这些走的是手动排版。布局是「一套什么都能做的框架等于什么决定都没替你做」这句话最容易被要求破例的地方:只要多加一种区块、多开一个样式属性,就能多盖住一种界面,而每加一个,每一个渲染层要对付的东西就多一样。

弹性的位置在颗粒度,不在能力。从自动排版到手动排版,中间没有断层,因为接数据那一段两边是同一套。


七、案例:订单那份布局说了什么、没说什么

案例的定义目录里有八份布局,一张表单一份,都是设计阶段产出之后再调整出来的。前面节选过的订单那一份说了这些:

  • 主表一个区块、两列、一个明细区块
  • 主表三个关联列与明细那一列,用的都是开窗选取的编辑器,带出来的显示列不再各自占一格
  • 金额那一列在 FormSchema 上标了只读,产出布局时一起带过来,界面上就是不给人填
  • 四个端读同一份,长出四个平台各自的样子

订单那份布局没有说的事比说了的更多:没说每个字段最后落在界面什么位置、没说开窗选取的编辑器长什么样、没说手机上怎么排、没说标题要翻成哪一种语言、没说明细在窄屏上怎么编。正因为都不是它的职责,它才翻得到四个端上去。


那一天要重写的东西

一份布局定义长出四个前端的界面,这句话真正的内容不是「界面也能用定义描述」,能描述只是入场条件。它的内容是三件事的总和:

  • 这一层的词汇很少,少到每一端都翻得动,也少到几百张表单长得一样
  • 它不知道自己会被翻成什么,所以多一个端就是多一个渲染层,而不是回头改定义
  • 它是整套定义里最容易过期的一份,而过期的时候被换掉的是渲染层,不是结构

十年后会出现什么样的前端我不知道,二十年前也没有人猜得到现在会长成这样。能先安排的只有一件事:让那一天到来的时候,要重写的东西是有限的,而且跟表单有几张无关。

明天谈算出来的字段与保存前的规则,能声明到什么程度。


本文为 2026 iThome 铁人赛参赛文章,原文为繁体中文。

iThome 原文 | HackMD 版

系列完整目录:ERP 架构师笔记:定义驱动的框架设计

文章摘自:https://www.cnblogs.com/jeff-tsai/p/23518335/day-07-one-layout-many-uis