使用 netui:repeater 标签进行分页和排序

 开发当今复杂 web 应用程序的开发人员所面临的一项常见任务是向用户显示数据,而这些任务的一个标准用户界面要求是使用户能够对数据进行排序和分页。分页的要求源于这样一个事实:通常,用户搜索的数据比在单页上能够合适显示的数据要多。因此,首先需要向用户展示结果数据的一个初始子集,并具有能够对剩余数据进行导航的能力。另一个常见的要求是排序,即允许用户以他们最感兴趣的顺序来查看数据。

 

  当实现这类解决方案时,通常会碰到的一个技术问题是:如何高效地执行所有这些任务,同时不用把数据集装载到内存中。本文说明了如何使用 netui:repeater 标签来实现这个目标。

  BEA WebLogic Workshop 8.1™ 为迭代和显示数据提供了两个不同的标签。它们分别是 netui:grid 和 netui:repeater 标签。 netui:grid 标签为处理分页和排序提供开箱即用的功能,而 netui:repeater 标签没有提供该功能。了解这一点之后,那么当您碰到需要对数据进行分页和排序时,始终使用 netui:grid 标签。

  不幸的是, netui:grid 标签具有许多限制,这些限制使它并非在所有情况下都有用。 netui:grid 标签的第一个限制是,开发人员对于每个单元中内容显示的控制非常少。例如,当数据库列中包含缩写的值,但是您想显示完整的值时,您会发现使用 netui:grid 标签无法做到。

  netui:grid 标签的第二个限制是,它要求只能绑定到 RowSet 中 。 Workshop 数据库控件所使用的 RowSet 具有的问题是,它对于较大数据集的效果不好,因为它会把数据的每一项都加载到内存中。例如,如果数据包含 1000 行, RowSet 将会把每一行的值都加载到内存,即便您只对开始 10 行感兴趣。另一方面, ResultSet只加载明确请求的数据。

  由于包含这两个限制,在使用 netui:grid 标签实现与 netui:repeater 标签提供的相同分页和排序功能时,开发人员通常要大费周章。开发人员可以采用不是特别可重用的代码,或者采用需要在页面流之间进行大量复制和粘贴的代码,才能使 netui:repeater 的分页和排序正确工作。

  本文的目的是举例说明如何有效且高效地使用 netui:repeater 标签处理分页和排序,同时在不牺牲性能的情况下重点强调代码的可重用性和灵活性。尽管本文使用了 WebLogic Workshop 数据库控件作为数据预取机制来说明如何实现目标,此处介绍的代码和技术可以轻松地用于处理来自其他数据源的数据。

一个例子

  根据我的经验,说明一种解决方案的最佳方式就是使用例子。出于本文的目的,我们将构建一个屏幕,在 portlet 中显示一份联系人清单。运行这个例子所需的所有代码都包含在本文中。下面显示了这个示例的一个工作画面。

Example in Action

  这个例子演示了该解决方案如何实现了一组可以在应用程序中的不同页面流和 JSP 页面之间轻松重用的标准功能。这些功能包括标准化的动作按钮( first 、 previous 、 next 和 last ),它们仅在适当的时候启用,创建可点击的排序列头,并向用户显示当前页号。

解决方案概览

  如前所述,解决方案的首要目标是可重用性:我们不想让开发人员必须在不同页面流中复制和粘贴大量规范代码,这样才能使解决方案正常工作。另外,解决方案需要有足够的灵活性,从而允许开发人员按照需要为单独的屏幕量身定做外观和功能。

  解决方案的关键方面在于,我们想尽可能地利用数据库的能力。因此,我们想适当调整我们使用的 SQL ,使数据库能够完成排序工作。某些数据库支持有助于使分页更加高效的专用 SQL 。如果您使用这些数据库的话,那么我建议适当地修改这个例子,以利用这些功能。然而,在本文中,我们将限制使用 ANSI SQL ,以获得最大程度的兼容性。

  因为我们使用的是 JSP ,所以自然而然浮现在脑海中的解决方案便是定制标签。定制标签允许开发人员创建可以在许多不同页面之间轻松重用的功能,而且只要处理得当,它们提供的灵活性也相当可观。一条显而易见的思路是扩展 netui:repeater 标签;不幸的是,在 WebLogic Platform 的 8.1 版本中, netui 是不可扩展的。

  幸运的是,在进一步分析问题之后,情况趋于明朗,我们不需要扩展 netui:repeater 标签。相反,通过在 netui:repeater 周围放置一个控制标签,我们可以间接地使用它。这个标签称为 repeater 程序块,它用于管理数据选择以及与该解决方案所需的其他标签进行交互。下面给出了一个使用这个标签的例子:

