Part V - Paging, Sorting, & Filtering in OpenText Content Server

List views are an important component of the OpenText Content Server interface. In a nutshell, a list view provides a summary of related information and is often rendered in an HTML table. Consider a few examples of list views in Content Server:

  • folder browsing;
  • audit tab;
  • assignments list;
  • user lists;
  • group membership list;
  • task list;
  • LiveReport;
  • etc.

The majority of list views benefit when operations are available to help find and organize the contents of the view. Common operations are sorting, filtering, and paging. Let's review:

  • Sorting. Sorting is usually applied by clicking a column header. This causes a page refresh or AJAX request to fetch the sorted information from the server. In some situations the sort can be applied by the client (with JavaScript) without making a new server request.
  • Filtering. With large list views it can get difficult to find what you're looking for. A filter allows a user to type in some text or select a rule (e.g., a date range) to reduce the result set.
  • Paging. A single page with thousands of items provides an unfriendly browsing experience. A page with millions of items takes long to render and will likely cause the browser or server to crash. Pagination reduces the number of items on the screen, gives visual indication that more information is available, and allows the user to jump to a page by clicking a page number or "Next" button.

Unfortunately, Content Server doesn't provide a generic approach to apply these operations. Although Content Server has some of these features in their views, the solutions are proprietary to the view and can't be reused in other contexts. In other words, the basic logic behind sorting, filtering, and paging is rewritten each time it's required.

# Introducing Paginator

To address this problem I created the Paginator class, which is a component of the RHCore framework. The class wraps a RecArray and provides methods to assist with sorting, filtering, and paging. Let's jump in with an example:

// for brevity we skip error checking
RecArray recs = CAPI.Exec(prgCtx.fDbConnect.fConnection, "select * from DTree")

// construct a paginator instance
Frame paginator = $RHCore.Paginator.New(recs)

A Paginator object has a number of useful methods:

// returns the total number of Records
Integer totalNumberOfItems = paginator.count()

// set the page size to 25 items per page

// fetch the number of pages, which will depend on the pageSize
Integer pageCount = paginator.pageCount()

// apply a filter and reduce the set to items where
// the "name" column contains the string "Enterprise"

// pageCount will return a different value than before now that a
// filter has been applied
pageCount = paginator.pageCount()

// set the page number we'd like to view

// check if we have a next page (i.e., page 4)
// (this method is useful when rendering Previous/Next buttons)
Boolean nextPageExists = paginator.hasNext()

// sort the contents by name, descending order

The sorted and filtered items for the page are retrieved with the items() or iterator() method. The items() method returns the page as a RecArray while the iterator() method returns an Iterator object (see the section on iterators in Part II of this blog series).

RecArray myPagedFilteredSortedItems = paginator.items()

// or

Frame myPagedFilteredSortedIterator = paginator.iterator()

This means we can render the Paginator object in our WebLingo or RHTemplate by iterating the output of one of these methods. The developer still needs to create the user interface components to interact with these features, but this is easy to do with RHTemplate. RHTemplate has a filter, sort, and paginator filter, which can be used to apply filtering, sorting, and paging to a data set directly from within a template.

For rendering the developer can use the provided paginator.html template widget:

{% include "rhcore/paginator.html" with paginator=paginator pageParm="page" %}

This will render a basic pagination component with the page count, item count, and highlighted page in the following style. It also manages the URL generation as defined by the pageParm parameter (e.g., &page=3):

A widget is also available for creating sort headers with the appropriate URL and up/down arrows. More information on the sort header macro can be found in Part III of this blog series.

# Extensions

You may have recognized a problem with the previous example: What if the select * from DTree call returns a million rows? This would certainly cause performance problems and likely crash the server. To get around this I created a subclass of Paginator called RHQuery, which behaves like Paginator but manages everything at the database level. To the developer it has the same interface except for the constructor, which looks like this:

Frame paginator = $RHCore.RHQuery.New(prgCtx, "select * from DTree")

Everything else in the example is exactly the same. The difference is that sorting, filtering, and paging are handled by the database and makes using the control possible over a million items. Behind the scenes this is done by extending the SQL query before making the database call. Provisions are in place to prevent SQL injection and to ensure parameters are valid (e.g., the sort parameter is a valid column). This means a developer can set parameters on the Paginator object directly from a request value. For example:



Similarly, a subclass of RHQuery called LiveReportQuery is available and allows a Paginator object to be constructed from a LiveReport. The object ignores the "Record Limit" field and allows the data from a LiveReport to be punched up with filtering, sorting, and paging without having to embed any of that logic in the LiveReport itself.

# Wrapping Up

The Paginator class and its subclasses are all key-value compliant and work seamlessly with RHModel and RHTemplate. I'm using it in a number of projects and with just a few lines of a code can add paging to any list view that requires it. It's efficient, reusable, and convenient. I couldn't imagine doing it any other way.

Questions or comments? Leave a comment below.