XML界面
Moqui中的页面以两种方式组织:
- 每个页面存在于一个子页面的层级中
- 一个页面可能是图中的一个节点,通过转换与其他节点连接
层级模型被用来引用页面,在一个URL中通过在层级中的路径来指定渲染哪个页面。页面也包含到其他页面的链接(一个超链接或一个表单提交),更像在一个图中通过转换从一个节点去到另一个的结构。
子页面
子页面层级主要被用来动态包括其他的页面、子页面或儿子页面。一个页面的子页面也能被用来生成菜单。
当页面被渲染时,它有一个根页面和一个页面名列表。
根页面在Moqui配置XML文件中用 moqui-conf.webapp-list.webapp.root-screen 元素为每个 webapp 配置。可为每个webapp基于主机名模式配置多个根页面,提供了在单个webapp内的虚拟主机的简便方法。
你应该至少有一个包罗所有的根页面元素,即主机名host被设置为正则表达式"."。MoquiDefaultConf.xml文件使用默认的*webroot组件,你可以在runtime或组件的Moqui配置XML文件中覆盖它的根页面。
如果子页面名字列表没有到达一个叶子页面(没有子页面),那么将使用screen.subscreens.default-item属性指定的默认的子页面。
有4种方式把子页面加到一个页面:
- 目录结构 : 对于单个应用中的页面,通过目录结构: 在父页面所在目录创建一个与父页面文件名相同名字(不要.xml后缀)的文件夹,并把XML页面文件放入那个文件夹。(name=文件名除去.xml,title=screen.default-title,location=父页面的minus文件名 + / + 子页面的文件名)
- 页面XML文件 : 要包含其他应用或共享的不在任一应用中的页面,用screen.subscreens元素下的subscreens-item元素
- 数据库记录 : 要增加、移除页面树中任意位置的子页面,或修改子页面的顺序、主题,使用 moqui.screen.SubscreensItem 实体中的记录
- Moqui配置XML文件 : 基于数据库记录实现方式的配置文件替代,无需修改被挂载的页面,就能在其下添加子页面;在组件目录中的MoquiConf.xml内,能在 screen-facade 元素下放置任意Moqui配置XML设置,包括 screen.subscreens-item 元素。
1) 目录结构 对于第1种(目录结构),一个目录结构看起来像是这样(从Example应用):
ExampleApp.xml
ExampleApp
Feature.xml
Feature
FindExampleFeature.xml
EditExampleFeature.xml
Example.xml
Example
FindExample.xml
EditExample.xml
要注意的模式是,如果这是一个子页面,应该有一个目录,目录名和XML页面文件名一样,只是没有.xml扩展。Feature.xml文件是一个有子页面的例子,FindExampleFeature.xml没有子页面(页面层级的叶子)。
2) 页面XML文件
对于第2种实现(页面XML文件),subscreens-item元素看起来会像是这个来自appx.xml文件用以挂载Example应用的根页面的元素:
<subscreens-item name="example" menu-title="Example" menu-index="8"
location="component://example/screen/ExampleApp.xml"/>
3) 数据库记录
对于第3种(数据库记录),数据库中SubscreensItem实体中的记录看起来会像这样(上面XML元素的一个改编):
<moqui.screen.SubscreensItem subscreenName="example" userGroupId="ALL_USERS"
menuTitle="Example" menuIndex="8" menuInclude="Y"
screenLocation="component://webroot/screen/webroot/apps.xml"
subscreenLocation="component://example/screen/ExampleApp.xml"/>
4) Moqui配置XML文件
对于第4种(Moqui配置XML文件),你可以把这些元素放入任何将被合并到运行时配置的Moqui配置XML文件。主要的方式是,在组件目录中的MoquiConf.xml文件中做这些,这样配置和页面在同一个组件中,不必修改和维护其他地方的文件。关于Moqui配置XML选项的更多细节请查看运行和部署章节。这有一个来自moqui/example组件中的MoquiConf.xml的例子:
<screen-facade>
<screen location="component://webroot/screen/webroot/apps.xml">
<subscreens-item name="example" menu-title="Example" menu-index="97"
location="component://example/screen/ExampleApp.xml"/>
</screen>
</screen-facade>
在widgets(visual元素)部分内,使用subscreens-active元素指定在哪渲染活跃子页面。你也能用subscreens-menu元素指定所有子页面的菜单位置。对于单个元素,使用subscreens-panel元素同时指定子页面和菜单位置。
到一个页面的全路径将总是明确的,在每个页面下有一个默认的子页面的同时,可以在条件下有多个默认项。在 webroot.xml 页面中有一个对于iPad默认去另一个子页面的例子:
<subscreens default-item="apps">
<conditional-default item="ipad"
condition="(ec.web.request.getHeader('User-Agent')?:'').matches('.*iPad.*')">
</subscreens>
有了这个,一个页面路径如果明确指定,那么将去到"apps"子页面或"ipad"子页面;但是如果不明确,那么如果User-Agent匹配,将默认去到 ipad.xml,否则默认去到平常的 apps.xml 子页面。这两个下面都有example和tools页面层级,但是为了满足不同的平台的要求,HTML和CSS有轻微的区别。
一旦一个例如FindExample的页面通过这两个之一被渲染,它的链接将在URL中保留相对路径外的基础的页面路径,这样用户将留在原始默认指向的路径。
独立(Standalone)页面
通常页面将按渲染路径从根页面开始被渲染。路径上的每个页面被加入到输出。一个页面沿着渲染路径,在路径中前面没有加入到输出的页面,则是一个“独立页面”。
当你想要这一个页面控制它的所有输出,不使用子页面层级中其父页面的标头、菜单、脚标等等。
有两种方式使得页面独立:
- 设置 screen.standalone 属性为true,使得页面总是独立
- 要在独立地渲染任意页面,传入lastStandalone=true参数,或在一个页面的pre-action(screen.pre-actions元素下的动作)设置。
第一个选项对应用中独立于其他部分、并且需要不同的装饰等的根页面很有用。第2个选项对于有时被用在一个应用的上下文并且其他实际被用来生成像CSV文件这样未加装饰的输出,或者在一个对话窗口、页面章节中动态加载时很有用。
页面转换
一个转换作为页面的一部分,定义如何从一个页面到另一个,同时如果可以如何处理输入。一个转换当然能回到同一个页面,并且经常要处理输入。
转换中的逻辑(转换动作)应当仅用作处理输入,而不是为了显示而准备数据。那是页面动作的事情。对应的,页面动作不应该被用来处理输入(下面详述)。
当一个XML页面在一个web应用中运行,在URL中转换跟在页面之后。在任何上下文中,转换条目在子页面路径元素的最后。例如下面:第一个路径去到EditExample页面,第二个去到页面内的updateExample转换:
/apps/example/Example/EditExample
/apps/example/Example/EditExample/updateExample
当一个转换是HTTP请求的目标,与此转换相关的动作都将被运行,然后发一个重定向请求到HTTP客户端(一般是一个web浏览器),以去到转换指向的页面的URL。如果转换没有逻辑,并指向另一个页面或外部URL,当生成一个去那个转换的链接时,将会自动去到那另一个页面或外部URL,跳过调用转换。这些主要只应用于运行在一个web应用中的XML页面。
一个从一个页面到另一个页面的简单转换,此例中从FindExample到EditExample,看起来像这样:
<transition name="editExample">
<default-response url="../EditExample"/>
</transition>
url属性中的路径基于两个页面的位置,在同一个父页面的兄弟页面。在这个属性中一个点号(".")指向当前页面,两个点号("..")指向父页面,遵循和Unix文件路径一样的模式。
对于有输入处理的页面,使用的最佳模式是转换调用一个服务。用这种实现,服务被定义为接受提交到对应转换的表单。这使得两者的设计更清晰并且提供了像“服务定义上的某些校验被用来生成匹配的客户端校验”这样的好处。当一个service-call元素被直接放在transition元素下时,和它在actions块中有一点点不同,它自动从上下文中获得入参(等于in-map="context"),并将出参放入上下文(等于out-map="context")。
此类转换看起来像这样(EditExample页面上的updateExample转换):
<transition name="updateExample">
<service-call name="org.moqui.example.ExampleServices.update#Example"/>
<default-response url="."/>
</transition>
此例中,default-response.url属性是简单的一个点号,指向当前页面并意味着在转换被处理后,它将去到当前页面。
一个页面转换通过使用actions元素也能用动作替代服务调用。如果转换既有服务调用又有actions元素,将先运行service-call,然后运行actions。就像Moqui中的所有XML文件中的所有actions元素,子元素是标准的Moqui XML Actions,被转换为Groovy脚本。有actions的页面转换看起来像是这样(简化后的例子,也来自FindExample页面):
<transition name="getExampleTypeEnumList">
<actions>
<entity-find entity-name="..." list="...">
<econdition field-name="..." from="..."/>
<order-by field-name="..."/>
</entity-find>
<script>ec.web.sendJsonResponse([exampleTypeEnumList:exampleTypeEnumList])</script>
</actions>
<default-response type="none"/>
</transition>
此例也显示了你应当如何执行一个简单的实体查找操作并返回JSON响应结果到HTTP客户端。注意对ec.web.sendJsonResponse()方法的调用和default-response.type属性的none值告诉它不要处理任何额外的响应。
如default-response所暗示的,你也能用conditional-response元素有条件地选择一个响应。此元素是可选的,你可以指定任意多个,尽管你应当总是至少有一个default-response元素在没有条件满足时使用。也有一个可选的error-response,你可以用来指定在转换动作中出错的情况下的响应。
有一个 condition-response 的转换看起来会像这个来自DataExport页面的简化的例子:
<transition name="EntityExport.xml">
<actions>
<script><![CDATA\[if (...) noResponse = true]]></script>
</actions>
<conditional-response type="none">
<condition>
<expression>noResponse</expression>
</condition>
</conditional-response>
<default-response url="."/>
</transition>
这允许脚本指定不发送响应(当它发回导出的数据时),否则它转回当前页面。注意在conditon.expression下的文本是简单的一个Groovy表达式,其值为布尔值。
所有的 *-response 元素可以有parameter子元素,在重定向到url或目标页面的激活时被用到。每个页面有一个期望参数的列表,在需要覆盖参数的来源(默认定义在页面下的parameter标记中)的地方或传递额外参数时才需要。
这是 default-response,conditional-response和error-response元素共有的属性:
- type: 默认为url,可以为:
- none: 没有响应,除了转换动作外不做其他事
- screen-last: 如果没有一个保存自之前的某个请求的页面(使用save-current-screen属性,对于登录请求会自动设置),则去到来自最后一个请求的页面。如果没有发现最后一个页面,将会使用url中发现的值;如果也没有,则会去到默认页面(就是去到为每个子页面设置的根页面)。
- screen-last-noparam: 像 screen-last,但是不会传递任何参数
- url: 重定向到url-type的url属性中指定的URL
- url:基于 url-type,响应中要跟随重定向到的URL。默认url-type是页面路径,意味着从当前页面到期望页面、转换或子页面内容的路径值
- 使用"."来表示当前页面,".."表示在运行时页面路径的父页面。".."可以出现多次,例如"../.."是父页面的父页面(祖父页面)。如果screen-path类型url以"/"开始,将是相对于根页面,而不是相对于当前页面。
- 如果url-type是plain,那么这可以是任何有效的URL(相对于当前域名或绝对的)
- url-type: 可以是screen-path(默认)或plain。一般响应会去到另一个页面,因此screen-path是默认的,但是如果你想去到一个相对或绝对URL,使用plain类型
- parameter-map: 就像parameter子元素,可被用来指定重定向时传递的参数
- save-current-screen: 保存当前页面的路径和参数以待后续使用,一般和screen-last类型的响应一起使用。
- save-parameters: 在执行重定向前保存当前参数(和请求属性),这样在重定向后渲染的页面的上下文就像转换的原始请求一样。
参数和web设置
页面定义中的首要事情之一是传递到页面的参数。这被用在构建一个URL以链接到此页面或为页面渲染准备上下文时。使用parameter元素来做,看起来像是这样:
<parameter name="exampleId"/>
name这一个属性是必须的,其他属性是可选的。如果你想要一个默认的静态值,可以用value属性;如果默认从上下文中的一个字段取值,而不是从匹配参数名的参数中取,用from属性。
参数应用到所有渲染模式的同时,有些设置只应用到页面在一个基于web的应用中被渲染时。这些选项在screen.web-settings元素上,包括:
- allow-web-request: 默认为true。设置为false不允许通过HTTP客户端访问
- require-encryption: 默认为true。为不用那么安全并且不需要加密(也就是HTTPS)的页面设置为false。
- mime-type:默认为 text/html。基于页面如何被展示(渲染模式)而不同,但是当总是产出一个确定类型的输出时,在这里设置相应的mime type。
- character-encoding: 对于文本输出默认为UTF-8。如果用一个不同的编码来渲染文本,在这里设置。
页面动作、前置动作(Pre Actions)和总是执行的动作(Always Actions)
在渲染页面的可视元素(小部件)之前,screen.actions元素下的XML动作完成了数据的准备。同样的XML动作被用于服务和其他工具,在 逻辑和服务 章节中有描述。这些动作是运行服务和脚本的元素(inline的Groovy或通过ResourceFacade支持的任意类型的脚本),它们执行基本的实体和数据移动操作等等。
页面动作应当只被用作为输出准备数据。使用转换动作来处理输入。
当页面被渲染时,它的顺序是按在界面路径中被发现的顺序,每个页面的动作在列表中的每个页面被渲染时运行。要在路径中的第一个页面被渲染之前执行动作,使用pre-actions元素。这主要被用于准备页面需要的数据,页面包括当前页面(即页面路径中的当前页面之前)。当使用时,留意一个页面可以被不同环境中的不同的页面包括。
如果你想动作在页面渲染之前执行,并且在任何转换被运行之前,那么使用 always-actions 元素。always-actions和pre-actions之间的主要区别是pre-actions只是在一个页面或子页面被渲染之前运行,而always-actions将在当前页面中的任何转换和任何子页面中的任何转换前执行。不管页面是否将被渲染,always-action都会执行,而pre-actions只在页面将被渲染前执行(即路径中的独立页面之下)。
XML页面Widget
screen.widgets元素下的元素是被渲染的可视元素,或在产出文本时实际产出输出文本的元素。最常见的widget是XML表单(使用form-single和form-list元素)和被包含的模板。关于XML表单的细节见后续章节。
XML表单不是特定于任何渲染模式的,而模板天生的是特定于一个特定的渲染模式的。这意味着要支持多种类型的输出,你将需要多个模板。webroot.xml页面(默认根页面)有一个包含不同渲染模式的多个模板的例子:
<render-mode>
<text type="html"
location="component://webroot/screen/includes/Header.html.ftl"/>
<text type="xsl-fo" no-boundary-comment="true"
location="component://webroot/screen/includes/Header.xsl-fo.ftl"/>
</render-mode>
同样的页面也有一个用内联文本的支持多种渲染模式的例子:
<render-mode>
<text type="html"><![CDATA[</body></html>]]></text>
<text type="xsl-fo">
<![CDATA[</fo:flow></fo:page-sequence></fo:root>]]></text>
</render-mode>
这些是显示基本的东西的widget元素:
- link: 去一个转换、其他页面或任意URL的一个超链接
- image: 显示一个图片
- label: 显示一些文本
要设定页面的结构,使用这些widget元素:
- section: 页面的一个命名部分,有条件(condition)、动作(action)、小部件(widget)和fail-widget(当条件值为false时运行)
- section-iterate:像section,但是是遍历一个集合中的每个条目
- container: 页面的一个区域
- container-panel: 页面的一个区域,此结构有页头、页脚,在页头、页脚之间有左、中、右三格
- container-dialog: 刚开始是隐藏的,但按钮被按下时弹出的一个页面区域
- dynamic-dialog: 一个按钮和一个弹出框的占位符,通过当前页面的一个转换从服务器加载弹出框的内容
- include-screen: 如字面意思,包含另一个页面
Section、Condition和Fail-Widget
section是一个包含其他小部件的特殊的小部件。它能被用在其他页面部件元素被用在的任何地方。一个section有widgets、condition和fail-widgets子元素。screen元素也支持这些子元素,使得它是页面的一种顶层部分。
condition元素被用来指定一个条件。如果求值为true,widgets元素下的小部件将会被渲染,如果为false,那么将会渲染fail-widgets元素下的小部件。
宏模板和自定义元素
Moqui的XML页面和XML表单文件被用一个Freemarker(FTL)模板文件中的一套宏转化为期望的输出。当屏幕被渲染时,每个XML元素都有一个宏来生成它的输出。
有两种方式来指定用于渲染页面的宏模板:
- 为所有页面:Moqui配置XML文件中的moqui-conf.screen-facade.screen-text-output.macro-template-location属性;对应每个渲染模式(即html、xml、csv、xsl-fo等)都有一个screen-text-output元素,由screen-text-output.type属性标识。
- 为单个页面:screen.macro-template.location属性;你也可以为每个渲染模式指定一个macro-template元素,由macro-template.type属性标识。
宏模板的位置可以是任何Resource Facade支持的位置。你将使用的最常见的位置类型包括component、content和runtime目录。
Moqui包含的默认的宏模板在MoquiDefaultConf.xml中指定,还带着所有其他默认设置。你可以在运行时指定的Moqui配置XML文件中用你自己的覆盖它们。
当你使用一个自定义的宏模板文件,你不需为每个你想区别渲染的元素包括一个宏。你可以在文件开始包含一个你想使用的默认宏文件或任意其他的宏文件,然后为期望的元素覆盖宏。在你的文件中包括另一个宏文件,看起来会像是这样:
<#include "classpath://template/DefaultScreenMacros.html.ftl"/>
这里的位置可以是ResourceFacade支持的任意位置。
你可以使用这个方法来添加你自己的自定义元素。换句话说,你自己自定义宏模板文件中的宏,不必是Moqui中已有元素的覆盖,它们可以是你想的任何东西。
使用此方法来添加你自己的想在你应用中的各个页面保持一致的小部件元素和表单字段类型。例如你可以用像默认宏中的对话框那样的动态HTML为特定的容器添加宏,或一个特殊的像幻灯片(slide)的表单字段,或用JavaScript创建的自定义表单字段小部件。
当你为自定义元素添加宏时,你可以开始在你的XML页面文件中使用,即使它们没有被XSD文件校验。如果你想它们被校验:
- 创建你自己的自定义XSD文件
- 包含一个或多个Moqui默认的XSD文件
- 添加你的元素定义到自定义XSD
- 在你的XML页面文件的screen.xsi:noNamespaceSchemaLocation属性中引用你的自定义XSD文件
CSV、XML、PDF和其他页面输出
因为单个XML页面文件能支持多种渲染模式的输出,使用页面的 renderMode 参数来选择使用什么渲染模式。对于基于web的应用,这可以是一个URL参数。对于任意应用,这可以在页面动作中被设置,一般是一个pre-action(即在 screen.pre-actions 元素下)。
此参数的值可以是匹配Moqui配置XML文件中screen-text-output.type属性的任意字符串。这既包括Moqui自带的类型,也包括你在运行时配置文件中添加的任意类型。
在渲染路径中的所有页面被不管渲染模式地渲染,所以如果你只想输出路径中最后一个页面的内容(像CSV),除了renderMode参数外,使用lastStandalone=true参数。