<rpb:repeaterBlock name="contacts" filter="<%=filter%>" >
  <netui:repeater dataSource="{pageFlow.contacts}">
    ...
  </netui:repeater>
</rpb:repeaterBlock>

  如上所示, rpb:repeaterBlock 标签有两个强制性属性: name 和 filter 。需要第一个属性 name 的原因是,我们需要能够在同一个页面上支持多个 repeater 标签实例。惟一的名称确保了任何生成的元素和脚本可以与同一页面上标签的其他实例区分开。

  由于我们要管理数据的选择,第二个属性 filter 就应运而生。解决方案要求一个持久类( RepeaterFilter 类)来充当过滤器,以确定对于排序和分页来说,数据库方面需要完成哪些工作。这个类的目的是持久化当前页面和请求之间的排序,并充当 repeaterBlock 标签和其他辅助标签之间的一种通信机制。

  因此,这个 RepeaterFilter 实例通过过滤器属性被传递给标签,这样各种动作就能按照需要自动应用到过滤器。如前所述,我们必须在请求之间持久化 RepeaterFilter , 以保存分页和排序信息。实现这一点最容易的方式是使过滤器成为页面流中的一个字段,这样它就可以与请求之间的页面流一起被序列化。

  最后,我们需要各种动作标签来封装解决方案将要提供的分页和排序功能。这包括导航标签,比如用于生成可排序头的 firstPage 、 previousPage 、 nextPage 、 lastPage 和 columnHeader 标签。这些动作标签与 repeaterBlock 标签和 RepeaterFilter 类进行通信,以便按照用户要求修改过滤器,从而生成数据的新视图。

  联系人例子的完整 JSP 代码如下所示:

<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<%@ page import="com.bea.ps.repeater.RepeaterFilter"%>
<%@ page import="com.bea.wlw.netui.pageflow.PageFlowUtils"%>
<%@ page import="contacts.ContactsController"%>
<%@ taglib uri="netui-tags-databinding.tld" prefix="netui-data"%>
<%@ taglib uri="netui-tags-html.tld" prefix="netui"%>
<%@ taglib uri="netui-tags-template.tld" prefix="netui-template"%>
<%@ taglib uri="rpb" prefix="rpb"%>
<netui:html>
  <head>
    <title>
      Contacts
    </title>
  </head>
  <body>
    <%
    ContactsController controller = (ContactsController)PageFlowUtils.getCurrentPageFlow(request);
    RepeaterFilter filter = controller.getRepeaterFilter();
    %>
    
    <netui:form action="refresh">
      <rpb:repeaterBlock name="contacts" filter="<%=filter%>"
      ascendingImage='<%=request.getContextPath()+"/images/down-arrow.gif"%>' 
      descendingImage='<%=request.getContextPath()+"/images/up-arrow.gif"%>'>
      
      <netui-data:repeater dataSource="{pageFlow.contacts}">
	<netui-data:repeaterHeader>
	  <table class="tablebody" border="1">
	    <tr>
	      <th><rpb:columnHeader field="LAST_NAME">Last Name</rpb:columnHeader></th>
	      <th><rpb:columnHeader field="FIRST_NAME">First Name</rpb:columnHeader></th>
	      <th><rpb:columnHeader field="HOME_PHONE">Home Phone</rpb:columnHeader></th>
	      <th><rpb:columnHeader field="WORK_PHONE">Work Phone</rpb:columnHeader></th>
	      <th><rpb:columnHeader field="MOBILE_PHONE">Mobile Phone</rpb:columnHeader></th>
	    </tr>
	  </netui-data:repeaterHeader>
	  <netui-data:repeaterItem>
	    <tr>
	      <td><netui:label value="{container.item.lastName}"/></td>
	      <td><netui:label value="{container.item.firstName}"/></td>
	      <td><netui:label value="{container.item.homePhone}"/></td>
	      <td><netui:label value="{container.item.workPhone}"/></td>
	      <td><netui:label value="{container.item.mobilePhone}"/></td>
	    </tr>
	  </netui-data:repeaterItem>
	  <netui-data:repeaterFooter></table></netui-data:repeaterFooter>
	</netui-data:repeater>
	<br>
	<rpb:firstPage label="First"/>
	<rpb:previousPage label="Previous"/>
	<rpb:nextPage label="Next"/>
	<rpb:lastPage label="Last"/>
	<rpb:pageNumber/>
      </rpb:repeaterBlock>
    </netui:form>
  </body>
