java - Tapestry (5.7.2) - 通过 XHR 从内部组件刷新外部区域
问题描述
我们遇到了 Tapestry(版本 5.7.2)和组件区域刷新的问题。
我们有一个包含(分区)组件循环的页面,其中每个组件都有一个异步事件(参见代码)。
我们想要实现的是,通过 XHR 刷新,我们刷新了组件区域和页面包含的另一个区域,我们通过接口获得。
在此代码示例中,当我们单击第一个组件时,它会刷新区域,但会“忘记”@Persist 注释字段,该字段采用第二个组件的值。
如果我们点击第二个组件,它也会刷新第一个组件。
我们做错了什么?似乎微不足道的区域令人耳目一新,但我们没有得到它,我们尝试了不同的方法,但必须回退以不那么优雅的方式处理这部分。
页面代码:
@InjectComponent
private Zone listingZone;
@Persist
private String[] names;
@Property
private String lastRefreshZoneName;
@Property
private String _name;
Object onActivate() throws Exception {
names = new String[]{"first","second"}; //eg. loaded from DB
return null;
}
public LocalDateTime getTime() {
return LocalDateTime.now();
}
public String[] getNames() {
return names;
}
@Override
public Zone getOuterZone() {
return listingZone;
}
@Override
public void onTriggerOn(String name) {
lastRefreshZoneName = name;
}
使用这样一个简单的 TML:
<t:zone t:id="pageZone">
Page zone: ${time}<br/>
<hr/>
<t:loop source="names" value="name">
<t:attribute.zonedcomponent t:parameter="${name}"/>
</t:loop>
<hr/>
<t:zone t:id="listingZone">
Listing zone, last refresh: ${time}<br/>
Last refresh zone name: ${lastRefreshZoneName}
</t:zone>
</t:zone>
对于此示例, ZonedComponent 是具有区域和事件链接的组件,如下所示:
@Inject
private Request request;
@Inject
private AjaxResponseRenderer ajaxResponseRenderer;
@Inject
private ComponentResources resources;
@InjectComponent
private Zone componentZone;
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private String parameter;
@Persist
@Property
private String name;
void setupRender() {
this.name = parameter;
}
public LocalDateTime getTime() {
return LocalDateTime.now();
}
void onTrigger(String name) {
if (request.isXHR()) {
// this.name = name; // if we don't uncomment this, then it doesnt even propagate the 'name' correctly
SomeInterface page = (SomeInterface)resources.getPage();
page.onTriggerOn(this.name);
ajaxResponseRenderer.addRender(componentZone)
.addRender(page.getOuterZone());
}
}
public static interface SomeInterface {
ClientBodyElement getOuterZone();
void onTriggerOn(String name);
}
像这样的 tml 区域:
<t:zone t:id="componentZone" style="border:1px solid black">
Component name: ${name}<br/>
Component zone: ${time}<br/>
<t:eventlink t:event="trigger" t:context="${name}" async="true">
async event from ${name}
</t:eventlink>
</t:zone>
解决方案
从您的示例中可以学习三个关键点。
组件(服务器端)ID 与客户端 ID - 使用 Ajax 和区域时,组件事件处理程序需要知道客户端元素 ID。您可以简单地在模板文件中硬编码一个。但是,当在循环中使用组件时,id 不再是唯一的,并且事件处理程序无法知道要更新(不)哪个客户端元素。一种解决方案是使用
JavaScriptSupport
服务来分配客户端 ID。事件冒泡- 组件事件不必在它们被触发的组件内处理。它们实际上可以从嵌套组件“冒泡”到外部组件/页面。也可以在两个地方处理事件。这使您可以大大简化代码:无需获取包含组件/页面并调用您必须引入以使其工作的接口的方法。有关更多详细信息,请参阅 Tapestry 文档中组件事件页面上的事件冒泡部分。
组件参数- 组件参数具有 Java 类型。传递参数值时,只需引用一个属性。
${...}
仅在需要将表达式转换为字符串的情况下使用该语法。请参阅“不要使用 ${...} 语法!”部分 在Tapestry 文档的组件参数页面上。
了解了上述内容后,您的示例可以重写如下。
页面类:
@Property
private String[] names;
@Property
private String name;
@Property
private String lastRefreshZoneName;
@Inject
AjaxResponseRenderer ajaxResponseRenderer;
@InjectComponent
private Zone listingZone;
void onActivate() {
names = new String[] { "first", "second" }; // eg. loaded from DB
}
public LocalDateTime getTime() {
return LocalDateTime.now();
}
public void onTrigger(String name) {
lastRefreshZoneName = name;
ajaxResponseRenderer.addRender(listingZone);
}
页面模板:
Page rendered at: ${time}
<br />
<hr />
<t:loop source="names" value="name">
<t:zonedComponent t:name="name" />
</t:loop>
<hr />
<t:zone t:id="listingZone" id="listingZone">
Listing zone, last refresh: ${time}
<br />
Last refresh zone name: ${lastRefreshZoneName}
</t:zone>
组件类:
@Parameter(defaultPrefix = BindingConstants.PROP)
@Property
private String name;
@Inject
private Request request;
@Inject
private AjaxResponseRenderer ajaxResponseRenderer;
@InjectComponent
private Zone componentZone;
@Inject
JavaScriptSupport jsSupport;
@Inject
private ComponentResources resources;
@Property
String clientId;
void setupRender() {
clientId = jsSupport.allocateClientId(resources);
}
public LocalDateTime getTime() {
return LocalDateTime.now();
}
boolean onTrigger(String name, String clientId) {
if (request.isXHR()) {
// Since the fields were cleared after the original rendering of the
// page, they need to be assigned again so that they are available
// when the XHR response is rendered.
this.name = name;
this.clientId = clientId;
// Queue only this component's zone. The containing page takes care
// of its one zone.
ajaxResponseRenderer.addRender(componentZone);
}
return false; // Bubble up: allow containing page/component to do some
// more handling
}
}
组件模板:
<t:zone t:id="componentZone" id="${clientId}" style="border:1px solid black" >
Client id: ${clientId}
<br />
Component name: ${name}
<br />
Component zone: ${time}
<br />
<t:eventlink t:id="link" t:event="trigger" t:context="[name,clientId]" async="true">
async event from ${name}
</t:eventlink>
</t:zone>