首页 > 解决方案 > 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>

标签: javatapestry

解决方案


从您的示例中可以学习三个关键点。

  1. 组件(服务器端)ID 与客户端 ID - 使用 Ajax 和区域时,组件事件处理程序需要知道客户端元素 ID。您可以简单地在模板文件中硬编码一个。但是,当在循环中使用组件时,id 不再是唯一的,并且事件处理程序无法知道要更新(不)哪个客户端元素。一种解决方案是使用JavaScriptSupport服务来分配客户端 ID。

  2. 事件冒泡- 组件事件不必在它们被触发的组件内处理。它们实际上可以从嵌套组件“冒泡”到外部组件/页面。也可以在两个地方处理事件。这使您可以大大简化代码:无需获取包含组件/页面并调用您必须引入以使其工作的接口的方法。有关更多详细信息,请参阅 Tapestry 文档中组件事件页面上的事件冒泡部分。

  3. 组件参数- 组件参数具有 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>

推荐阅读