</netui:html>

  正如上述代码中看到的那样,定制标签为布局提供了极大的灵活性,而且不会影响netui:repeater标签的使用。

幕后

  在上一部分中,我们从高层次的角度讨论了解决方案。现在我们来看看解决方案如何工作的具体细节了。正如我们已经表明的那样,代码依赖于一个叫做 repeaterBlock 的控制标签,它包装了 netui:repeater 标签。 repeaterBlock 标签提供两个重要的功能部分,这两个功能对于实现我们的目标非常关键。

  首先,它生成了两个隐藏的 HTML 字段和一个脚本块,动作标签使用脚本块与 repeaterBlock 标签通信。当用户点击像 nextPage 这样的动作时,动作通过 JavaScript 调用脚本块,同时告诉它已经被调用。脚本块把动作名称和数据复制给隐藏元素,然后提交 netui:form 。 repeaterBlock 生成的 HTML 如下:

<INPUT TYPE="HIDDEN" NAME='contacts_REPEATER_ACTION'>
<INPUT TYPE="HIDDEN" NAME='contacts_REPEATER_DATA'>
<SCRIPT>
function performcontactsRepeaterAction(action,data) {
  for(var i=0; i<document.forms.length; i++) {
    if (document.forms[i].contacts_REPEATER_ACTION!=null) {
     document.forms[i].contacts_REPEATER_ACTION.value=action
     document.forms[i].contacts_REPEATER_DATA.value=data
     document.forms[i].submit();
    }
  }
}
</SCRIPT>

  其次,当表单提交时, repeaterBlock 查找任何发出的动作,并根据过滤器来调用动作。动作修改过滤器,这样当预取数据时,它就会反映用户调用的动作效果。动作标签生成了以下 HTML :

<a href=# onClick="performcontactsRepeaterAction('SORT_ACTION','MOBILE_PHONE'); 
   return false;">Mobile Phone</a>

  正如上述代码片断中看到的那样,当点击列头时,它只会调用 repeaterBlock 父标签所生成的脚本。

  这种设计的优点是可以消除处理各种分页和排序动作通常所需的规范代码。对它进行的所有处理都是透明的,而不必明确地定义页面流动作。然而,需要了解的是,这种方法具有两个小小的限制:

  • 1. netui:form 必须有一个不执行操作的默认动作。因为 repeater 块动作提交表单,所以默认的 netui:form 动作不执行任何操作,这一点很重要;我最喜欢把这个动作标记为“ refresh ”,但是您可以随意称呼它。下图说明了刷新动作的示例联系人页面流:

    Example Contacts Page Flow

    2. 在 rpb:repeaterBlock 标签开始之后,需要预取数据,因为这是更新过滤器来反映被调用动作的地方。如果在开始处理这个标签之前就预取了数据,那么将不会更新过滤器来反映用户动作。

  这里出现的关键问题是对于访问数据库中的数据来说,过滤器是如何起作用的。前面我们阐明了 RepeaterFilter 是页面流的一个实例变量,所以在我们的例子中,页面流包含以下代码:
public class ContactsController extends PageFlowController
{
   /**
    * @common:control
    */
   private controls.Contacts contacts;
   private RepeaterFilter filter;

   /**
    * Retrieve a set of contacts based on the current filter
    */
   public Contact[] getContacts() 
   { 
     return contacts.getContacts(filter);
   }

   /**
    * Return the current filter
    */
   public RepeaterFilter getRepeaterFilter()
   {
     return filter;
   }

   /**
    * Create the initial filter with default parameters for this page flow
    */
   protected void onCreate() throws Exception
   {
     filter = new RepeaterFilter();
     filter.setSortField("LAST_NAME");
     filter.setPageSize(5);
   }

   /**
    * This method represents the point of entry into the page flow
    * @jpf:action
    * @jpf:forward name="success" path="contacts.jsp"
    */
   protected Forward begin()
   {
     return new Forward("success");
   }

   /**
    * @jpf:action
    * @jpf:forward name="success" path="contacts.jsp"
    */
   protected Forward refresh()
   {
     return new Forward("success");
   }
}

  正如上述代码中看到的那样,通过重写 onCreate() 方法,我们把 RepeaterFilter 创建为页面流的一部分。因为 RepeaterFilter 是页面流的一个字段,它将和页面流一起被序列化,从而确保当前页面和排序在请求之间保留。

  注意,在方法 getContacts() 中,我们把过滤器传递给联系人控件,这样它就可以使用过滤器中的值来预取合适的数据。在 contacts.jsp 页面中, netui:repeater 标签调用 getContacts() 方法来访问数据。联系人控件中的方法 getContacts() 如下:

