Paging and Sorting using the netui:repeater tag (WLP 8.1)
Abstract
A common task facing developers of today's complex web applications is displaying data to the user, and a standard user interface requirement in these tasks is enabling users to sort and page through the data. The paging requirement stems from the fact that often users search through data that is larger than can reasonably be displayed on a single page. As a result of this, the user needs to be presented with an initial subset of the data along with the capability to navigate through the remainder. Another common requirement is sorting, allowing the user to view the data in an order that most interests them.
A technical problem commonly arises while implementing such a solution: how to perform all these tasks efficiently, without loading the dataset into memory. This article shows how the netui:repeater
tag can be used to accomplish this goal.
Introduction
BEA WebLogic Workshop 8.1 provides two different tags for iterating and displaying data. These are the netui:grid
and netui:repeater
tags, respectively. The netui:grid
tag provides out-of-the-box functionality for handling paging and sorting, while the netui:repeater
tag does not. Given this, it would seem to be an easy call to always use the netui:grid
tag when paging and sorting of data are required.
Unfortunately, the netui:grid
tag has a number of limitations that prevent it from being useful in all cases. The first limitation of the netui:grid
tag is that the developer has very little control over the rendering of content in each cell. For example, you may find the netui:grid
tag frustrating if a database column contains abbreviated values but you want to display the full values.
The second limitation of the netui:grid
tag is its requirement that it can only bind to RowSets
. The problem with the RowSet used by the Workshop database control is that it does not perform well with larger sets of data since it loads every item of data into memory. For example, if your data contained a thousand rows, the RowSet would load the values for every row into memory even if you were only interested in the first ten rows. A ResultSet
, on the other hand, only loads the data that is specifically requested.
Given these two limitations, developers often struggle with how to accomplish the same paging and sorting functionality provided by the netui:grid
tag with the netui:repeater
tag. Developers may resort to code that is not particularly reusable and that requires copying and pasting a significant amount of boilerplate code between page flows to get paging and sorting with netui:repeater
working correctly.
The purpose of this article is to illustrate how to handle paging and sorting using the netui:repeater
tag in an efficient and effective manner with specific emphasis on maximizing code reuse and flexibility without sacrificing performance. While this article illustrates how to accomplish the goal using WebLogic Workshop database controls as the data fetching mechanism, the code and techniques outlined here could be easily adapted to handle data fetched from other sources.
An Example
In my experience, the best way to illustrate a solution is to use an example. For the purposes of this article we will be building a screen that displays a list of contacts in a portlet. All of the code required to run this example is included with the article. An image of this example in action appears below.
This example demonstrates how the solution implements a standard set of features that is easily reused among different page flows and JSP pages within the application. This includes features like standardized action buttons (first, previous, next, and last) that become enabled only when appropriate, creating clickable sorting column headers, and displaying the current page number to the user.
Solution Overview
As mentioned previously, the primary goal of the solution is reusability: we don't want developers to have to copy and paste a lot of boilerplate code in different page flows to make the solution work. Additionally, the solution needs to be sufficiently flexible to allow developers to tailor the appearance and functionality as required for individual screens.
The key aspect of the solution is that we want to leverage the power of the database as much as possible. Therefore, we want the database to at least do the sorting for us by tuning the SQL we use appropriately. Some databases support proprietary SQL that helps make paging more efficient. If the database you are using is one of these, then I would recommend tweaking this example appropriately to leverage that feature. However, in this article, we will limit ourselves to ANSI SQL for maximum compatibility.
Since we are working with JSP, the natural solution that springs to mind is custom tags. Custom tags enable developers to create functionality that is easily reusable among many different pages, and they provide considerable flexibility when done correctly. An obvious idea is to extend the netui:repeater
tag; unfortunately netui is not extensible in version 8.1 of the WebLogic Platform.
Fortunately, after analyzing the problem further, it becomes clear that we do not need to extend the netui:repeater
tag. Rather, we can work with it in an indirect fashion by placing a controlling tag around the netui:repeater
. This tag will be called a repeater block and its purpose is to manage the selection of data and interaction with other tags needed to drive this solution. Here is an example of using this tag:
<rpb:repeaterBlock name="contacts" filter="<%=filter%>" > <netui:repeater dataSource="{pageFlow.contacts}"> ... </netui:repeater> </rpb:repeaterBlock>
As shown above, the rpb:repeaterBlock
tag has two mandatory attributes: name and filter. The first attribute, name, is required since we need to be able to support multiple repeater tag instances on the same page. A unique name ensures that any generated elements and scripts can be kept separate from other instances of the tag on the same page.
The second attribute, filter, comes into play as we manage the selection of data. The solution requires a persistent class, the RepeaterFilter
class, to act as a filter for what needs to be done on the database side with respect to sort order and paging. The purpose of this class is to persist the current page and sort order between requests and to act as a communications mechanism between the repeaterBlock
and other subsidiary tags.
Therefore, this RepeaterFilter
instance is passed to the tag through the filter attribute so that various actions can be applied automatically to the filter as needed. As mentioned previously, we must persist the RepeaterFilter
between requests to preserve paging and sorting information. The easiest way to accomplish this is by making the filter a field in the page flow so that it will be serialized with the page flow between requests.
Finally, we need a variety of action tags to encapsulate the paging and sorting functionality that the solution will be providing. These include navigation tags such as firstPage
, previousPage
, nextPage
, lastPage
, and a columnHeader
tag for generating a sortable header. These action tags communicate with the repeaterBlock
tag and the RepeaterFilter
class to modify the filter as requested by the user to generate a new view of the data.
The complete JSP code for the contacts example appears below:
<%@ 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>
As you can see from the code above, the custom tags allow considerable flexibility in layout and do not effect the usage of the netui:repeater
tag.
Under the Hood
In the previous section we discussed the solution from a high-level perspective. Now it is time to look to under the covers to see how this solution works in detail. As we have shown, the code is dependent on a controlling tag called a repeaterBlock
that wraps a netui:repeater
tag. The repeaterBlock
tag provides two important pieces of functionality that are key to achieving our goals.
First, it generates two hidden HTML fields and a script block that is used by the action tags to communicate with the repeaterBlock
tag. When a user clicks an action like nextPage
, the action calls the script block through JavaScript telling it that it has been invoked. The script block copies the action name and data to the hidden elements and submits the netui:form
. The HTML generated by the repeaterBlock
appears as follows:
<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>
Second, when the form is submitted, the repeaterBlock
looks for any action that was posted and invokes that action against the filter. The action modifies the filter so that when the data is fetched, it reflects the effect of the action invoked by the user. The action tags generate the following HTML:
<a href=# onClick="performcontactsRepeaterAction('SORT_ACTION','MOBILE_PHONE');return false;">Mobile Phone</a>
As you can see from the above snippet, when a column header is clicked, it merely invokes the script generated by the parent repeaterBlock
tag.
This design has the benefit of eliminating the boilerplate code that would normally be required to handle the various paging and sorting actions. It is all handled transparently instead of having to explicitly define page flow actions. There are, however, two minor limitations with this approach that one needs to be aware of:
The
netui:form
must have a default action that does not perform an operation. Since the repeater block actions submit the form, it is important that the defaultnetui:form
action be one that does nothing; my preference is to label this action "refresh," however you are free to call it what you will. Below is an image of the example contacts page flow showing the refresh action:The data needs to be fetched after the start of the
rpb:repeaterBlock
tag since this is where the filter is updated to reflect invoked actions. If the data is fetched before the start of this tag is processed, then the filter will not be updated to reflect user actions.
The key question that arises at this point is how does the filter come into play with respect to accessing the data in the database? Previously, we stated that the RepeaterFilter
is an instance variable of the page flow, so in our example the page flow contains the following code:
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"); } }
As you can see in the code above, we create the RepeaterFilter
as part of the page flow by overriding the onCreate()
method. Since the RepeaterFilter
is a field of the page flow, it will be serialized with the page flow, ensuring our current page and sort order is preserved between requests.
Notice in the method getContacts()
we pass the filter to the contacts control so that it can use the values in the filter to fetch the appropriate data. The getContacts()
method is invoked by the netui:repeater
tag in the contacts.jsp page to access data. The method getContacts()
in the contacts control appears as follows:
/** * @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); }
This method merely takes the necessary criteria from the filter and passes it to a database control to fetch the necessary data. A helper method in RepeaterHelper
called processIterator
is used to pull out just the values we need for the current page. You can use this method in your own code if you are using database controls in a similar fashion; otherwise, you can write the same sort of method for handling data that is specific to the mechanism you are using.
The code in the getContacts()
method in the database control is standard SQL and appears below:
/** * @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);
The database control returns the data in an iterator as this is an efficient way of accessing objects from database controls. When you return an iterator from a database control, it wraps a ResultSet
and only creates an object when you specifically request it from the iterator. Therefore, data is only fetched when you start requesting objects.
Note that this is intended as a generic example for retrieving a page of data that should work with most RDBMS and JDBC driver implementations but is not necessarily optimal from a performance standpoint. Many RDBMS and JDBC driver implementations include features specifically designed to support paging operations, and you should attempt to leverage these features whenever possible.
Conclusion
This article shows how easy it is to enable paging and sorting with the netui:repeater
tag in an efficient and reusable manner. By avoiding excessive boilerplate code you should be able to quickly and effectively apply the code included with this article in your own projects and hopefully increase your productivity when using the netui:repeater
tag.
Additional Reading
Download
The download archive contains several components:
rpb.jar
- Compiled jar for repeater block tagsrpbsrc.zip
- Source code for repeater block tagsexample.zip
- Source code for contacts example; includes controls and pageflow. To use this, create a portal application with a portal web application, and unzip the files into the portal web application directory with folders intact.
Appendix 1. Using the Tags in Your Own Application
Now that we understand how the solution works, it's time to look at how we can put it to work in our own applications. The easiest way to do this is to simply walk through how to set up everything in a step-by-step fashion. By following these steps in your own applications, you should find it trivial to get the solution working.
Step 1. Add the tag library to your application
The first step is adding the repeater block custom tags to your own portal application. To do this, simply add the rpb.jar containing the repeater block tags to your WEB-INF/lib directory. Next, add the tag library reference to WEB-INF/web.xml as follows:
<taglib> <taglib-uri>rpb</taglib-uri> <taglib-location>/WEB-INF/lib/rpb.jar</taglib-location> </taglib>
Step 2. Create a page flow
The page flow is simply a standard page flow with a few lines of boilerplate code attached. After you have created your page flow, add a member variable for the RepeaterFilter
instance, and create an instance of the filter in the page flow by overriding the onCreate()
method as follows:
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); }
Step 3. Add a method to the page flow for accessing the data
The next step is to add a method for accessing the data that will be displayed in the JSP page using the netui:repeater
tag. This method must pass the RepeaterFilter
in the page flow to the data accessor so that the data provided is in line with what the filter requires. In our contacts example, we pass the filter itself from the page flow to the contacts control to fetch the correct data as per this code:
/** * Retrieve a set of contacts based on the current filter */ public Contact[] getContacts() { return contacts.getContacts(filter); }
Note, however, that the repeater block tags provided are independent of data access; you can access your data in any manner you choose as long as it fulfills the requirements of the filter with respect to current page and sort order.
Step 4. Add the tag library to the JSP page
Add the repeater block tag library to your JSP page as follows:
<%@ taglib uri="rpb" prefix="rpb"%>
Step 5. Retrieve the current filter in the JSP page
The repeaterBlock
element requires access to the RepeaterFilter
so you must retrieve an instance of the filter from the page flow in the JSP page itself. There are a variety of ways to do this, but my preference is to use the following code in the JSP itself:
<% ContactsController controller = (ContactsController)PageFlowUtils.getCurrentPageFlow(request); RepeaterFilter filter = controller.getRepeaterFilter(); %>
Step 6. Create a netui:form
around the netui:repeater
tag
Since the repeaterBlock
submits the current form to invoke actions, there needs to be a netui:form
tag available to be submitted. The default action of this from should do nothing since we do not want to invoke a page flow action that performs processing when the user invokes page or sort changes. The netui:form
tag would usually appear similar to this code:
<netui:form action="refresh"> .. </netui:form>
The code for the refresh action in the page flow would simply be a no-op method as follows:
/** * @jpf:action * @jpf:forward name="success" path="contacts.jsp" */ protected Forward refresh() { return new Forward("success"); }
Step 7. Add the repeater block tags as needed
The final step is simply to add the repeater block tags as needed. Below is an example from the contacts.jsp included with this article:
<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>
Step 8. Enjoy your coffee
That should be it. Sit back and enjoy your coffee as what was once an onerous task now becomes routine and mundane.