/**
 * @common:operation
 */
public Contact[] getContacts(RepeaterFilter filter)
{
  //Remove this line if you do not need the last page action feature and
  //want to avoid the extra hit to the database to count rows
  filter.setRowCount(contactDB.getContactCount());
  //Get data from database control
  Iterator it =
      contactDB.getContacts(filter.getSortField(),filter.getSortDirection().getName());
  //Process data from Iterator returned by database control and return it
  return (Contact[])RepeaterHelper.processIterator(it,Contact.class,filter);
}

  这个方法仅仅利用了过滤器的必要标准,并将其传递给数据库控件来预取必要的数据。 RepeaterHelper 中有一个帮助器方法叫做 processIterator ,用于提出当前页面中我们所需要的值。如果您正以类似的方式使用数据库控件,则可以在自己的代码中使用这种方法;否则,可以编写同一类方法来处理特定机制的数据。

在数据库控件中, getContacts() 方法中的代码是标准的 SQL ,如下所示:

/**
 * @jc:sql statement::
 *  SELECT CONTACT_ID ID,FIRST_NAME FIRSTNAME,LAST_NAME LASTNAME,STREET,CITY,
 *  PROVINCE,POSTCODE,HOME_PHONE HOMEPHONE,WORK_PHONE WORKPHONE,
 *  MOBILE_PHONE MOBILEPHONE
 *  FROM CONTACTS
 *  ORDER BY {sql: field} {sql: sort}
 *  ::
 *  iterator-element-type="com.bea.ps.example.Contact"
 */
Iterator getContacts(String field, String sort);

  数据库控件在一个迭代器中返回数据,因为这是一种从数据库控件访问对象的高效方式。当从数据库控件返回一个迭代器时,它包装了一个 ResultSet ,而且当您特地从迭代器请求它时,它只创建一个对象。因此,只有当您开始请求对象时,而不是当您使用迭代器的 next() 方法通过光标移动时,才会预取数据。

  注意,这里提供的是一个普通的提取数据的例子,它可用于大多数 RDBMS 和 JDBC 驱动程序实现,但是从性能的角度来看未必是最佳的。许多 RDBMS 和 JDBC 驱动程序实现包括特别为支持分页操作而设计的功能,而且您应该尝试尽可能利用这些功能。

结束语

  本文展示了以一种高效和可重用的方式使用 netui:repeater 标签来支持分页和排序有多么容易。通过避免过多的规范代码,您可以快速而有效地在自己的项目中应用本文包含的代码,并在使用 netui:repeater 标签时提高自己的效率。

其他读物

netui:grid文档

netui:repeater文档

下载

下载文件包含几个组件:

rpb.jar —— repeater 块标签编译后的 jar 文件。

rpbsrc.zip —— repeater 块标签的源代码。

example.zip —— 联系人例子的源代码;包括控件和页面流。要使用这个例子,需要创建一个门户应用程序和一个门户 web 应用程序,把文件解压缩到门户 web 应用程序目录中,保持文件夹原封未动。

附录 1. 在自己的应用程序中使用标签

  既然我们知道解决方案是如何工作的,那么应该考虑如何将其应用于我们自己的应用程序中。做到这一点的最简单方式是了解如何一步一步地设置好一切。通过在自己的应用程序中遵循这些步骤,您将发现,使解决方案运转起来其实很简单。

步骤 1. 向应用程序中添加标签库
 第一步是向门户应用程序中添加 repeater 块定制标签。为此,只要将包含 repeater 块标签的 rpb.jar 添加到 WEB-INF/lib 目录即可。接着,将标签库引用添加到 WEB-INF/web.xml 中,如下所示:

<taglib>
  <taglib-uri>rpb</taglib-uri>
  <taglib-location>/WEB-INF/lib/rpb.jar</taglib-location>
</taglib>

步骤 2. 创建页面流
  
这里的页面流只不过是附加了一些规范代码行的标准页面流。当您创建完页面流之后,为RepeaterFilter实例添加一个成员变量,然后通过重写onCreate()方法,在页面流中创建一个过滤器实例,如下所示:

private RepeaterFilter filter;
/**
 * Create the initial filter with default parameters for this page flow
 */
protected void onCreate() throws Exception
{
  filter = new RepeaterFilter();
  filter.setSortField("LAST_NAME");
  filter.setPageSize(5);
}

步骤 3. 向页面流添加一个用于访问数据的方法
  
下一步是添加一个用于访问数据的方法,这里的数据是指要使用 netui:repeater 标签在 JSP 页面中显示的数据。此方法必须把页面流中的 RepeaterFilter 传递给数据访问程序,这样所提供的数据才能与过滤器要求的一致。在我们的联系人例子中,我们把过滤器本身从页面流传递给联系人控件,以便按照下面的代码取出正确的数据:

/**
 * Retrieve a set of contacts based on the current filter
 */
public Contact[] getContacts()
{
  return contacts.getContacts(filter);
}

  然而,需要注意的是,所提供的 repeater 块标签是独立于数据访问的;可以选择以任何方式访问数据,只要对于当前页面和排序来说,它满足过滤器的要求。 

步骤 4. JSP 页面添加标签库
  向 JSP 页面添加 repeater 块标签库,如下所示:

<%@ taglib uri="rpb" prefix="rpb"%>

步骤 5. 获得 JSP 页面中的当前过滤器
  repeaterBlock 元素需要访问 RepeaterFilter ,所以必须从 JSP 页面本身中的页面流获得一个过滤器实例。有很多方法可以做到这一点,但是首选在 JSP 本身中使用下面的代码:

<%
ContactsController controller =
 (ContactsController)PageFlowUtils.getCurrentPageFlow(request);
RepeaterFilter filter = controller.getRepeaterFilter();
%>

步骤 6. 围绕 netui:repeater 标签创建一个 netui:form 标签
  因为 repeaterBlock 提交当前的表单来调用动作,需要提交一个可用的 netui:form 标签。这个表单的默认动作应该是不执行任何操作,因为当用户调用页面或者修改排序时,我们不想调用执行处理的页面流动作。 netui:form 标签通常的出现方式类似于下面这样:

<netui:form action="refresh">
  ..
</netui:form>

  在页面流中,刷新动作的代码只不过是一个空操作方法,如下所示:

/**
 * @jpf:action
 * @jpf:forward name="success" path="contacts.jsp"
 */
protected Forward refresh()
{
  return new Forward("success");
}

步骤 7. 在需要时添加 repeater 块标签
  最后一步是在需要时简单地添加 repeater 块标签。下面给出了本文包含的 contacts.jsp 的一个例子:

<rpb:repeaterBlock name="contacts" filter="<%=filter%>"
  ascendingImage='<%=request.getContextPath()+"/images/down-arrow.gif"%>'
  descendingImage='<%=request.getContextPath()+"/images/up-arrow.gif"%>'>
  <netui-data:repeater dataSource="{pageFlow.contacts}">
    <netui-data:repeaterHeader>
      <table class="tablebody" border="1">
	<tr>
	  <th><rpb:columnHeader field="LAST_NAME">Last Name</rpb:columnHeader></th>
	  <th><rpb:columnHeader field="FIRST_NAME">First Name</rpb:columnHeader></th>
	  <th><rpb:columnHeader field="HOME_PHONE">Home Phone</rpb:columnHeader></th>
	  <th><rpb:columnHeader field="WORK_PHONE">Work Phone</rpb:columnHeader></th>
	  <th><rpb:columnHeader field="MOBILE_PHONE">Mobile Phone</rpb:columnHeader></th>
	</tr>
      </netui-data:repeaterHeader>
      <netui-data:repeaterItem>
	<tr>
	  <td><netui:label value="{container.item.lastName}"/></td>
	  <td><netui:label value="{container.item.firstName}"/></td>
	  <td><netui:label value="{container.item.homePhone}"/></td>
	  <td><netui:label value="{container.item.workPhone}"/></td>
	  <td><netui:label value="{container.item.mobilePhone}"/></td>
	</tr>
      </netui-data:repeaterItem>
      <netui-data:repeaterFooter></table></netui-data:repeaterFooter>
  </netui-data:repeater>
  <br>
  <rpb:firstPage label="First"/>
  <rpb:previousPage label="Previous"/>
  <rpb:nextPage label="Next"/>
  <rpb:lastPage label="Last"/>
  <rpb:pageNumber/>
</rpb:repeaterBlock>

步骤 8. 大功告成
  大功告成。坐下来享受您的咖啡,因为曾经繁重的一项任务现在已经变得很简单了。

  Gerald Nunn 是 BEA Systems 专业服务的业务首席顾问。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值